【大白话说Java面试题 第106题】【并发篇】第6题:synchronized 锁的锁对象可以是什么?
📌人工智能开发:基于Spring AI的智能对话系统设计:Java全栈实现RAG与工具调用
第6题:synchronized 锁的锁对象可以是什么?
📚回答:
- 核心考点:
synchronized锁对象的选择是并发编程中最基础也最最容易踩坑的知识点。大厂面试不会只问"锁对象可以是类对象、实例对象、任意对象",而是深入考察锁对象选择不当导致的死锁、性能瓶颈、锁粒度问题,以及String 常量池、Integer 缓存池等特殊对象的锁陷阱。面试官真正想判断的是:你是否能识别常见锁对象误用场景,并给出正确的工程实践方案。
1. 三种锁对象类型与字节码实现
| 修饰位置 | 锁对象 | 字节码实现 | 锁范围 |
|---|---|---|---|
| 静态方法 | Class对象(Example.class) | ACC_SYNCHRONIZED标志 +Class对象 | 整个类,所有实例共享 |
| 实例方法 | 当前实例(this) | ACC_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 的锁对象取决于修饰位置:
- 静态方法:锁对象是
Class对象(Example.class),所有实例共享同一把锁; - 实例方法:锁对象是
this,每个实例有独立锁; - 同步代码块:锁对象是显式指定的任意对象,最灵活。
但选择锁对象时必须遵循四个原则: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 字面量作为锁对象有两个严重问题:
- 常量池复用:Java 字符串常量池会复用相同字面量。如果两个不相关的类都使用
private final String lock = 'CONFIG',它们实际上竞争同一把锁,可能导致意外阻塞和死锁。 - String 的不可变性不等于引用不可变性:虽然 String 内容不可变,但如果使用
new String()创建独立对象,可以规避常量池复用问题。不过更推荐直接用new Object()作为锁对象,语义更清晰。
类似地,Integer 的 -128~127 缓存也会导致相同问题。" [citation:4][citation:5]
- 常量池复用:Java 字符串常量池会复用相同字面量。如果两个不相关的类都使用
追问 4:“synchronized(this) 和 synchronized 方法有什么区别?”
高分回答:
"两者在字节码层面略有不同,但锁对象都是
this,语义完全一致:synchronized方法:JVM 在方法标志位设置ACC_SYNCHRONIZED,进入方法时自动获取this锁,退出时自动释放;synchronized(this):显式在代码块前后插入monitorenter和monitorexit指令。
推荐使用synchronized(this)的场景:需要更细粒度的控制,比如只同步部分代码而非整个方法。
不推荐使用this作为锁的场景:外部代码可能直接synchronized(obj)获取this锁,导致不可控竞争。生产环境推荐用私有Object锁。" [citation:4][citation:13]
追问 5:“如何设计一个高并发的计数器,锁对象怎么选?”
高分回答:
"高并发计数器的锁对象设计要分场景:
- 单计数器:直接用
AtomicInteger或LongAdder,无需锁对象; - 多计数器(如按用户 ID 统计):使用分段锁或哈希分锁:
privatefinalObject[]locks=newObject[16];publicvoidincrement(LonguserId){synchronized(locks[userId.hashCode()%16]){// 操作}} - 读写分离场景:读操作远多于写操作,使用
ReentrantReadWriteLock替代 synchronized,读锁共享、写锁互斥。 - 极端高并发:使用
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(),偏向锁会被永久禁用,失去零开销优势。在高并发场景下,锁对象的设计往往比锁的实现更重要。
觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯
