线程们,请排队!Java 锁的那些事儿
🔒 Java 锁全景分类与演进逻辑
一、为什么需要锁?
在多线程的世界里,如果没有规则,事情很容易“乱套”。想象一下:几个人同时往同一个储蓄罐里存钱和取钱,如果没人管理,很可能出现“多拿少放”的状况。Java 程序中也是类似:多个线程同时访问共享资源,如果不加以控制,就会出现各种怪异问题。

1.1 数据竞争(Race Condition)
当多个线程同时读写同一个变量时,最终结果可能完全取决于谁先执行。
例子:
int count = 0;
Thread t1 = new Thread(() -> count++);
Thread t2 = new Thread(() -> count++);
t1.start();
t2.start();
理论上 count 应该变成 2,但实际上可能只加到 1,因为 count++ 并不是原子操作,它包含 读取、加 1、写回 三步。如果两个线程交替执行,就可能“覆盖”彼此的修改。
想象两个厨师同时往锅里加盐,一个把 1 克加进去,另一个也加 1 克,但锅里最后只加了 1 克…这就是数据竞争。
1.2 内存可见性问题
即使变量被修改了,其他线程也不一定立即看到最新值。
-
Java 内存模型(JMM)允许线程把变量缓存在寄存器或 CPU 缓存中
-
如果没有同步措施,线程可能看到的是“老数据”,就像有人更新了共享的 Excel 表格,你却还在看前一天的版本。
1.3 操作不可分割性问题
很多操作看起来像一步,但实际上是由多步组成:
-
例子:
i++→ 读取变量i→ 加 1 → 写回内存 -
如果没有同步,多线程同时执行时,这三步可能被“打断”,最终结果出乎意料。
就像你在排队买票,同时两个售票员都给你出票,结果可能出票两次或者漏掉一张。
1.4 线程安全(Thread Safety)
线程安全的核心目标就是:
不管多少线程同时操作,程序的行为都像只有一个线程在运行一样一致。
- 换句话说,线程安全就像是在一个拥挤的自助餐厅里,大家都排队取餐,互不干扰。
🔒 解决方案:锁机制
Java 提供了锁机制来保证线程安全:
-
控制访问顺序
- 确保某一时刻,只有允许的线程访问共享资源
-
保证可见性
- 修改立即对其他线程可见
-
保证原子性
- 防止操作被中断或交叉执行
在 Java 中,锁机制经历了一条“从小白到高手”的演化之路:
| 时代 | 锁类型 | 特点 |
|---|---|---|
| 早期 | synchronized 内置锁 |
简单、安全,但性能一般 |
| 现代 | Lock 系列显式锁 |
灵活、可中断、可公平、条件等待 |
| 高级 | 读写锁 / StampedLock / CAS | 提升读多写少场景吞吐量 |
简单总结:Java 的锁就像排队神器,它既能让大家有序取餐,又能兼顾速度,避免有人插队或吃到两份。
二、 最早期:内置锁(synchronized)
2.1 背景
synchronized 是 Java 最早提供的锁机制,就像 Java 世界里的守门员:
-
每个对象都有一个门锁(对象监视器
Monitor) -
当线程想进入同步代码块时,必须先“拿到钥匙”
-
拿不到钥匙的线程只能排队等候
这意味着:同一时刻,只有一个线程可以访问被保护的共享资源。
2.2 使用方式
synchronized 有两种常用形式:
- 方法锁(锁住整个方法)
public synchronized void increment() {
count++;
}
好比把整个餐厅锁起来,顾客只能一个一个进去吃饭。
- 代码块锁(锁住部分代码)
public void increment() {
synchronized(this) {
count++;
}
}
就像锁住餐厅的一角,只允许一个人使用那部分自助餐台,其余区域不受影响,提高效率。
2.3 特性
| 特性 | 描述 | 比喻 |
|---|---|---|
| 独占锁 | 同一时刻只有一个线程能持有锁 | 守门员只允许一个人通过门口 |
| 可重入 | 同一线程可多次获取同一把锁 | 自己已经进门,可以自由出入,不会被阻止 |
| 非公平 | 线程获取顺序无法保证 | 排队时有人可能插队 |
| 不可中断 | 获取锁过程中无法打断 | 人正在门口排队,不能被请出去 |
2.4 优缺点
优点:
-
简单易用,语法直接支持
-
JVM 内部优化较多(偏向锁、轻量级锁)
-
可保证线程安全,防止数据竞争
缺点:
-
粗粒度,竞争激烈时线程阻塞/唤醒开销大
-
不支持尝试获取锁(tryLock)或中断等待
-
编程灵活性有限,复杂场景难以控制
小结:synchronized 就像是“最早的排队守门员”,简单可靠,但面对高峰期(高并发),效率就不够理想了。
2.5 内部原理(Monitor + 对象头)
-
对象头(Mark Word)
- 每个对象都带一个对象头,用来存储锁状态、线程 ID、偏向标志等信息
-
Monitor
- JVM 为对象分配一个 Monitor,用来管理锁的状态和等待队列
-
获取锁流程
-
偏向锁:如果线程是第一次获得锁,偏向该线程,后续直接获取,无需 CAS
-
轻量级锁:多线程尝试 CAS 更新对象头,成功则获得锁,否则进入自旋等待
-
重量级锁:自旋失败后,线程挂起,等待唤醒
-
可以把对象头想象成钥匙包,Monitor 是门口的守门员。不同的锁优化策略就是守门员的不同工作模式:偏向锁偷懒、轻量级锁谨慎、重量级锁严格。
2.6 适用场景
-
小临界区,低并发访问
-
线程简单,递归调用较多
-
不需要公平性和中断控制
典型例子:累加器、对象状态更新、简单缓存等场景。
2.7 小结
synchronized 是 Java 锁的起点:
-
解决了线程安全问题:保证同一时间只有一个线程访问共享资源
-
可重入:避免递归调用死锁
-
JVM 优化:偏向锁、轻量级锁、重量级锁、自适应自旋
-
限制:编程灵活性低,高并发性能不足
可以理解为 Java 锁的“入门款守门员”,功能基础、可靠,但面对高并发,需要升级版(显式锁)来提升效率和灵活性。
三、JVM 内置锁升级机制(锁演进之道)
synchronized 乍一看很简单,但它背后隐藏了一套智能优化机制。JDK1.6 以后,JVM 对内置锁进行了多级升级,以提高性能。
理解这些升级机制,有助于我们写出高效的多线程代码。
3.1 偏向锁(Biased Locking)
概念:
-
偏向锁就是 JVM 给“唯一竞争线程”的一种特殊待遇:默认认为该锁大部分时间只有一个线程会访问。
-
拿锁时不用 CAS 或阻塞,直接偏向第一个获取锁的线程。
原理:
-
对象头的 Mark Word 存储锁标志和线程 ID
-
第一个获取锁的线程“拿钥匙”,锁偏向该线程
-
后续同一线程再次访问,直接进入临界区,不做 CAS
-
如果其他线程也来竞争,偏向锁升级为轻量级锁
可以理解为:就像一家咖啡店的常客卡,门口守门员知道你是老顾客,不用排队,直接放行。
如果突然来了新顾客,守门员就得认真判断谁先到,这时偏向锁就“升级”。
优点:
-
大幅减少 CAS 操作,提高单线程访问性能
-
低竞争场景下几乎零开销
缺点:
- 多线程竞争时需要撤销偏向锁,成本略高
适用场景:
-
对象主要被单线程访问
-
临界区较小,频繁获取锁
3.2 轻量级锁(Lightweight Lock)
概念:
-
当偏向锁失效(出现竞争)时,JVM 升级为轻量级锁
-
使用 CAS 自旋而非挂起线程,尝试在短时间内获取锁
原理:
-
线程尝试通过 CAS 将对象头 Mark Word 的锁状态标记为自己
-
如果成功 → 获取锁
-
如果失败 → 自旋等待一段时间
-
自旋仍失败 → 升级为重量级锁,线程挂起
就像门口守门员允许短时间内几个人“抢先一步”进去试试看,如果大家同时冲门,守门员就会叫大家排队。
优点:
-
避免线程阻塞/唤醒开销
-
适合短时间锁竞争
缺点:
-
自旋消耗 CPU
-
锁持有时间长时效果不好
适用场景:
-
临界区很短,锁竞争不激烈
-
高并发但线程执行时间短
3.3 重量级锁(Heavyweight Lock)
概念:
-
当轻量级锁竞争激烈、自旋失败时,JVM 升级为重量级锁
-
线程获取不到锁就挂起,进入操作系统队列等待唤醒
原理:
-
CAS 自旋失败后,锁升级
-
阻塞等待,直到锁释放
-
锁释放时通知队列中的线程
就像守门员严格控制门口,大家排好队,有序入场,不能插队。
如果门口排队太多人,大家只能耐心等候。
优点:
-
保证安全,避免 CPU 空转
-
可应对高并发锁竞争
缺点:
-
挂起/唤醒开销大
-
性能不如轻量级锁
适用场景:
-
高竞争,临界区较长
-
多线程同时访问共享资源
3.4 自适应自旋(Adaptive Spinning)
概念:
-
JVM 根据历史锁的获取情况,动态决定线程是否自旋
-
优化轻量级锁和重量级锁之间的平衡
原理:
-
线程获取锁时,JVM 检查最近锁持有时间
-
如果锁释放快 → 线程选择自旋
-
如果锁释放慢 → 线程挂起,减少 CPU 消耗
就像守门员有“经验”:如果上次有人排队很快就轮到自己,他就让你先试一试;如果上次排队太久,他就叫你乖乖排队。
优点:
-
提高 CPU 利用率
-
动态适应不同并发场景
缺点:
- 自旋仍然有耗费,长锁依然不适合
3.5 锁升级流程总结
-
偏向锁:单线程访问时零开销
-
轻量级锁:短时间竞争自旋,减少挂起
-
重量级锁:长时间竞争,保证安全
-
自适应自旋:智能选择自旋或阻塞,平衡性能和安全
偏向锁(Biased Locking)
↓ 竞争出现
轻量级锁(Lightweight Lock / 自旋)
↓ 自旋失败
重量级锁(Heavyweight Lock / 阻塞)
JVM 的锁升级机制是一套“智慧守门员策略”,自动在 低竞争、高性能 vs 高竞争、安全性之间切换,让我们用最少的成本保证线程安全。
四、JUC 显式锁体系:从黑盒到定制化
JDK1.5 引入了 java.util.concurrent.locks 包(简称 JUC),给了开发者一套比 synchronized 更灵活的“锁工具箱”。
如果说 synchronized 是“一键安全”的自动挡车,那么 Lock 接口体系 就是“手动挡 + 多种驾驶模式”的专业赛车,让你自己决定怎么开。
4.1 ReentrantLock(可重入独占锁)
4.1.1 为什么会出现它?
大家都知道 Java 有个“原生锁”——synchronized,好用是好用,但有几个“硬伤”:
-
没啥个性:只能“要么锁定,要么等”,不能插队,不能超时,不能中断。
-
不能排队叫号:它不关心你是不是第一个来的,后到的线程有时能“插队”成功。
-
一条“等待通道”:所有线程只能在一条“等待区”干等,没办法分组等待。
于是,JUC(java.util.concurrent)里的大佬们造了个更灵活的家伙 —— ReentrantLock,它就像升级版的“锁王”,给了你更多玩法。
📌 典型应用场景:
-
需要 公平锁(排队叫号,先来先服务)
-
需要 响应中断 或者 设置超时时间
-
一个锁里要有 多个等待区(Condition)
4.1.2 内部原理(锁是怎么玩的?)
ReentrantLock 的底层靠 AQS(AbstractQueuedSynchronizer) 打工。
理解它,就像看一把“可重入的锁”的生命周期:
-
尝试抢锁
CAS把state从 0 改成 1 → 成功 = 我占到锁了!- 失败 → 乖乖去 等待队列(FIFO 队列,按顺序排队)。
-
排队等号
- 队列里的线程一个个排好队,等前面释放锁再尝试获取。
-
可重入
-
如果当前线程已经拿过锁,再次请求 →
state++(计数器+1)。 -
释放时 →
state--,直到归零才真正释放。
-
这就像进电梯。一个人(线程)进去把门关上(
lock),如果还是他自己要进(重入),电梯不会拒绝,但“电梯占用次数”要
+1。只有等他把所有“占用次数”清零(unlock),电梯才算真正空出来。
AQS(抽象队列同步器)是什么
- 一句话概括
AQS 是 Java 并发包中实现锁和同步器的基础框架,通过一个状态值表示资源情况,并用 FIFO
队列管理获取失败的线程,实现高效、可控的线程阻塞与唤醒。像 ReentrantLock、Semaphore 等都是基于 AQS 构建的。
- AQS 在底层干了什么
1️⃣ 用
state表示资源状态
- 资源有没有被占、还能用几个
- 具体含义由实现类决定(锁、信号量、倒计时)
2️⃣ 获取失败的线程进入 FIFO 队列
- 不自旋、不忙等
- 线程被安全地挂起等待
3️⃣ 释放资源时精准唤醒下一个线程
- 只唤醒队首
- 避免无效竞争和惊群
- AQS 的特点
通用性强:一套机制支持多种同步工具
性能友好:CAS + 阻塞,不浪费 CPU
扩展性好:只需定义“什么时候算获取成功”
语义可定制:独占 / 共享 都能支持
- AQS 用在了哪些地方
ReentrantLock、Semaphore、CountDownLatch、ReadWriteLock 等, 本质都是在用 AQS
这套排队和状态控制机制。
- 和 synchronized 的一句话对比
synchronized 是 JVM 内置的固定锁,
AQS 是 Java 层可扩展的同步框架,灵活性和可控性更强。
4.1.3 公平 vs 非公平
ReentrantLock 提供了两种“排队模式”:
-
非公平锁(默认)
-
线程来了先“插个队”试试,有空就直接上。
-
优点:效率高,吞吐量大。
-
缺点:有些老实人线程可能永远排不到(饥饿)。
-
-
公平锁
-
严格按队列顺序,先来先服务。
-
优点:避免饥饿,保证公平。
-
缺点:效率低点,因为要排队。
-
👉 就像买奶茶:
非公平模式:谁敢冲到柜台前谁先买。
公平模式:必须老老实实站队,先来后到。
4.1.4 常用方法示例
ReentrantLock lock = new ReentrantLock(true); // true = 公平锁
try {
lock.lockInterruptibly(); // 获取锁,但能被中断
// 临界区逻辑:大家不能同时改的共享资源
} catch (InterruptedException e) {
System.out.println("线程被中断,放弃排队!");
} finally {
lock.unlock(); // 千万别忘了释放锁
}
常见方法小抄:
-
lock():普通获取锁(不可中断)。 -
lockInterruptibly():获取锁时可以被中断。 -
tryLock():尝试获取一次,失败就拉倒。 -
tryLock(time, unit):尝试获取锁,超过时间自动放弃。 -
unlock():释放锁。 -
newCondition():创建一个条件对象,用来实现“多个等待区”。
4.1.5 优缺点一览
| ✅ 优点 | ❌ 缺点 |
|---|---|
| 灵活:能超时、能中断、能公平 | 代码比 synchronized 麻烦 |
| 支持多条件等待(Condition) | 手动加解锁,容易忘记 unlock |
| 可重入,控制粒度更细 | 低竞争时性能略低于 synchronized |
👉 总结:
ReentrantLock就像一辆配置高的车:功能多、能玩花活,但得熟悉操作,否则容易“忘打方向盘”。
4.1.6 适用场景
-
高并发,必须严格控制 锁的顺序
-
线程可能需要 超时放弃 或 中途撤退
-
一个锁需要 多个等待区(Condition),比如:
- “厨师等菜切好”
- “服务员等菜出锅”
- “顾客等菜上桌”
这在 synchronized 里只能硬塞一个“等待区”,而 ReentrantLock 可以优雅拆分。
ReentrantLock = 可插队、可排队、可撤退、可多候场的“高级版 synchronized”。
4.2 ReentrantReadWriteLock(读写锁)
4.2.1 背景(为什么要用它?)
在很多系统里,读操作 >>> 写操作。
比如:
-
配置中心:大家都在查配置,偶尔才有更新
-
商品详情页:成千上万次查询,偶尔改一次价格
如果用 独占锁(ReentrantLock / synchronized):
👉 写的时候确实安全,但 读也被串行化了,大大浪费性能。
所以就需要 读写锁:
-
读共享:多个读线程可以并发执行,互不干扰
-
写独占:写线程来了,必须清场,避免读写冲突
这就像一个 图书馆:
- 多个读者(读线程)可以同时看书
- 但管理员(写线程)要整理书架时,必须赶走所有读者
4.2.2 内部原理(它怎么玩?)
ReentrantReadWriteLock 还是靠 AQS(AbstractQueuedSynchronizer) 打工,不过 state 被“一分为二”:
-
高 16 位:读锁计数(支持多个线程同时读,存储读锁的持有次数)(理论最大值 = 2¹⁶ = 65536)
-
低 16 位:写锁计数(只能 0 或 1,一个线程独占)
规则:
-
读锁(共享锁)
-
多个线程可以同时持有
-
只要没有写线程在操作,就能直接读
-
-
写锁(独占锁)
-
同一时刻只能有一个线程持有
-
写线程存在时,读线程也要阻塞
-
-
锁降级(支持)
-
写锁 → 读锁 可以无缝降级
-
但 读锁 → 写锁 不允许(避免死锁)
-
👉 比喻:
- 写锁像管理员清场:必须先赶走所有人,再独自操作。
- 锁降级 = 管理员整理完后,顺手坐下读书(写→读)。
4.2.3 方法示例
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 读操作
rwLock.readLock().lock();
try {
// 多线程可并发执行
System.out.println("读取数据...");
} finally {
rwLock.readLock().unlock();
}
// 写操作
rwLock.writeLock().lock();
try {
// 独占执行,期间读写都被阻塞
System.out.println("修改数据...");
} finally {
rwLock.writeLock().unlock();
}
锁降级示例:
rwLock.writeLock().lock();
try {
// 修改数据
rwLock.readLock().lock(); // 写锁持有时再加读锁(锁降级)
try {
// 继续以读的身份持有
} finally {
rwLock.readLock().unlock();
}
} finally {
rwLock.writeLock().unlock();
}
4.2.4 优缺点(盘点一下)
| ✅ 优点 | ❌ 缺点 |
|---|---|
| 读多写少场景性能高 | 写线程可能饿死(读优先模式) |
| 支持锁降级(写→读) | 编程复杂,要小心锁的获取顺序 |
| 保证线程安全 | 读锁和写锁可重入,嵌套不当容易死锁 |
⚠️ 注意点:
写锁优先 or 读锁优先:默认是 非公平,可能导致写线程长期等不到机会
锁升级不支持:读锁 → 写锁 会死锁
组合使用要小心:读里加写、写里加读,必须搞清楚锁降级规则
4.2.5 适用场景
最典型的:读多写少 的业务逻辑。
-
缓存:读数据很频繁,偶尔才更新
-
配置中心:查询多,更新少
-
商品/用户详情页:查询远多于修改
-
本地数据结构:例如
Map或List的并发访问
ReentrantReadWriteLock = 图书馆的借阅规则:读书人可以一起看,但管理员整理时必须清场。
适合读多写少场景,能极大提升并发性能。
4.3 StampedLock(JDK8 乐观读锁)
4.3.1 背景(为啥还要再造轮子?)
虽然 ReentrantReadWriteLock 已经解决了“读多写少”的问题,
但是它依然是 悲观锁:
-
只要有写操作想进来,读操作都得停下排队。
-
在读操作 远多于写操作 的场景下,这还是有点浪费。
于是 JDK8 又整了个“狠活” ——
👉 StampedLock,引入了 乐观读,进一步榨干性能。
4.3.2 特性(它有什么不同?)
-
乐观读(Optimistic Read)
-
获取一个 stamp(戳记),相当于给数据打上版本号
-
直接无锁读取数据(不阻塞写线程)
-
读完后校验 stamp:
-
✅ stamp 没变 → 数据安全
-
❌ stamp 变了 → 有写操作 → 升级为悲观读锁
-
-
-
不可重入
-
和 ReentrantLock 不同,StampedLock 不可重入
-
必须手动管理好
stamp,否则容易死锁
-
-
写锁(独占锁)
- 和 ReentrantReadWriteLock 一样,写操作仍然独占
👉 打个比方:
悲观锁:你看书时,管理员怕有人改书,直接把书馆锁上。
乐观读:你先拿书看(不加锁),结束后再问“有人改书没?”
没人改 → 直接用 * 有人改 → 只能再借一次(升级成悲观锁)
4.3.3 方法示例(代码小抄)
StampedLock lock = new StampedLock();
// 乐观读
long stamp = lock.tryOptimisticRead();
try {
int val = sharedData; // 无锁读取
if (!lock.validate(stamp)) { // 校验 stamp
// 有写操作干扰,回退到悲观读
stamp = lock.readLock();
try {
val = sharedData;
} finally {
lock.unlockRead(stamp);
}
}
System.out.println("读取值: " + val);
} finally {
// 乐观读不需要 unlock
}
// 写锁
long wStamp = lock.writeLock();
try {
sharedData++;
System.out.println("更新数据...");
} finally {
lock.unlockWrite(wStamp);
}
4.3.4 优缺点(盘点一下)
| ✅ 优点 | ❌ 缺点 |
|---|---|
| 乐观读几乎无阻塞,吞吐量高 | 不可重入,使用复杂 |
| 写操作少时性能极佳 | 乐观读失败要回退,逻辑更啰嗦 |
| 支持写锁、悲观读 | 必须熟悉 stamp 的用法 |
⚠️ 注意:
- 不能和 ReentrantLock 一样随便重入,否则 stamp 管理混乱 → 直接翻车
- 适合 读非常多、写极少 的系统,否者回退成本高
4.3.5 适用场景
-
缓存系统(查询远多于更新)
-
统计类数据(读操作占绝大多数)
-
性能敏感,需要进一步压榨读性能的系统
总结(锁家族大对比)
| 锁类型 | 独占/共享 | 可重入 | 公平性 | 可中断 | 优点 | 缺点 | 典型场景 |
|---|---|---|---|---|---|---|---|
| ReentrantLock | 独占 | ✔ | 可选 | ✔ | 灵活、支持 Condition | 编程复杂 | 高并发互斥、可中断任务 |
| ReentrantReadWriteLock | 读共享/写独占 | ✔ | 可选 | ✔ | 读多写少性能高 | 写线程可能饿死 | 缓存、读多写少数据结构 |
| StampedLock (JDK8) | 读共享/写独占 | ✘ | ✘ | ✘ | 乐观读高性能 | 不可重入、逻辑复杂 | 高并发读、统计系统 |
🔑 总结:
ReentrantLock:灵活、可中断、可插队
ReentrantReadWriteLock:读多写少,性能提升
StampedLock:乐观读,进一步压榨性能
五、按分类视角看锁
5.1 独占锁 vs 共享锁
独占锁(Exclusive Lock)
-
定义:同一时刻只能被一个线程持有
-
代表:
synchronized、ReentrantLock、StampedLock.writeLock() -
优点:简单、安全,保证线程安全
-
缺点:并发度低,读多写少场景性能差
-
适用场景:更新共享资源、临界区操作
💡 比喻:独占锁像 电梯:一次只能一个人占用电梯,别人得排队。
共享锁(Shared Lock)
-
定义:允许多个线程同时持有
-
代表:
ReentrantReadWriteLock.readLock()、StampedLock.readLock() -
优点:读操作并发度高
-
缺点:写线程可能饿死
-
适用场景:缓存读多写少、查询频繁的数据结构
💡 比喻:共享锁像 图书馆:多个人可以同时看书,但管理员整理书架时必须清场。
5.2 公平锁 vs 非公平锁
| 特性 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取顺序 | FIFO,先到先得 | 当前线程可插队 |
| 优点 | 避免饥饿 | 吞吐量高 |
| 缺点 | 性能略低 | 可能导致线程饥饿 |
| 典型锁 | ReentrantLock(true) |
synchronized、ReentrantLock(false) |
| 适用场景 | 顺序敏感、严格控制 | 高吞吐量并发任务 |
💡 总结:公平性牺牲性能,非公平锁更常用。
5.3 可重入锁 vs 不可重入锁
-
可重入锁:同一线程可多次获取锁
-
代表:
synchronized、ReentrantLock、ReentrantReadWriteLock -
优点:递归调用/嵌套调用不会死锁
-
-
不可重入锁:每次获取必须释放
-
代表:
Semaphore(默认) -
缺点:嵌套使用要小心,否则容易死锁
-
💡 比喻:可重入锁像 钥匙圈,同一把钥匙可以多次开门;不可重入锁像 一次性门票,进一次得出门再买票。
5.4 悲观锁 vs 乐观锁
| 特性 | 悲观锁 | 乐观锁 |
|---|---|---|
| 核心假设 | 冲突概率大 | 冲突概率小 |
| 获取方式 | 先加锁 | 不加锁,提交时验证 CAS |
| 代表 | synchronized、ReentrantLock |
AtomicInteger、StampedLock.tryOptimisticRead() |
| 优点 | 安全性高 | 高吞吐量 |
| 缺点 | 阻塞、性能低 | 失败重试增加复杂性 |
| 典型场景 | 写操作多、强一致性 | 读多写少、高并发统计 |
💡 比喻:
- 悲观锁:担心别人抢你的东西,先把它锁起来
- 乐观锁:先拿着用,用完再检查有没有被修改
5.5 自旋锁 vs 阻塞锁
-
自旋锁(Spin Lock)
-
线程短时间忙等,不挂起
-
代表:JVM 轻量级锁、CAS
-
优点:避免挂起/唤醒开销
-
缺点:CPU 消耗高,长时间自旋效率低
-
-
阻塞锁(Blocking Lock)
-
获取不到锁就挂起
-
代表:重量级锁、ReentrantLock
-
优点:节省 CPU
-
缺点:挂起/唤醒开销大
-
💡 自适应自旋:JVM 会根据历史成功率决定是否自旋。
5.6 可中断锁 vs 不可中断锁
-
可中断锁
-
代表:
ReentrantLock.lockInterruptibly() -
优点:响应中断,避免死锁
-
-
不可中断锁
-
代表:
synchronized -
优点:语法简单
-
缺点:线程等待不可打断
-
💡 比喻:
- 可中断锁 = 等锁时能随时喊“我不等了!”
- 不可中断锁 = 一旦等,就得等到底
总结
-
独占/共享:控制并发度
-
公平/非公平:控制顺序
-
可重入/不可重入:控制嵌套
-
悲观/乐观:控制冲突策略
-
自旋/阻塞:控制等待方式
-
可中断/不可中断:控制响应能力
六、特殊锁机制 & 工具类
6.1 Semaphore(信号量)
-
作用:控制同时访问某资源的线程数
-
典型场景:连接池、限流
-
原理:就像 公共厕所的隔间,同时只能 N 个人上
Semaphore semaphore = new Semaphore(3); // 同时允许3个线程访问
semaphore.acquire();
try {
// 临界区
} finally {
semaphore.release();
}
6.2 CountDownLatch(倒计时锁)
-
作用:等待一组操作完成后再继续
-
场景:线程启动协调、并行任务等待
-
原理:像 倒计时炸弹,等 N 个线程完成才触发
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 执行任务
latch.countDown();
}).start();
}
latch.await(); // 等待3个线程完成
6.3 CyclicBarrier(循环屏障)
-
作用:多个线程在某一点等待,然后同时继续
-
场景:并行计算分段合并
-
原理:像 大家在起跑线上一起等待发令枪
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("所有线程到达屏障"));
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 执行任务
barrier.await();
}).start();
}
6.4 Phaser(可重用分阶段屏障)
-
特性:多阶段、动态注册、可重复使用
-
场景:复杂任务流水线
-
原理:比 CyclicBarrier 更灵活的分段任务调度器
6.5 分段锁(Segmented Lock)
-
场景:提高哈希表的并发度
-
代表:JDK7
ConcurrentHashMap -
原理:将数据拆分为多个段,每段独立加锁 → 并发访问效率提升
💡 比喻:像图书馆分区域管理,不同区域的读者互不影响
6.6 锁粗化 / 锁消除
-
锁粗化:JIT 将多次连续加锁合并 → 减少锁操作开销
-
锁消除:JIT 发现锁无竞争 → 自动去掉锁
-
目标:减少不必要的加锁/解锁,提高性能
6.7 应用场景与最佳实践
| 场景 | 推荐锁类型 | 原因 |
|---|---|---|
| 高并发读多写少 | ReentrantReadWriteLock / StampedLock | 读共享提高吞吐量 |
| 高并发独占操作 | ReentrantLock | 可中断/tryLock控制 |
| 严格顺序执行 | 公平锁 | 避免线程饥饿 |
| 资源限制 | Semaphore | 控制并发访问数量 |
| 任务等待 | CountDownLatch / CyclicBarrier / Phaser | 线程协调 |
| 小临界区 | synchronized | 简单、JVM优化性能高 |
实践建议:
- 锁粒度尽量小,避免长时间持锁
- 优先考虑乐观锁或 CAS,减少阻塞
- 读多写少场景用读写锁,提升吞吐量
- 复杂场景使用显式锁,搭配 Condition 控制多条件等待
锁类型对比总表
| 锁类型 | 独占/共享 | 可重入 | 公平性 | 可中断 | 优点 | 缺点 | 典型场景 |
|---|---|---|---|---|---|---|---|
| synchronized | 独占 | ✔ | ✘ | ✘ | 简单、JVM优化 | 低并发性能差 | 小临界区、低竞争 |
| ReentrantLock | 独占 | ✔ | 可选 | ✔ | 灵活、Condition | 编程复杂 | 高并发互斥、可中断 |
| ReentrantReadWriteLock | 共享/独占 | ✔ | 可选 | ✔ | 读多写少性能高 | 写饥饿、复杂 | 缓存、读多写少数据 |
| StampedLock | 共享/独占 | ✘ | ✘ | ✘ | 乐观读高性能 | 不可重入、逻辑复杂 | 高并发读、统计数据 |
| Semaphore | 独占 | ✘ | ✘ | ✔ | 控制并发数 | 无可重入 | 资源限流、连接池 |
| CountDownLatch | - | ✘ | ✘ | ✔ | 线程等待 | 只能用一次 | 任务协调 |
| CyclicBarrier | - | ✘ | ✘ | ✔ | 循环等待 | 注册线程固定 | 分段任务协调 |
| Phaser | - | ✘ | ✘ | ✔ | 多阶段灵活 | 编程复杂 | 多阶段任务流水线 |
工具类锁 = 锁的“百变工具箱”:
小资源用synchronized、高并发互斥用ReentrantLock、读多写少用读写锁、超高并发读用StampedLock、线程协调/资源限制用 Semaphore/Barrier/Phaser。
🔄 Java 锁家族总结
-
synchronized
-
安全、简单、JVM 内置
-
缺点:性能有限,读多写少容易浪费
-
-
显式锁(ReentrantLock)
-
功能升级:可中断、可公平、支持 Condition
-
适合高并发互斥、需要灵活控制的场景
-
-
读写锁(ReentrantReadWriteLock / StampedLock)
-
读多写少场景神器
-
StampedLock 引入 乐观读,吞吐量进一步提升
-
-
JVM 锁优化
-
偏向锁、轻量级锁、自适应自旋
-
在低竞争场景下自动提高性能
-
-
工具类锁
-
Semaphore:控制同时访问的线程数
-
CountDownLatch:等待一组任务完成
-
CyclicBarrier:线程同步到某个点
-
Phaser:多阶段、可重复使用的任务屏障
-
-
实践原则
-
锁粒度尽量小 → 避免长时间持锁
-
优先使用 乐观锁 / CAS → 减少阻塞
-
读多写少 → 用 读写锁 提升吞吐
-
显式锁适合复杂场景,搭配 Condition 多条件等待
-
💡 一句话记忆:
“先内置锁,再显式锁,再分读写,再靠 JVM 优化,再用工具锁,锁要小、读优先、复杂场景显式管理。”
更多推荐

所有评论(0)