当前位置: 首页 > news >正文

【大白话说Java面试题 第106题】【并发篇】第6题:synchronized 锁的锁对象可以是什么?

📌人工智能开发:基于Spring AI的智能对话系统设计:Java全栈实现RAG与工具调用

第6题:synchronized 锁的锁对象可以是什么?

📚回答:

  • 核心考点
    synchronized锁对象的选择是并发编程中最基础也最最容易踩坑的知识点。大厂面试不会只问"锁对象可以是类对象、实例对象、任意对象",而是深入考察锁对象选择不当导致的死锁、性能瓶颈、锁粒度问题,以及String 常量池、Integer 缓存池等特殊对象的锁陷阱。面试官真正想判断的是:你是否能识别常见锁对象误用场景,并给出正确的工程实践方案。

1. 三种锁对象类型与字节码实现
修饰位置锁对象字节码实现锁范围
静态方法Class对象(Example.classACC_SYNCHRONIZED标志 +Class对象整个类,所有实例共享
实例方法当前实例(thisACC_SYNCHRONIZED标志 +this引用单个实例
同步代码块显式指定的任意对象monitorenter+monitorexit代码块范围
  • 1.1 静态方法——类级锁

    publicclassCounter{privatestaticintcount=0;publicstaticsynchronizedvoidincrement(){count++;}}

    字节码:方法标志位ACC_SYNCHRONIZED+ACC_STATIC,锁对象为Counter.class
    特点:所有实例、所有线程竞争同一把锁,并发度最低,但保证类级数据一致性。

  • 1.2 实例方法——对象级锁

    publicclassCounter{privateintcount=0;publicsynchronizedvoidincrement(){count++;}}

    字节码:方法标志位ACC_SYNCHRONIZED,锁对象为this
    特点:不同实例之间互不干扰,并发度高于类级锁。

  • 1.3 同步代码块——灵活指定

    publicclassCounter{privatefinalObjectlock=newObject();privateintcount=0;publicvoidincrement(){synchronized(lock){count++;}}}

    字节码monitorenter+monitorexit指令,锁对象为lock引用指向的对象。
    特点:最灵活,可精确控制锁粒度,是生产环境的首选方式。


2. 锁对象选择的五大原则
  • 2.1 原则一:锁对象必须是 final 或不可变

    // ❌ 错误:锁对象引用可变privateObjectlock=newObject();publicvoidmethod(){synchronized(lock){...}}// 某处执行 lock = new Object(); → 两个线程持有不同锁,同步失效// ✅ 正确:final 保证引用不可变privatefinalObjectlock=newObject();
  • 2.2 原则二:锁对象必须是私有的

    // ❌ 错误:外部可获取锁对象,导致不可控竞争publicfinalObjectlock=newObject();// 外部代码:synchronized(counter.lock) { ... } → 不可控死锁// ✅ 正确:私有 + finalprivatefinalObjectlock=newObject();
  • 2.3 原则三:避免使用可变对象作为锁

    // ❌ 错误:StringBuilder 内容变化后 hashCode 变化,但锁对象引用没变privatefinalStringBuilderlock=newStringBuilder();// 虽然引用 final,但 StringBuilder 本身可变,语义混乱// ✅ 正确:使用专门的 Object 实例privatefinalObjectlock=newObject();
  • 2.4 原则四:避免使用可被外部访问的对象作为锁

    // ❌ 错误:使用字符串字面量(常量池复用)privatefinalStringlock="LOCK";// 其他类也可能用 "LOCK" 作为锁 → 意外竞争// ✅ 正确:new String("LOCK") 或直接用 ObjectprivatefinalObjectlock=newObject();
  • 2.5 原则五:细粒度锁优于粗粒度锁

    // ❌ 错误:一个大锁保护所有操作publicsynchronizedvoidmethodA(){...}publicsynchronizedvoidmethodB(){...}// methodA 和 methodB 互不干扰,却竞争同一把锁// ✅ 正确:分离锁privatefinalObjectlockA=newObject();privatefinalObjectlockB=newObject();publicvoidmethodA(){synchronized(lockA){...}}publicvoidmethodB(){synchronized(lockB){...}}

3. 常见锁对象陷阱与避坑指南
  • 3.1 陷阱一:String 常量池复用

    // ❌ 致命错误:不同类使用相同字符串字面量,竞争同一把锁publicclassServiceA{privatefinalStringlock="CONFIG_LOCK";publicvoidupdate(){synchronized(lock){...}}}publicclassServiceB{privatefinalStringlock="CONFIG_LOCK";// 常量池复用,同一对象!publicvoidupdate(){synchronized(lock){...}}}

    原理:Java 字符串常量池会复用相同字面量,"CONFIG_LOCK"在 JVM 中只有一份。ServiceA 和 ServiceB 实际上竞争同一把锁,可能导致意外阻塞和死锁。

    解决方案

    // ✅ 方案一:使用 new String() 创建独立对象privatefinalStringlock=newString("CONFIG_LOCK");// ✅ 方案二:直接使用 Object(推荐)privatefinalObjectlock=newObject();
  • 3.2 陷阱二:Integer 缓存池

    // ❌ 致命错误:Integer 缓存导致锁对象相同privatefinalIntegerlock=100;// -128~127 缓存范围内// 其他类:private final Integer anotherLock = 100; → 同一对象!

    原理Integer.valueOf()对 -128~127 有缓存,相同值返回同一对象。

    解决方案

    // ✅ 使用 new Integer() 或 ObjectprivatefinalObjectlock=newObject();
  • 3.3 陷阱三:this 锁的隐式共享

    // ❌ 问题:外部可直接 synchronized(obj) 获取 this 锁publicclassCounter{publicsynchronizedvoidincrement(){count++;}}// 外部代码:Counterc=newCounter();synchronized(c){// 获取了 Counter 实例的锁!c.increment();// 重入,但语义混乱}

    解决方案

    // ✅ 使用私有锁对象,隐藏锁细节publicclassCounter{privatefinalObjectlock=newObject();publicvoidincrement(){synchronized(lock){count++;}}}
  • 3.4 陷阱四:集合类作为锁对象

    // ❌ 问题:Collections.synchronizedList 的锁就是 list 本身List<String>list=Collections.synchronizedList(newArrayList<>());synchronized(list){// 正确,与 synchronizedList 内部锁一致for(Strings:list){...}// 迭代必须外部同步}// 但如果用其他对象锁,就无法保护 list 的内部操作
  • 3.5 陷阱五:Class 对象的隐式竞争

    // ❌ 问题:反射和同步都可能锁定 Class 对象publicstaticsynchronizedvoidmethodA(){...}// 外部代码:synchronized(Example.class){// 获取了 Class 锁!// 此时 methodA 被阻塞}

4. 高级锁对象设计模式
  • 4.1 分段锁(Segment Lock)

    publicclassConcurrentHashMapV7<K,V>{privatestaticfinalintSEGMENT_COUNT=16;privatefinalSegment<K,V>[]segments;staticclassSegment<K,V>{privatefinalObjectlock=newObject();privatefinalHashMap<K,V>map=newHashMap<>();publicVput(Kkey,Vvalue){synchronized(lock){returnmap.put(key,value);}}}publicVput(Kkey,Vvalue){intindex=hash(key)%SEGMENT_COUNT;returnsegments[index].put(key,value);}}

    原理:将数据分成多个段,每段独立加锁,不同段的写操作可并行。JDK 7 的ConcurrentHashMap采用此设计 [citation:4]。

  • 4.2 读写分离锁

    publicclassReadWriteData{privatefinalObjectreadLock=newObject();privatefinalObjectwriteLock=newObject();privatevolatileintdata;publicintread(){synchronized(readLock){returndata;}}publicvoidwrite(intvalue){synchronized(writeLock){data=value;}}}

    注意:此示例中读锁和写锁分离,但读操作不互斥(多个线程可同时读)。更完善的实现应使用ReentrantReadWriteLock

  • 4.3 按哈希值分锁

    publicclassHashLock{privatefinalObject[]locks=newObject[16];publicHashLock(){for(inti=0;i<locks.length;i++){locks[i]=newObject();}}publicvoidlock(Objectkey){synchronized(locks[key.hashCode()%locks.length]){// 操作}}}

    适用场景:按用户 ID、订单 ID 等维度加锁,相同 ID 的操作串行,不同 ID 的操作并行。


5. 锁对象与对象头 Mark Word 的关系

锁对象的选择直接影响对象头 Mark Word 的锁状态变化 [citation:5][citation:13]:

锁对象类型Mark Word 初始状态锁升级路径
普通new Object()无锁(001)无锁 → 偏向锁 → 轻量级锁 → 重量级锁
Class对象无锁(001)同上,但类对象通常长期存活,偏向锁收益低
已计算 hashCode 的对象无锁(001),不可偏向无锁 → 轻量级锁 → 重量级锁(跳过偏向锁)

关键细节

  • 调用hashCode()会占用 Mark Word 的 31 位空间,导致无法使用偏向锁(偏向锁需要存储线程 ID);
  • 如果锁对象在同步块内调用了hashCode(),JVM 会撤销偏向锁,升级为轻量级锁 [citation:13]。

6. 面试官追问与高分回答模板
  • 追问 1:“synchronized 的锁对象可以是什么?”

    低分回答:“类对象、实例对象、任意对象。”(没有区分场景和陷阱)

    高分回答

    "synchronized 的锁对象取决于修饰位置:

    1. 静态方法:锁对象是Class对象(Example.class),所有实例共享同一把锁;
    2. 实例方法:锁对象是this,每个实例有独立锁;
    3. 同步代码块:锁对象是显式指定的任意对象,最灵活。
      但选择锁对象时必须遵循四个原则:final 引用不可变、私有不可外部访问、避免 String/Integer 常量池复用、粒度尽量细。生产环境推荐用private final Object lock = new Object(),避免使用this或类对象,防止外部意外竞争。" [citation:4][citation:5]
  • 追问 2:“为什么锁对象要用 final 修饰?”

    高分回答

    "锁对象必须用final修饰,核心原因是保证引用不可变。如果锁对象引用被修改,两个线程可能持有不同的锁对象,导致同步完全失效。
    例如:

    privateObjectlock=newObject();// 非 final// 线程 A:synchronized(lock) { ... }// 某处执行 lock = new Object();// 线程 B:synchronized(lock) { ... } // 持有的是新锁,与线程 A 不互斥

    使用final可以在编译期检查引用是否被修改,从源头避免此类 Bug。" [citation:4]

  • 追问 3:“用 String 作为锁对象有什么问题?”

    高分回答

    "用 String 字面量作为锁对象有两个严重问题:

    1. 常量池复用:Java 字符串常量池会复用相同字面量。如果两个不相关的类都使用private final String lock = 'CONFIG',它们实际上竞争同一把锁,可能导致意外阻塞和死锁。
    2. String 的不可变性不等于引用不可变性:虽然 String 内容不可变,但如果使用new String()创建独立对象,可以规避常量池复用问题。不过更推荐直接用new Object()作为锁对象,语义更清晰。
      类似地,Integer 的 -128~127 缓存也会导致相同问题。" [citation:4][citation:5]
  • 追问 4:“synchronized(this) 和 synchronized 方法有什么区别?”

    高分回答

    "两者在字节码层面略有不同,但锁对象都是this,语义完全一致:

    • synchronized方法:JVM 在方法标志位设置ACC_SYNCHRONIZED,进入方法时自动获取this锁,退出时自动释放;
    • synchronized(this):显式在代码块前后插入monitorentermonitorexit指令。
      推荐使用synchronized(this)的场景:需要更细粒度的控制,比如只同步部分代码而非整个方法。
      不推荐使用this作为锁的场景:外部代码可能直接synchronized(obj)获取this锁,导致不可控竞争。生产环境推荐用私有Object锁。" [citation:4][citation:13]
  • 追问 5:“如何设计一个高并发的计数器,锁对象怎么选?”

    高分回答

    "高并发计数器的锁对象设计要分场景:

    1. 单计数器:直接用AtomicIntegerLongAdder,无需锁对象;
    2. 多计数器(如按用户 ID 统计):使用分段锁哈希分锁
      privatefinalObject[]locks=newObject[16];publicvoidincrement(LonguserId){synchronized(locks[userId.hashCode()%16]){// 操作}}
    3. 读写分离场景:读操作远多于写操作,使用ReentrantReadWriteLock替代 synchronized,读锁共享、写锁互斥。
    4. 极端高并发:使用LongAdder(分段累加)或Striped64(JDK 内部实现),完全无锁。
      核心原则:锁的粒度要匹配数据的粒度。如果数据可以分区,锁也应该分区。" [citation:4]
  • 追问 6:“锁对象调用 hashCode() 会影响 synchronized 吗?”

    高分回答

    “会,而且影响很严重。调用hashCode()会占用对象头 Mark Word 的 31 位空间,而偏向锁需要在这 31 位中存储线程 ID(54 位)和 epoch(2 位)。
    如果锁对象在同步块内或之前调用了hashCode(),JVM 会撤销偏向锁,后续该对象的 synchronized 直接进入轻量级锁逻辑,失去偏向锁的零开销优势。
    源码层面,HotSpot 的biasedLocking.cpp中有明确逻辑:当对象已计算 identity hashCode 时,偏向锁尝试会失败,直接走轻量级锁路径。
    工程建议:如果确定对象会作为锁使用,避免调用其hashCode();如果必须计算哈希,考虑使用独立的Object作为锁,而非业务对象本身。” [citation:13]


7. 方案选型速查表
场景推荐锁对象避坑要点
简单实例同步private final Object lock = new Object()不要用this,防止外部竞争
静态数据同步private static final Object lock = new Object()不要用Class对象,防止反射竞争
类级方法同步synchronized(Xxx.class)注意与反射锁的冲突
按 ID 分锁Object[] locks哈希分桶桶数量要合理,避免哈希冲突
分段锁每段独立的Object段数 = 2 的幂次,方便位运算取模
读写分离ReentrantReadWriteLock不要用两个synchronized对象模拟
高并发计数LongAdder/AtomicInteger不要用synchronized

💡面试官想要的满分总结

synchronized锁对象的选择不是"能用就行",而是并发编程正确性的第一道防线。核心原则可以总结为“私有、final、专用、细粒度”八字诀:

  • 私有:锁对象必须private,防止外部不可控竞争;
  • final:引用必须不可变,防止同步失效;
  • 专用:锁对象应专门创建(new Object()),不要用业务对象、String 字面量、Integer 缓存值;
  • 细粒度:锁的范围尽量小,能用代码块不用方法,能分段不分全局。

最常见的陷阱是String 常量池复用Integer 缓存池复用,不同类使用相同字面量或缓存值作为锁,会导致意外的全局竞争。生产环境推荐统一使用private final Object lock = new Object()模式,简单、安全、语义清晰。

最后记住:锁对象的选择直接影响对象头 Mark Word 的锁状态。如果锁对象调用了hashCode(),偏向锁会被永久禁用,失去零开销优势。在高并发场景下,锁对象的设计往往比锁的实现更重要。


觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯

http://www.gsyq.cn/news/1502710.html

相关文章:

  • 用C语言手搓一个Windows经典扫雷:从二维数组到完整游戏逻辑的保姆级实现
  • 语义嵌入空间中的概念生成轨迹分析与应用
  • 避开STC8H IAP开发的那些坑:从官方例程到稳定可用的串口不停电下载代码
  • 【大白话说Java面试题 第107题】【并发篇】第7题:说说 Lock 锁?
  • 用Raspberry Pi Pico做个便携MP3播放器:SD卡+I2S音频模块完整接线与代码解析
  • 手把手复现:用Python仿真5G NR的CPE估计与补偿流程(附代码解读)
  • 终极手机号码定位系统:3步实现免费地理位置查询
  • 突破传统文献管理:Zotero-GPT如何用AI重塑学术工作流
  • Spring 零基础入门到进阶 JdbcTemplate 62-64
  • Apache CXF 3.1.18 命令行工具集:含 WSDL/Java 双向生成、JAX-WS/JAX-RS 运行支持与企业级安全组件
  • 2026年进口alloy825靠谱品牌推荐 - myqiye
  • C++实战:如何用现代C++(C++17/20)优雅地封装一个SHA-256工具类
  • 嵌入式Linux驱动开发 —— 从DTS到代码的桥梁与简单OF系列API(5)
  • 英雄联盟自动化工具箱:5个核心功能提升游戏效率
  • 从原理到代码:手把手用Python复现D-InSAR二轨法核心流程(附Jupyter Notebook)
  • MATLAB人脸考勤工具包:摄像头实时识别+GUI操作+打卡记录自动生成
  • 别再死记硬背Zookeeper命令了!用Curator 5.5.0 + Spring Boot 3.x实战分布式锁(附12306抢票源码)
  • 别再硬算!用Python的SciPy库5行代码搞定‘翻译任务分配’这类指派问题
  • 威海黄金回收避坑指南 2026年6月最新金价与靠谱店铺推荐 - 余生黄金回收
  • 独立开发者必看:如何用 Claude 快速构建一个 Chrome 插件原型 | 实战攻略
  • 致远OA漏洞检测终极指南:12大安全漏洞一键扫描与利用
  • 用 Rust 写 AI Agent 是什么体验?ADK-Rust 框架深度解析
  • MATLAB车牌识别小工具:带GUI界面,支持本地BMP图一键识别与字符高亮显示
  • 2026年成都专线物流公司排行:成都零担物流/成都上门接货的物流公司/成都专线托运/五大服务商核心能力对比 - 优质品牌商家
  • AVI视频一键拆解成单帧图片的小巧Windows工具
  • 2026年6月博物馆展柜定制厂家技术分享:靠谱选择与实测标准 - 奔跑123
  • 2026年最火的鱼蛙火锅加盟品牌排行榜单 - 品牌排行榜
  • 铜川各区旧黄金怎么卖才划算 2026回收防坑干货指南 - 余生黄金回收
  • 拒绝被淘汰:基于大模型Agent的全栈临床科研新范式,医生如何抢占学术先机?
  • TMS320F28377D CLA+FPU实战:手把手教你搞定1024点FFT(附完整源码)