面试被问synchronized锁升级,这5个问题答不上来直接挂!

synchronized锁升级过程:面试必问的Java并发核心机制

前言

提起Java并发编程,synchronized绝对是面试中的"常驻嘉宾"。不管是一面、二面还是三面,总有面试官会问:"synchronized是怎么实现的?锁升级过程是怎样的?"

很多人只会背"无锁→偏向锁→轻量级锁→重量级锁"这四个词,一旦追问细节就卡壳了。今天我们就来彻底搞懂synchronized的锁升级过程,不仅要知其然,更要知其所以然。

一、为什么需要锁升级?

在说锁升级之前,先思考一个问题:为什么synchronized要设计成可升级的?

想象一个场景:

  • 代码块99%的时间只有一个线程访问
  • 只有1%的时间会有多线程竞争

如果一开始就直接加重量级锁(操作系统Mutex),每次加锁都需要用户态到内核态的切换,开销巨大。但大部分时间根本不需要这么重的锁!

所以JVM的设计思路是:先用最轻量的方式加锁,发现竞争激烈了再逐步升级。这样既能保证性能,又能确保线程安全。

二、synchronized的锁状态

synchronized有四种锁状态,只能升级,不能降级

复制代码
无锁 → 偏向锁 → 轻量级锁 → 重量级锁

这四种状态是通过对象头的Mark Word来标识的:

锁状态 Mark Word存储内容 标志位
无锁 对象哈希码、GC年龄 01
偏向锁 偏向线程ID、偏向时间戳 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量(Mutex)的指针 10

三、偏向锁:无竞争时的极致优化

3.1 什么是偏向锁?

偏向锁的核心思想:"这个锁被我承包了"

当一个线程第一次访问同步块时,会在对象头的Mark Word中记录下自己的线程ID。之后这个线程再次进入同步块时,只需要判断Mark Word中的线程ID是不是自己,连CAS都不需要做。

java 复制代码
// 伪代码演示偏向锁的工作流程
if (mark_word.has_biased_lock()) {
    // 已经有偏向锁了
    if (mark_word.get_thread_id() == current_thread) {
        // 是同一个线程,直接进入同步块
        return;
    } else {
        // 有竞争,撤销偏向锁,升级为轻量级锁
        revoke_biased_lock();
        inflate_to_lightweight_lock();
    }
} else {
    // 无锁状态,尝试偏向当前线程
    if (mark_word.cas_bias(current_thread_id)) {
        // 偏向成功,记录thread_id
        return;
    }
}

3.2 偏向锁的撤销

偏向锁虽好,但不是所有场景都适用。以下情况会撤销偏向锁:

  1. 其他线程竞争:当有其他线程尝试获取同一个锁时,偏向锁被撤销
  2. 批量重偏向:当一个线程创建了大量对象并偏向,但另一个线程去使用这些对象时,会触发批量重偏向
  3. 批量撤销:如果偏向锁撤销次数过多(默认20次),JVM会认为这个类不适合偏向,批量撤销

3.3 偏向锁的开启与关闭

偏向锁在JDK 6中是默认开启的,但可能在高并发场景下反而成为负担。如果你的系统并发度很高,可以选择关闭:

bash 复制代码
# 关闭偏向锁(启动参数)
-XX:-UseBiasedLocking

# 延迟开启偏向锁(项目启动初期关闭,等warm up后再开启)
-XX:BiasedLockingStartupDelay=4000  # 4秒后开启

四、轻量级锁:自适应自旋

4.1 轻量级锁的获取

当有线程竞争偏向锁时,偏向锁会升级为轻量级锁。升级过程如下:

  1. 线程在栈帧中创建锁记录(Lock Record)
  2. 将Mark Word复制到锁记录中
  3. 使用CAS尝试将对象头的Mark Word指向栈帧中的锁记录
