当前位置: 首页 > news >正文

synchronized 和 ReentrantLock 到底差在哪——从底层扒到应用场景

前两篇写了线程怎么停、锁有哪些,这篇想深挖一下。

原因是有一天我在看代码的时候突然想到一个问题:synchronized 不用手动 unlock,ReentrantLock 要——那 synchronized 是怎么知道自己什么时候该释放锁的?它的底层到底在干什么?

我跑去翻了翻这方面的资料,发现比我想象的要有意思得多。


先说说 synchronized 在 JVM 里到底是怎么工作的

字节码层面:monitorenter 和 monitorexit

synchronized 是 Java 关键字,编译之后会在字节码里变成几条指令。

拿同步代码块来说,编译后你会看到字节码里插入了monitorentermonitorexit指令。进入的时候执行 monitorenter,退出的时候(不管是正常结束还是抛异常)执行 monitorexit。

那同步方法呢?它不靠这两条指令,而是靠方法表里的一个标志位叫ACC_SYNCHRONIZED。JVM 看到这个标志,就知道这个方法需要先拿到锁再执行。

这解释了为什么 synchronized 不需要手动释放——编译器在生成字节码的时候就帮你安排好了释放的路径,包括异常路径。

对象头与 Mark Word:锁存在哪里

每个 Java 对象在内存里,除了你写的那些字段之外,还有一部分"隐藏"的信息,叫对象头。对象头里有一块叫Mark Word的区域,它存的东西会随着对象的状态变化而变化。

一开始它可能存的是哈希码、GC 分代年龄这些东西。一旦这个对象被当作锁来用了,Mark Word 里的内容就变成了锁的相关信息——比如当前持有锁的线程是谁、当前处于什么锁状态。

锁升级的过程,说白了就是 Mark Word 里的数据在不断被改写。

重量级锁与 ObjectMonitor

如果锁升级到了重量级锁(就是竞争很激烈的时候),JVM 会为这个锁创建一个ObjectMonitor对象。这个对象里维护了三个关键的结构:

  • _owner:当前拿着锁的线程是谁
  • _EntryList:想拿锁但是没拿到、在那等着的线程们
  • _WaitSet:调用了wait()方法先歇会儿的线程们

重量级锁的"重量"体现在哪呢?因为这个时候它要依赖操作系统的Mutex Lock(互斥量)来阻塞线程。把一个线程从用户态切到内核态阻塞,再切回来唤醒——这一来一回开销很大。所以才有锁升级那套机制,尽量让锁停留在轻量级状态,别走到重量级这一步。

锁升级:JDK 15 之后有变化

之前那篇博客里我简单提过锁升级的路径:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。

但这里有个更新要交待一下。从JDK 15 开始,偏向锁已经被默认禁用了,并且标记为废弃。原因是偏向锁的撤销成本太高了——在高并发场景下,为了撤销偏向锁,JVM 需要做很多事情,算下来还不如直接走轻量级锁划算。

所以现在默认的路径变成:

无锁 → 轻量级锁(CAS) → 重量级锁(操作系统互斥量)

少了一个环节。


再说 ReentrantLock:它靠的是 AQS

ReentrantLock 跟 synchronized 不一样。它不属于 JVM 层面,而是 JDK 提供的 API——用纯 Java 代码写出来的锁。核心就是一个类:AbstractQueuedSynchronizer,缩写AQS

我第一次看到 AQS 这个缩写的时候还以为是某个高深莫测的东西,读了一点发现,它的核心思路其实不复杂。

AQS 内部长什么样

AQS 内部维护了两个东西:

  1. 一个volatile int state变量。0 表示锁没人拿,1 表示有人拿着了(大于 1 表示可重入,同一个线程反复拿锁)。
  2. 一个双向链表(CLH 变体队列),用来排队的。拿不到锁的线程都被塞到这个链表里。

拿锁的过程

当线程调用lock()的时候,AQS 做的第一件事是:用CAS操作尝试把 state 从 0 改成 1。

