在并发编程中,"锁"几乎是绕不开的话题。Java 之所以能在多线程环境下保持数据一致性,很大程度上依赖了其丰富而高效的锁机制。
一、 为什么我们需要"锁升级"?(Synchronized 的进化)
1. Java 对象头(Mark Word)与锁状态
Java 锁的状态其实是记录在对象头中的。这是一个动态变化的 64 位数据结构。

2. 锁升级状态流转
在 JDK 1.5 之前,synchronized 是一个重量级锁。无论竞争是否激烈,只要线程进入同步块,JVM 就会向操作系统申请互斥量(Mutex)。
为什么这很慢?
因为这涉及**用户态(User Mode)到内核态(Kernel Mode)**的切换。这种切换需要保存线程上下文、更新寄存器等,开销极大。有时候,线程执行代码的时间只有几纳秒,但挂起和唤醒线程却要几毫秒,"排队的时间比吃饭的时间还长"。
为了解决这个问题,JDK 1.6 引入了锁升级机制:JVM 不再一上来就动用操作系统的重武器,而是先尝试用"轻武器"解决。
1. 锁的四种状态详解
-
无锁(No Lock)
-
描述:没有任何线程持有对象锁。
-
场景:刚创建的对象。
-
-
偏向锁(Biased Lock)------"熟人免检"
-
原理 :JVM 认为,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。
-
机制 :当线程 A 第一次获取锁时,JVM 把线程 A 的 ID 写到对象头的 Mark Word 中。以后线程 A 再来,只要检查 Mark Word 里是不是自己的 ID,如果是,直接通行,连 CAS 操作都不用做。
-
注:JDK 15+ 默认废弃了偏向锁,因为现代应用线程竞争普遍较多,撤销偏向锁反而消耗性能。
-
-
轻量级锁(Lightweight Lock)------"占座贴条"
-
触发:当有第二个线程 B 尝试获取锁时,偏向锁失效,升级为轻量级锁。
-
原理 :线程 B 不会立即阻塞。它会在自己的栈帧中创建一个叫 Lock Record 的空间,然后尝试用 CAS (Compare And Swap) 指令,把对象头的 Mark Word 替换为指向自己 Lock Record 的指针。
-
比喻:就像去图书馆,虽然座位被占了,但我贴张条子说"我在排队",然后我不走,在旁边等着(自旋)。
-
-
重量级锁(Heavyweight Lock)------"找管理员"
-
触发:如果线程 B 自旋了很多次(JDK 自适应自旋)还没拿到锁,或者竞争线程过多。
-
原理 :CAS 失败,锁膨胀。对象头指向操作系统底层的 Monitor(监视器) 对象。
-
后果:未抢到锁的线程被挂起,进入阻塞状态,等待操作系统唤醒。
-

3. AQS (AbstractQueuedSynchronizer) 等待队列
ReentrantLock 的核心。
synchronized 是 JVM 也就是 C++ 层面实现的,而 java.util.concurrent 包下的锁是 JDK 层面用 Java 代码实现的。它们的基石是 AQS (AbstractQueuedSynchronizer)。
AQS 到底是什么?
你可以把 AQS 想象成一个银行的叫号大厅。
-
State(资源状态):
-
大厅里的柜台窗口。state = 0 表示窗口空闲;state = 1 表示有人在办理;state > 1 表示同一个人在办多项业务(可重入)。
-
所有线程都通过 CAS 操作去争抢修改这个 state。
-
-
CLH 队列(等待区):
-
抢不到窗口的人,不能挤在窗口前(浪费 CPU 自旋),必须去休息区排队。
-
这是一个双向链表。新来的线程被包装成 Node 节点,挂到队尾。
-
前一个节点释放锁后,会唤醒它的后继节点:"哥们,到你了"。
-

