香蕉大香蕉(06敬畏市场)头像
高频八股 · Java 并发编程(4):synchronized 与锁升级封面图

高频八股 · Java 并发编程(4):synchronized 与锁升级

高频八股 · Java 并发编程(4):synchronized 与锁升级,一次讲透

写在前面

上一期讲 CAS,它是"无锁"的思路:不加锁,靠自旋重试。

但无锁只适合冲突少的场景。一旦冲突激烈、临界区又大,还是得老老实实加锁。

这一期就聊 Java 里最经典的锁——synchronized。

一、synchronized 锁的到底是谁?

synchronized 有三种用法,锁的对象完全不一样,这是第一个容易混的点:

// 1. 修饰实例方法:锁的是「当前实例 this」
public synchronized void method() { }

// 2. 修饰静态方法:锁的是「当前类的 Class 对象」
public static synchronized void staticMethod() { }

// 3. 修饰代码块:锁的是「括号里的对象」
public void block() {
    synchronized (this) { }        // 锁 this
    synchronized (Singleton.class) { }  // 锁 Class
}

一句话记牢:

  • 实例方法 → 锁 this;
  • 静态方法 → 锁 Xxx.class;
  • 代码块 → 锁括号里的那个对象。

注:synchronized 是可重入的,同一个线程再次进入同一把锁,不会被自己阻塞。

二、底层原理:对象头 + Monitor

① 对象头里的 Mark Word

每个 Java 对象在内存里都有一段"对象头",其中 Mark Word 会记录运行时的状态:哈希码、GC 分代年龄、锁状态标志、偏向线程 ID 等。

锁标志位(低 2 位)大概是这么分的:

标志位状态
01无锁 / 偏向锁
00轻量级锁
10重量级锁
11GC 标记

② Monitor(管程)

每个对象都关联一个 Monitor(C++ 里的 ObjectMonitor),它内部维护:

  • _owner:当前持有锁的线程;
  • _entryList:在门外排队等锁的线程;
  • _waitSet:调用 wait() 后进入等待的线程;
  • _recursions:重入次数。

③ 字节码层面

  • 同步代码块:编译后出现 monitorenter / monitorexit 指令;
  • 同步方法:方法上打一个 ACC_SYNCHRONIZED 标志。

无论哪种,最终都是围绕 Monitor 的 _owner 来抢锁。

三、锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁

这是 synchronized 的核心考点。为什么要升级?因为不同竞争程度,用最合适的锁最划算——能用轻的就不用重的。

3.1 偏向锁(Bias)

场景:只有一个线程反复进入同步块,压根没竞争。

做法:第一次加锁时,用 CAS 把自己的线程 ID 写进 Mark Word。之后这个线程再来,只要检查 Mark Word 里的 ID 是自己,就直接放行——连 CAS 都省了。

代价:一旦有别的线程来抢,就得撤销偏向锁,这个操作比较贵。

⚠️ 补充个新变化:JDK 15 起,偏向锁已默认禁用(JEP 374)。原因就是维护成本高、实际收益有限。面试说出来会显得你跟得上版本。

3.2 轻量级锁

场景:有竞争,但很轻(线程交替执行,几乎不重叠)。

做法:

  1. 把 Mark Word 复制到当前线程栈帧里的 Lock Record;
  2. 用 CAS 把对象头的 Mark Word 指向这个 Lock Record;
  3. 成功就拿到锁;失败就自旋重试几次。

自旋还是失败?那就只能膨胀了。

3.3 重量级锁

场景:竞争激烈,自旋也抢不到。

做法:锁膨胀成重量级锁,抢不到锁的线程进入 Monitor 的 _entryList 阻塞,需要操作系统介入调度。

代价:涉及用户态 → 内核态切换,开销最大。

3.4 升级链条(记住这张图)

无锁 → 偏向锁 → 轻量级锁 → 重量级锁

关键结论:锁只能升级,一般不能降级。(一旦膨胀成重量级,就很难退回去了)

四、锁优化:JVM 帮你偷偷做的事

① 锁消除

JIT 通过逃逸分析,发现某个锁对象根本不会逃逸出当前方法(比如方法内 new 的 StringBuffer),就直接把锁去掉,压根不加。

② 锁粗化

如果你在循环里反复加锁解锁:

for (int i = 0; i < 1000; i++) {
    synchronized (lock) { doSomething(); }
}

JVM 会把它合并成一次大锁,避免频繁加解锁的开销。

③ 自适应自旋

自旋次数不是固定的,JVM 会根据上次自旋是否成功动态调整——成功多就多转几圈,失败多就少转。

五、synchronized vs ReentrantLock

这张表是为下一期铺路的,先有个印象:

维度synchronizedReentrantLock
实现层次JVM 层(Monitor)JDK 层(AQS)
锁释放自动手动 unlock()(必须放 finally)
可中断❌✅ lockInterruptibly()
公平锁只能非公平可选公平 / 非公平
条件变量1 个(wait/notify)多个 Condition
尝试获取❌✅ tryLock()

一句话:简单场景用 synchronized(省心、不会忘解锁);需要超时、可中断、多条件、公平锁,才用 ReentrantLock。

六、高频面试题速答

Q1:synchronized 的三种用法分别锁什么?
实例方法锁 this、静态方法锁 Class 对象、代码块锁括号里的对象。

Q2:synchronized 的底层原理?
对象头 Mark Word + Monitor。代码块靠 monitorenter/monitorexit,方法靠 ACC_SYNCHRONIZED 标志。

Q3:锁升级的过程?
无锁 → 偏向锁(单线程,CAS 写线程 ID)→ 轻量级锁(有竞争,CAS 指向 Lock Record + 自旋)→ 重量级锁(竞争激烈,进入 Monitor 阻塞)。只能升不能降。

Q4:什么是锁消除和锁粗化?
锁消除:逃逸分析发现锁对象不逃逸,直接去掉锁;锁粗化:把循环里多次加解锁合并成一次。

Q5:偏向锁为什么被废弃?
撤销成本高、收益有限,JDK 15 起默认禁用。

Q6:synchronized 和 ReentrantLock 的区别?
见上表——核心是:synchronized 自动释放、简单;ReentrantLock 手动释放,但支持可中断、公平锁、多条件、tryLock。

七、一句话总结

synchronized 锁的是对象,底层靠 Mark Word + Monitor;竞争越激烈,锁就越"重"——能轻则轻,这就是锁升级的意义。


(本系列持续更新中,共 10 期,敬请关注~)

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2401_88601046/article/details/168343077

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--