一聚教程网:一个值得你收藏的教程网站

热门教程

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

时间:2026-06-17 09:05:48 编辑:袖梨 来源:一聚教程网

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

回答:

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

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

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

    public class Counter {
        private static int count = 0;    public static synchronized void increment() {
            count++;
        }
    }
    

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

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

    public class Counter {
        private int count = 0;    public synchronized void increment() {
            count++;
        }
    }
    

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

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

    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 或不可变

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

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

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

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

    //  错误:一个大锁保护所有操作
    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 常量池复用

    //  致命错误:不同类使用相同字符串字面量,竞争同一把锁
    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 实际上竞争同一把锁,可能导致意外阻塞和死锁。

    解决方案

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

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

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

    解决方案

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

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

    解决方案

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

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

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

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

    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 读写分离锁

    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 按哈希值分锁

    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:5][citation:13]:

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

关键细节

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

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

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

    高分回答

  • 追问 2:"为什么锁对象要用 final 修饰?"

    高分回答

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

    高分回答

  • 追问 4:"synchronized(this) 和 synchronized 方法有什么区别?"

    高分回答

  • 追问 5:"如何设计一个高并发的计数器,锁对象怎么选?"

    高分回答

  • 追问 6:"锁对象调用 hashCode() 会影响 synchronized 吗?"

    高分回答


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

面试官想要的满分总结


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

热门栏目