java 复制代码
// 轻量级锁获取伪代码
public void lock(Object obj) {
    // 在当前线程的栈帧中创建锁记录
    LockRecord lockRecord = new LockRecord();
    lockRecord.setMarkWord(obj.getMarkWord());
    
    // CAS尝试将对象头指向当前线程的锁记录
    if (obj.getMarkWord().casSetMarkWord(lockRecord)) {
        // 加锁成功!
        return;
    }
    
    // CAS失败,说明有竞争,尝试自旋等待
    int spinCount = 0;
    while (true) {
        if (obj.getMarkWord().casSetMarkWord(lockRecord)) {
            return;
        }
        spinCount++;
        if (spinCount > MAX_SPIN_COUNT) {
            // 自旋次数过多,升级为重量级锁
            inflate_to_heavyweight_lock();
            return;
        }
        // CPU空转等待
        for (int i = 0; i < 10; i++) {
            // 自旋等待
        }
    }
}

4.2 轻量级锁的解锁

解锁时,使用CAS将Mark Word恢复回去:

java 复制代码
public void unlock(Object obj) {
    LockRecord lockRecord = findCurrentThreadLockRecord();
    if (lockRecord.getMarkWord() == obj.getMarkWord()) {
        // 尝试用CAS恢复Mark Word
        obj.setMarkWord(lockRecord.getOriginalMarkWord());
    } else {
        // 说明有竞争,释放锁并唤醒等待线程
        release_and_notify_waiters();
    }
}

4.3 自旋锁的优化:自适应自旋

固定自旋次数有个问题:自旋次数太多会浪费CPU,自旋次数太少又会在真正需要等待时放弃。

JDK 6引入了自适应自旋(Adaptive Spinning)

  • 如果同一个锁上次自旋等待成功了,下次自旋次数会增加
  • 如果一个锁很少自旋成功,可能直接就不自旋了
  • 这种"自适应"让JVM能够根据实际运行情况动态调整

五、重量级锁:操作系统层面的互斥

5.1 为什么需要重量级锁?

当自旋次数过多,或者竞争非常激烈时,轻量级锁会升级为重量级锁。重量级锁会让线程进入阻塞(Blocked)状态,由操作系统负责线程调度。

关键点:重量级锁的加解锁不需要CPU忙等待,而是让线程睡眠等待,避免了无意义的CPU消耗。

5.2 Monitor对象

在JVM中,重量级锁通过**Monitor(监视器锁)**实现。每个Java对象都可以关联一个Monitor:

复制代码
Monitor结构:
├── Owner:记录拥有锁的线程
├── WaitSet:调用wait()方法后等待的线程队列
├── EntryList:等待获取锁的线程队列(阻塞队列)
└── Recursion:重入次数计数

5.3 重量级锁的工作流程

java 复制代码
// 进入monitorenter指令对应的逻辑
public void monitorenter(Object obj) {
    Monitor monitor = obj.getMonitor();
    
    if (monitor.owner == null) {
        // 没有人持有锁,当前线程获取
        monitor.owner = current_thread;
        monitor.recursion = 1;
    } else if (monitor.owner == current_thread) {
        // 重入!计数器+1
        monitor.recursion++;
    } else {
        // 被其他线程持有,当前线程阻塞等待
        current_thread.block();
        monitor.entryList.add(current_thread);
    }
}

// 退出monitorexit指令对应的逻辑
public void monitorexit(Object obj) {
    Monitor monitor = obj.getMonitor();
    
    if (monitor.recursion > 1) {
        monitor.recursion--;
    } else {
        monitor.owner = null;
        monitor.recursion = 0;
        // 唤醒一个等待线程
        if (!monitor.entryList.isEmpty()) {
            Thread next = monitor.entryList.removeFirst();
            next.unpark();  // 唤醒线程
        }
    }
}

六、锁升级的实际案例

案例:Spring Bean初始化的并发问题

java 复制代码
@Service
public class UserService {
    private UserDao userDao;
    
    @Autowired
    public void setUserDao(UserDao userDao) {
        // 这里可能存在并发问题
        this.userDao = userDao;
    }
}

在Spring容器初始化时,如果有多个Bean同时引用UserService并设置依赖,可能会触发锁升级过程:

  1. 初始状态:无锁状态
  2. 第一个线程:对象头Mark Word设置为偏向锁,指向第一个线程
  3. 第二个线程竞争:偏向锁撤销 → 升级为轻量级锁 → 自旋等待
  4. 竞争加剧:轻量级锁膨胀 → 重量级锁 → 线程阻塞

