7分钟彻底搞懂Java各种锁
原视频地址【【程序员必看】Java各种锁详解:7分钟彻底搞懂Java各种锁!】https://www.bilibili.com/video/BV1yzs3zQEvD?vd_source=46212c2d164ea5464f361bff483cf8f6

1.悲观锁vs乐观锁

这一组锁是根据并发冲突的假设不同而进行区分,我们用上厕所来理解
悲观锁是被害妄想症(总假设会冲突),它就会锁上门再办事(先加锁再操作),java中我们熟悉的synchronized和Reentrantlock就是这种类型,只要一个线程抢到了锁,别的线程就只能阻塞等待,这种锁用在像秒杀减库存这种场景特别合适,虽然有点笨重但胜在绝对安全
那乐观锁呢,正好相反,它去上厕所,门都不锁,直接推门进去。等办完事出来检查一下,我进来时放的纸巾还是原来的品牌吗。如何还是,说明没人来过,万事大吉。如果被人换了,之前的就是白上了,需要重新再来一次。这个检查并替换的动作就是java中的CAS(Compare-And-Swap),咱们常用的AtomicInterger、AtomicLong这些原子类就是靠的这手绝活,它特别适合更新帖子阅读数这种竞争不太激烈的场景,性能起飞。不过它也有个坑就是经典的ABA问题。就是你女朋友找你分手了,她又找了个新男友,然后又分手了,最后来找你复合,她还是那个她,但中间发生过啥,你还真不知道。想解决也很简单,加个版本号就行,AtomicStampedReference就是干这个的

口述版本:
悲观锁:
悲观锁假设冲突总是会发生,因此每次操作时都加锁,确保没有其他线程干扰。
synchronized和ReentrantLock都是悲观锁的实现,适合数据一致性要求高的场景,但性能较低,因为每个线程都需要等待锁释放。
乐观锁:
乐观锁假设冲突不会发生,线程不加锁,操作前后检查数据是否被修改。如果没被修改,就完成操作;如果被修改,就重新尝试。CAS(Compare-And-Swap)是乐观锁的典型实现,像
AtomicInteger就是通过 CAS 实现的。适合冲突较少的场景,但存在ABA问题,可以通过版本号来解决。
2.公平锁vs非公平锁

这一组是根据排队的规矩来分的,公平锁就像银行叫号,主打的就是先来先到,非公平锁就像路边招手打车,谁快谁上。
公平锁的优点:童叟无欺,确保每个线程都有出头之日,但是坏处也很明显,银行叫号中一个老太太办业务又慢又麻烦,其他人也只能在那之后乖乖等着,队伍不长才怪(吞吐量低),常用的ReentrantLock可以添加一个布尔型的参数true就表示采用的公平锁
非公平锁的优点:就像路边打车他才不管有没有人排队,谁动作快抢到了车谁就上,这么做的好处很明显,相比于公平锁省去了叫号、线程唤醒的麻烦,整个系统吞吐量一下就上去了,而锁的最大敌人就是吞吐量,所以ReentrantLock默认就是非公平锁,同样的synchronized也是默认非公平锁,因此除非你的业务有严格的先后顺序要求,否则就用默认的非公平锁,因为它更快


口述版本:
公平锁:
公平锁保证线程按照请求锁的顺序来获取锁,先来先得。一个典型的例子就是
ReentrantLock(true),它通过队列来保证每个线程都能公平地获取锁。适合需要严格控制线程获取顺序的场景,但性能相对较低,因为线程管理开销较大。
非公平锁:
非公平锁允许线程在竞争时插队,后到的线程有可能抢到锁。ReentrantLock 默认就是非公平锁,性能较高,因为不需要维护严格的线程顺序。适合高并发、吞吐量要求高的场景,但有可能导致某些线程长时间无法获取锁(线程饥饿)。
3.排他锁vs共享锁

排他锁和共享锁,它们的核心区别就是是否允许有线程并发。
排他锁(写锁)顾名思义就是排斥他人,它就像你去KTV的包间,你一个人进去了,不管你在里面唱歌跳舞还是睡觉,反正门一关谁也别想进来。在系统当中这就是独占锁,一个线程把资源占住了,其他的读写操作全部排斥,都只能乖乖等着,synchronized和ReentrantLock都是这种锁
但有时候我们不需要这么霸道,比如大家一起看报纸,这就是一种不影响报纸内容的读操作,多少人看都无所谓这就是共享锁(读锁),一个线程占住了读锁其他线程也可以同样拿到读锁,这就是读读并发,但是这个时候如果想要在报纸上涂鸦,这就变成了改变报纸内容的写操作了,他必须得所有看书的人都走了才行,而且他涂鸦的时候谁都不能看。java你想实现共享锁也很容易,可以用ReentrantReadWriteLock,这个强大的工具,它里面就同时维护了读锁和写锁,在读多写少的场景,更多的是使用读锁,能让并发性能原地起飞,这对于性能调优非常的重要

| 对比项 | 排他锁(写锁) | 共享锁(读锁) |
|---|---|---|
| 并发性 | 只允许一个线程持有 | 可允许多个线程同时读取 |
| 是否阻塞 | 阻塞其他读写线程 | 仅阻塞写线程,不阻塞其他读线程 |
| 性能表现 | 并发度低 | 并发度高,适合读多写少场景 |
| Java 实现 | synchronized、ReentrantLock |
ReentrantReadWriteLock.readLock() |
| 应用场景 | 秒杀扣库存、数据修改 | 配置读取、缓存访问 |
补充:
ReentrantReadWriteLock内部使用 AQS(AbstractQueuedSynchronizer) 来实现不同模式的同步控制。- 在实际开发中,读多写少的业务逻辑(如缓存读取、系统配置查询)使用共享锁可以显著提升系统吞吐量。
- 而在写操作频繁或强一致性要求高的场景(如订单状态更新),则更适合使用排他锁。
精简版口述答案:排他锁(写锁)是独占的,一个线程获取后其他线程不能再访问,典型实现是 synchronized 和 ReentrantLock;
共享锁(读锁)允许多个线程同时读取,但写线程必须等待所有读线程释放锁才能写,ReentrantReadWriteLock 就是典型实现。
简单来说,写锁防冲突,读锁提性能,读多写少时用读写锁效果最佳。
4.可重入锁