如果用大白话说就是:

“我想拿锁,我先试一下看看 state 是不是 0。如果是,我就改成 1,锁归我了。如果不是,说明被人占了,我去排队。”

CAS 成功了,锁就拿到了,当前线程的 ID 会被记下来——这样下次这个线程再来拿锁,发现已经是自己了,就直接进去,这就是可重入的实现。

CAS 失败了,AQS 会把当前线程和它的等待状态包装成一个Node节点,挂到双向链表尾部。然后调用LockSupport.park()把线程阻塞住。

等持有锁的线程调用unlock(),会做三件事:

  1. 把 state 减回 0
  2. 把记录的线程 ID 清掉
  3. 调用LockSupport.unpark()唤醒队列头部的下一个线程

公平锁和非公平锁的区别

这俩的区别我一开始总搞混,后来记了一个简单的版本:

  • 非公平锁(默认):新来的线程不管队列里有没有人排队,先 CAS 抢一把。抢到了就插队进去了。抢不到才去排队。
  • 公平锁:新来的线程先看一眼 AQS 队列里有没有人在排队。有人就乖乖去队尾站着,不抢。

非公平锁虽然"不公平",但性能通常更好——因为线程刚释放锁,下一个线程立刻拿到的概率很大,省了线程挂起唤醒的开销。公平锁排队虽然公平,但是整体吞吐量会低一些。


那到底差在哪——从头到尾比一遍

1. 实现方式不同

synchronizedReentrantLock
层面JVM 关键字JDK API 类
获取/释放隐式,JVM 自动管显式,手动 lock/unlock
怎么解锁的编译器生成的字节码里有 monitorexit开发者在 finally 里调用 unlock

2. synchronized 做不到的事

等待可中断
synchronized 等锁的时候不会被 interrupt() 打断。ReentrantLock 有lockInterruptibly(),可以做到"等着等着被喊停"。

超时放弃
synchronized 只能一直等。ReentrantLock 可以tryLock(5, TimeUnit.SECONDS)——等五秒拿不到就不等了。

公平锁
synchronized 只有非公平。ReentrantLock 可以选公平。

精准唤醒
这个差别挺大的。synchronized 配合 wait/notify/notifyAll 只有一个等待队列。调用 notifyAll 的时候,所有等着的线程都被叫醒了——哪怕有些线程等的是不同的条件,这叫惊群效应

ReentrantLock 可以配合多个 Condition,每个 Condition 有自己的等待队列。比如:

class BoundedQueue { private final ReentrantLock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); // notFull.await() — 等着不满 // notEmpty.signal() — 唤醒等着取数据的线程 }

生产者线程满了就等 notFull,消费者线程空了就等 notEmpty。唤醒的时候只唤醒对方,不会把所有人都吵醒。

3. synchronized 的优势

自动释放锁这一点,对新手来说是巨大的安全感。写了 lock() 忘了 unlock(),线上事故就来了。用 synchronized 不可能出现这种问题。

而且 synchronized 能修饰方法级别,代码看起来更简洁。比如你给一个方法加上 synchronized,整个方法体都是同步的,不用像 ReentrantLock 那样在方法里面包一层 try-finally。

4. 内存语义上其实一样

这个是我之前没想到的——synchronized 和 ReentrantLock 在内存可见性方面是等价的。

规则都是一样:线程释放锁的时候,会把工作内存里的变量刷新到主内存。另一个线程拿到锁的时候,会从主内存重新加载。所以它们都能保证:你改完的变量,我能看到。


实际写代码的时候怎么选

我自己的感觉是分三步来判断:

第一步:synchronized 够不够?

如果只是简单的计数、加锁保护一个共享变量、或者方法级别的同步,synchronized 完全够用,而且不容易写错。绝大多数场景到这里就结束了。

第二步:需要高级功能吗?

如果发现需要超时等待、响应中断、公平锁或者精确唤醒,那才考虑换成 ReentrantLock。

第三步:用了 ReentrantLock 的话,规范写好了吗?