性能对比

锁类型 适用场景 加锁开销 解锁开销 CPU消耗
偏向锁 单线程、无竞争 最快(仅判断) 最快
轻量级锁 少量线程、短暂竞争 较快(CAS) 较快(CAS) 少量自旋
重量级锁 多线程、激烈竞争 慢(系统调用) 慢(系统调用) 无(阻塞)

七、实战建议

7.1 如何选择合适的锁策略

java 复制代码
// 场景1:单线程使用,完全无竞争
// 偏向锁最优
public class SingleThreadCache {
    private final Map<String, Object> cache = new HashMap<>();
    
    public synchronized Object get(String key) {
        return cache.get(key);
    }
}

// 场景2:短时并发,竞争不激烈
// 轻量级锁 + 自旋优化
public class ShortLockExample {
    private final AtomicInteger counter = new AtomicInteger();
    
    public void increment() {
        // 内部使用轻量级锁优化
        counter.incrementAndGet();
    }
}

// 场景3:高并发、长时间持锁
// 重量级锁,让线程睡眠
public class HeavyLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    private final List<String> list = new ArrayList<>();
    
    public void processBatch(List<String> batch) {
        lock.lock();
        try {
            // 耗时操作
            list.addAll(batch);
            Thread.sleep(1000);  // 模拟耗时
        } finally {
            lock.unlock();
        }
    }
}

7.2 减少锁粒度

java 复制代码
// 不推荐:粗粒度锁,整个集合锁住
public class BadExample {
    private final Map<String, Object> map = new ConcurrentHashMap<>();
    
    public synchronized void putAll(Map<String, Object> newMap) {
        map.putAll(newMap);  // 长时间持锁
    }
}

// 推荐:分段锁(ConcurrentHashMap原理)
public class GoodExample {
    private final Map<String, Object> map = new ConcurrentHashMap<>();
    
    public void putAll(Map<String, Object> newMap) {
        // 分段put,每次只锁一个桶
        newMap.forEach((key, value) -> {
            map.put(key, value);
        });
    }
}

7.3 JVM参数调优建议

bash 复制代码
# 高并发场景
-XX:+UsebiasedLocking=false      # 关闭偏向锁
-XX:PreBlockSpin=10              # 自旋次数(JDK 6用)
-XX:+UseCondCardMark            # 减少伪共享

# 追求低延迟
-XX:+UseG1GC                    # G1收集器
-XX:MaxGCPauseMillis=200        # 最大GC停顿

# 大内存场景
-XX:+UseZGC                      # ZGC低延迟收集器

八、总结

synchronized的锁升级机制是JVM的自适应优化策略 ,核心目标是用最轻量的方式处理无竞争,用最稳妥的方式处理激烈竞争

  1. 偏向锁:单线程独享,同一线程反复进入同步块时几乎零开销
  2. 轻量级锁:少量线程竞争时,通过CAS + 自旋避免线程切换
  3. 重量级锁:激烈竞争时,让线程真正睡眠,由OS调度

理解这个机制,不仅能帮助我们在面试中回答好问题,更能在实际工作中写出更高性能的并发代码。

记住:锁不是洪水猛兽,选对锁策略就能鱼与熊掌兼得。

相关推荐
姚青&1 小时前
测试技术体系
java·python
南境十里·墨染春水2 小时前
C++日志 2——实现单线程日志系统
java·jvm·c++
布吉岛的石头2 小时前
微服务网关统一鉴权、限流、日志实战
java·spring·微服务
超级无敌葛大侠2 小时前
Redis主从复制
java·redis
殷紫川3 小时前
90% Java 开发都踩过坑的 @Resource 与 @Autowired
java
kybs19913 小时前
springboot租车系统--附源码68701
java·hadoop·spring boot·python·django·asp.net·php
过期动态4 小时前
MySQL中的约束
android·java·数据库·spring boot·mysql
wxin_VXbishe4 小时前
springboot新能源车充电站管理系统小程序-计算机毕业设计源码29213
java·c++·spring boot·python·spring·django·php