可重入锁表示同一线程是否可以重复持有自己的锁,这个非常重要因为java中天天用的这个Reentrant它的意思就是可重入的意思,啥叫可重入呢?就是你拿你的钥匙打开了大门进屋了,然后发现你的卧室门也需要同一把钥匙打开,这时候你不需要去找其他钥匙,直接用手里的钥匙去开卧室门。一个线程拿到锁以后,就可以反复进入这个锁保护的代码块而不会被自己锁住,synchronized、ReentrantLock都是可重入的,它们内部有个计数器,你每进去一层就+1出去一层就-1,直到计数器变成0,才算真正把锁还回去。
要是锁是不可重入的,那你进入大门想再开卧室门的时候,发现钥匙已经被大门锁占住了,结果自己把自己锁死在了客厅

口述版本:
可重入锁:
可重入锁允许一个线程多次获取自己已经持有的锁,而不会导致死锁。它通过内部计数器来实现,每进入一次锁,计数器加 1,退出一次减 1,直到计数器为 0 锁才被释放。
举个例子:你有一把钥匙(锁),先打开大门(获取锁),再用同一把钥匙打开卧室门(再次进入锁保护的区域),不需要重新找钥匙。
Java 实现:synchronized 和 ReentrantLock 都是可重入锁,ReentrantLock 通过计数器管理锁的重入。
5.synchronized的智能升级

核心维度:根据“竞争激烈程度”自动优化锁状态,实现了强大的自动升级功能

我们以一个小故事来讲清楚偏向锁->轻量级锁->重量级锁的演化过程
你们公司有一台共享打印机(表示临界资源),程序员小王和其他同事每天都要用它打印资料。为了不冲突,公司安排了三种打印控制机制来「优化效率」,正好对应 JVM 中的三种锁状态。
锁优化三种形态对比类比
| JVM 锁类型 | 类比打印控制机制 | 特点 |
|---|---|---|
| 偏向锁(Biased) | 打印机记住上一个用户是谁 如果是同一个人再次使用,不做任何检查,直接打印 | 无竞争时性能最好(零开销) |
| 轻量级锁(Lightweight) | 多人要打印,先排队轮询查看是否空闲,只要打印机还没结束上一个人的工作,就自旋等待几次 | 低冲突下仍能保持高性能(无阻塞) |
| 重量级锁(Heavyweight) | 冲突太多了,打印任务就被挂起 / 唤醒,由操作系统调度谁先打印 | 冲突激烈时保障正确性,但性能最差 |
场景详解
偏向锁(Biased Lock)
- 小王是第一个用打印机的人
- 打印机会记住小王的身份
- 后面只要小王再次来打印,打印机就直接开工,不检查排队系统,因为它 “偏向” 小王
就像对象 MarkWord 中记录了某个线程的 ID,只有当其他线程来竞争才撤销偏向锁
轻量级锁(Lightweight Lock)
- 小王打印时,小李也来了
- 打印机会说:“先别挂起,等等看小王是不是很快结束”
- 小李就在打印机旁边 “自旋等待”
- 如果小王很快结束了,小李就立即上,没有阻塞、也没有上下文切换
自旋锁本质:用 CPU 忙等来换取线程不挂起
重量级锁(Heavyweight Lock)
- 现在来了十几个人都要打印,等太久了
- 打印机会说:“别等了,我排个队号,通知你们一个个来”
- 系统就开始用 阻塞 → 唤醒 → 再阻塞的方式调度线程
操作系统介入调度,线程切换代价高,效率最低
最后总结:
| 情况 | JVM 锁 | 打印类比 | 特点 |
|---|---|---|---|
| 单线程使用资源 | 偏向锁 | 打印机只认上一次的使用者 | 零开销,最快 |
| 少量线程争用 | 轻量级锁 | 排队观察是否释放 | 快速尝试获取锁 |
| 多线程激烈争用 | 重量级锁 | 线程挂起等待系统调度 | 安全但慢 |
补充:Java为了提升synchronized的性能,其实自己也在不断优化,JDK17取消了偏向锁,锁升级变得更加的顺滑

口述版本:
synchronized的智能升级:
synchronized 锁会根据线程竞争的情况自动升级,从而优化性能:
- 偏向锁(Biased Lock):
当没有线程竞争时,偏向锁会让同一线程重复获取锁时几乎没有开销。就像打印机记住了上一个用户,下一次他直接使用,不用排队。 - 轻量级锁(Lightweight Lock):
当有少量线程竞争时,轻量级锁通过自旋等待的方式减少阻塞,性能较高。类似于多个用户排队等待打印机,只有前一个操作完成才能继续。 - 重量级锁(Heavyweight Lock):
当线程竞争激烈时,使用重量级锁,操作系统介入调度线程,代价较高。就像多人争抢打印机时,需要挂起线程排队等候。
总结:
- 偏向锁:适用于没有竞争的场景,零开销。
- 轻量级锁:适用于竞争较少的场景,高效但不阻塞。
- 重量级锁:适用于激烈竞争的场景,但性能最差。
JDK17去掉了偏向锁,使锁的升级过程更加顺畅。
总结