二、 各种锁的场景分析
1. Synchronized
-
特点:语法最简单,隐式加锁释放,异常时自动释放,不易死锁。
-
适用场景:
-
代码块短小:业务逻辑很简单,比如只是对一个 count++ 操作或者简单的赋值。
-
绝大多数常规业务:在没有极致性能要求,且不存在复杂的交互(如尝试拿锁、超时打断)时,它是首选。
-
单例模式:DCL(双重检查锁)单例。
-
2. ReentrantLock (可重入锁)
-
特点:功能丰富。支持 tryLock()(尝试获取)、lockInterruptibly()(可被中断)、new ReentrantLock(true)(公平锁)。
-
适用场景:
-
场景一:防止死锁(使用 tryLock)
- 描述:比如转账业务,A 转给 B,B 转给 A,如果不小心可能死锁。使用 tryLock(),如果拿不到锁,等待 2 秒就放弃并回滚,而不是一直死等。
-
场景二:需要中断等待
- 描述:用户取消了操作,后台线程正在等待锁,使用 lockInterruptibly() 可以让线程响应中断信号立刻停止,而不是傻等。
-
场景三:严格排队(公平锁)
- 描述:比如抢票系统或打印队列,必须先来后到,不能让新来的插队,此时开启公平锁模式(虽然性能会下降)。
-
3. ReentrantReadWriteLock (读写锁)
-
特点:一把锁分为"读锁"和"写锁"。读读不互斥,读写互斥,写写互斥。
-
适用场景:
-
高频读取、低频修改的数据。
-
案例:本地缓存系统
-
假设你维护一个庞大的配置中心缓存。99% 的时间是业务线程在读取配置,只有 1% 的时间是管理员修改配置。
-
如果用 synchronized,所有读取线程都要排队,并发度极低。
-
使用读写锁,1000 个线程可以同时持有读锁读取数据,只有当写线程来临时,才阻塞读线程。
-
-
4. StampedLock (印章锁 - JDK 8)
-
特点 :读写锁的性能加强版。引入了乐观读。
-
注意 :不可重入,使用不当容易死锁;编码复杂度高。
-
适用场景:
-
极致性能要求的读多写少场景。
-
案例:金融系统的实时行情处理
-
行情数据变化很快,读取请求量巨大。
-
StampedLock 的乐观读允许线程在不加任何物理锁的情况下读取数据,最后校验一下"版本号"有没有变。如果没变,这次读取就相当于无锁,性能极高。
-
-

三:JDK 层面的锁(JUC)
当需要更高级的功能(如超时、公平性、读写分离)时,我们需要 java.util.concurrent.locks。
1. ReentrantLock 与 AQS
ReentrantLock 基于 AQS 实现。
-
State 变量:volatile int state。0 表示无锁,1 表示有锁,>1 表示重入次数。
-
CLH 队列:如上图所示,抢不到锁的线程会被封装成 Node 节点,加入双向链表排队。
java
import java.util.concurrent.locks.ReentrantLock;
public class FairLockDemo {
// 参数 true 表示公平锁:严格按照排队顺序获取锁
private final ReentrantLock lock = new ReentrantLock(true);
public void accessResource() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在运行");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 极其重要:务必在 finally 释放
}
}
}
2. 读写锁 ReentrantReadWriteLock
适用场景:读多写少(如缓存)。
-
读锁:共享锁,大家都能读。
-
写锁:独占锁,我写的时候,你们不能读也不能写。
-
缺陷:如果一直在读,写线程可能"饿死"(一直拿不到锁)。
3. 性能怪兽:StampedLock (JDK 8+)
这是 ReadWriteLock 的升级版,它引入了乐观读策略,性能极高。
- 乐观读:我不加锁,我先读,读完看看版本号(Stamp)有没有变。如果变了(说明中间有人写过),我再升级成悲观读锁重读一遍。
代码:StampedLock 乐观读
java
import java.util.concurrent.locks.StampedLock;
public class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
// 写方法:独占
public void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
// 读方法:乐观读(高性能关键)
public double distanceFromOrigin() {
// 1. 尝试乐观读,返回一个版本号戳
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
// 2. 检查在读的过程中,有没有被写过
if (!sl.validate(stamp)) {
// 3. 如果验证失败(说明数据不一致),升级为悲观读锁
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
场景:高性能阻塞队列(生产者-消费者)
我们需要两个等待队列:一个叫"不再满"(notFull),一个叫"不再空"(notEmpty)。
java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedQueue<T> {
private Object[] items;
private int count, putIndex, takeIndex;
private final ReentrantLock lock = new ReentrantLock();
// 两个条件队列:精准控制
private final Condition notFull = lock.newCondition(); // 队列不满,才能写
private final Condition notEmpty = lock.newCondition(); // 队列不空,才能读
public BoundedQueue(int size) {
items = new Object[size];
}
// 生产者方法
public void put(T t) throws InterruptedException {
lock.lock();
try {
// 如果满了,就在 notFull 这个房间里等
while (count == items.length)
notFull.await();
items[putIndex] = t;
if (++putIndex == items.length) putIndex = 0;
count++;
// 生产完了,去敲 notEmpty 房间的门,唤醒消费者
notEmpty.signal();
} finally {
lock.unlock();
}
}
// 消费者方法
public T take() throws InterruptedException {
lock.lock();
try {
// 如果空了,就在 notEmpty 这个房间里等
while (count == 0)
notEmpty.await();
T x = (T) items[takeIndex];
if (++takeIndex == items.length) takeIndex = 0;
count--;
// 消费完了,去敲 notFull 房间的门,唤醒生产者
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}