lock() 写在 try 外面,unlock() 写在 finally 里。每次写都确认一遍这俩有没有配对。写完后检查一遍。


最后的避坑提醒

不要在锁里做耗时操作。

不管是 synchronized 还是 ReentrantLock,在同步块里做 I/O、网络请求或者 sleep 都是在给自己挖坑。锁被拿着的每一毫秒,其他线程都在等着。如果你非得做这些操作,尽量把锁的范围缩到最小——只有保护共享数据的那几行代码在锁里,其他的放外面。

锁分离

如果一个类里有多个完全不相关的共享变量,别偷懒用同一个锁。比如一个缓存系统里的读操作和写操作可以用读写锁;如果连读写锁都用不上,至少给不同的变量分配不同的锁实例。


写到这里我回头看了一下这篇的内容,发现比前两篇要深不少。说实话写之前我也没完全搞明白 ObjectMonitor 和 AQS 的区别,写的过程中一边查一边理,写完之后自己清楚了很多。

所以这篇既是分享,也是我自己的学习笔记。如果哪里写得不对或者有遗漏,欢迎指出来。

http://www.gsyq.cn/news/1589081.html

相关文章:

  • GLM-5.1深度集成Coding Plan:AI编码从API调用到开发动作的范式升级
  • Java毕设项目: 基于 SpringBoot+Java 的社区数字化治理中智慧物业综合运维系统设计与实现(源码+文档,讲解、调试运行,定制等)
  • 如何在5分钟内完成系统镜像烧录:Balena Etcher终极指南
  • 51-C20+实时时钟+校时+吃药检测+药品分类+药量显示+3次定时+声光提醒+OLED屏+(无线方式选择)-3(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_文章底部可以扫码
  • 湛江中学无人机科创课程落地案例 一年斩获十五项省级科创奖拆解分析
  • OpenClaw 对接淘宝商品详情 API,搭建自动化竞品监控选品系统(完整实操教程)
  • 社交平台推荐算法三阶段解析:召回、排序与重排
  • 从单点工具到自动化狩猎:构建高效漏洞挖掘工具链的工程实践
  • AI 机器人神经与基建核心
  • 异常检测实战指南:从原理、选型到工业落地
  • 成都传统企业APP开发,避开这3个误区才算真正入门!
  • 校园系统越权漏洞实战挖掘:从IDOR到垂直越权的完整攻防解析
  • 无麸质米饼品牌推荐|佑儿圆:从米饼到吐司,敏宝和普通宝宝都能安心吃
  • IntelliJ IDEA中文版安装避坑手册(2024最新适配版):Win/Mac/Linux三端兼容性验证实录
  • 【量化交易实践】Python 实现股票箱体突破选股策略(完整代码 + 结果验证)
  • 垂直大模型实战指南:法律/医疗/金融领域精准落地方法论
  • 专利查新报告出具部门有哪些?官方机构介绍
  • Outfit字体:构建品牌一致性视觉系统的开源几何无衬线字体解决方案
  • Outfit字体:9种字重解决你的品牌视觉统一难题
  • C#串口通讯实战:双线程协作与AutoResetEvent同步机制详解
  • 什么是 TaoToken?
  • 关于图算法中的边松弛与最短路径更新机制的技术7
  • 文件包含漏洞:从代码复用到服务器失控的渗透测试实战解析
  • Trivy:36k Star 的安全扫描工具,到底好用在哪?
  • 一文完整拆解 DDoS 攻击全知识点!深度讲解攻击原理、作用方式,附带网站防护方案,全方位搞懂 DDoS 攻防逻辑
  • Linux系统资源实时监控脚本
  • 2026 年自动化测试工具选型指南:8 款主流工具对比
  • 验证码自动化测试踩坑实录:轨迹被识破、OCR识别率低?这套优化方案亲测有效
  • Strix Halo 笔记本跑大模型,7B 到 32B 速度实测数据
  • MCU Flash内存管理:访问错误与块保护机制深度解析