【大白话说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 静态方法------类级锁

    java 复制代码
    public class Counter {
        private static int count = 0;
    
        public static synchronized void increment() {
            count++;
        }
    }

    字节码 :方法标志位 ACC_SYNCHRONIZED + ACC_STATIC,锁对象为 Counter.class

    特点:所有实例、所有线程竞争同一把锁,并发度最低,但保证类级数据一致性。

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

    java 复制代码
    public class Counter {
        private int count = 0;
    
        public synchronized void increment() {
            count++;
        }
    }

    字节码 :方法标志位 ACC_SYNCHRONIZED,锁对象为 this

    特点:不同实例之间互不干扰,并发度高于类级锁。

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

    java 复制代码
    public class Counter {
        private final Object lock = new Object();
        private int count = 0;
    
        public void increment() {
            synchronized (lock) {
                count++;
            }
        }
    }

    字节码monitorenter + monitorexit 指令,锁对象为 lock 引用指向的对象。

    特点:最灵活,可精确控制锁粒度,是生产环境的首选方式。


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

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

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

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

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

    java 复制代码
    // ❌ 错误:一个大锁保护所有操作
    public synchronized void methodA() { ... }
    public synchronized void methodB() { ... }
    // methodA 和 methodB 互不干扰,却竞争同一把锁
    
    // ✅ 正确:分离锁
    private final Object lockA = new Object();
    private final Object lockB = new Object();
    public void methodA() { synchronized(lockA) { ... } }
    public void methodB() { synchronized(lockB) { ... } }

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

    java 复制代码
    // ❌ 致命错误:不同类使用相同字符串字面量,竞争同一把锁
    public class ServiceA {
        private final String lock = "CONFIG_LOCK";
        public void update() { synchronized(lock) { ... } }
    }
    public class ServiceB {
        private final String lock = "CONFIG_LOCK";  // 常量池复用,同一对象!
        public void update() { synchronized(lock) { ... } }
    }

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

    解决方案

    java 复制代码
    // ✅ 方案一:使用 new String() 创建独立对象
    private final String lock = new String("CONFIG_LOCK");
    
    // ✅ 方案二:直接使用 Object(推荐)
    private final Object lock = new Object();
  • 3.2 陷阱二:Integer 缓存池

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

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

    解决方案

    java 复制代码
    // ✅ 使用 new Integer() 或 Object
    private final Object lock = new Object();
  • 3.3 陷阱三:this 锁的隐式共享

    java 复制代码
    // ❌ 问题:外部可直接 synchronized(obj) 获取 this 锁
    public class Counter {
        public synchronized void increment() { count++; }
    }
    // 外部代码:
    Counter c = new Counter();
    synchronized(c) {  // 获取了 Counter 实例的锁!
        c.increment(); // 重入,但语义混乱
    }

    解决方案

    java 复制代码
    // ✅ 使用私有锁对象,隐藏锁细节
    public class Counter {
        private final Object lock = new Object();
        public void increment() { synchronized(lock) { count++; } }
    }
  • 3.4 陷阱四:集合类作为锁对象

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

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

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

    java 复制代码
    public class ConcurrentHashMapV7<K, V> {
        private static final int SEGMENT_COUNT = 16;
        private final Segment<K, V>[] segments;
    
        static class Segment<K, V> {
            private final Object lock = new Object();
            private final HashMap<K, V> map = new HashMap<>();
    
            public V put(K key, V value) {
                synchronized(lock) { return map.put(key, value); }
            }
        }
    
        public V put(K key, V value) {
            int index = hash(key) % SEGMENT_COUNT;
            return segments[index].put(key, value);
        }
    }

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

  • 4.2 读写分离锁

    java 复制代码
    public class ReadWriteData {
        private final Object readLock = new Object();
        private final Object writeLock = new Object();
        private volatile int data;
    
        public int read() {
            synchronized(readLock) { return data; }
        }
    
        public void write(int value) {
            synchronized(writeLock) { data = value; }
        }
    }

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

  • 4.3 按哈希值分锁

    java 复制代码
    public class HashLock {
        private final Object[] locks = new Object[16];
    
        public HashLock() {
            for (int i = 0; i < locks.length; i++) {
                locks[i] = new Object();
            }
        }
    
        public void lock(Object key) {
            synchronized(locks[key.hashCode() % locks.length]) {
                // 操作
            }
        }
    }

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


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

锁对象的选择直接影响对象头 Mark Word 的锁状态变化 citation:5citation: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:4citation:5
  • 追问 2:"为什么锁对象要用 final 修饰?"

    高分回答

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

    例如:

    java 复制代码
    private Object lock = new Object(); // 非 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:4citation:5
  • 追问 4:"synchronized(this) 和 synchronized 方法有什么区别?"

    高分回答

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

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

    高分回答

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

    1. 单计数器 :直接用 AtomicIntegerLongAdder,无需锁对象;

    2. 多计数器(如按用户 ID 统计) :使用分段锁哈希分锁

      java 复制代码
      private final Object[] locks = new Object[16];
      public void increment(Long userId) {
          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(),偏向锁会被永久禁用,失去零开销优势。在高并发场景下,锁对象的设计往往比锁的实现更重要。


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

相关推荐
质造者1 小时前
Python 本地 RAG 实战 | Ollama+ChromaDB 实现 PDF 离线智能问答
开发语言·python·pdf·大模型·rag
小当家.1051 小时前
AIGrader:一个 AI 作业批改平台的 Java EE 课设实战
java·人工智能·java-ee
slandarer1 小时前
MATLAB | 韦恩图的高阶版: UpSet图 更新升级啦!
开发语言·matlab
devilnumber1 小时前
Lambda|行为参数化 完整精讲
java·lambda·行为参数化
garmin Chen1 小时前
Prompt工程入门:让AI按你的要求工作(3)--Prompt工程与提示词安全评测概述
java·人工智能·python·安全·prompt
Leweslyh1 小时前
3GPP TS 28.312 意图驱动管理服务 — 极详细通俗解读
开发语言·php
阿正的梦工坊1 小时前
【Rust】05-结构体、枚举与模式匹配
java·数据库·rust
阿正的梦工坊1 小时前
【Rust】10-Cargo、测试与实用开发工作流
java·rust·log4j
用户298698530141 小时前
Java 实战:Word 文档中超链接的添加与自定义技巧
java·后端