锁的本质
锁(Lock)是保证原子性和可见性最直接的手段。其核心语义是:同一时刻,只允许一个线程执行被保护的代码段。 Java 提供了两类锁:
synchronized 内置锁
三种使用形式
public class SyncDemo {
private int count = 0;
private static int staticCount = 0;
// 形式1:同步实例方法 —— 锁的是 this 对象
public synchronized void increment() {
count++;
}
// 形式2:同步静态方法 —— 锁的是 SyncDemo.class 对象
public static synchronized void incrementStatic() {
staticCount++;
}
// 形式3:同步代码块 —— 锁的是指定对象,粒度更细
public void doWork() {
// 非同步代码(并发执行)
int localResult = expensiveCompute();
synchronized (this) { // 只同步必要的部分
count += localResult;
}
}
private int expensiveCompute() { return 1; }
}
对象头与 Mark Word
JVM 中每个对象都有一个对象头(Object Header),包含两个机器字:
锁升级:偏向锁 → 轻量级锁 → 重量级锁
Java 6 引入了锁升级机制(自适应锁),根据竞争激烈程度逐步升级,避免在低竞争场景下进入 开销大的重量级锁:
Java 15(JEP 374)默认禁用偏向锁,Java 21 正式废弃。原因是现代应用的锁竞争模式与偏向锁的设计假设不符, 维护偏向锁撤销逻辑反而增加了 JVM 复杂度。高度竞争的现代应用直接用轻量级锁+重量级锁即可。
wait() / notify() / notifyAll()
这三个方法用于线程间的协调,必须在 synchronized 块中调用(否则抛出
IllegalMonitorStateException):
// 经典生产者-消费者模式
public class BoundedBuffer<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
public BoundedBuffer(int capacity) { this.capacity = capacity; }
public synchronized void put(T item) throws InterruptedException {
while (queue.size() == capacity) { // 用 while,不用 if(防止虚假唤醒)
wait(); // 释放锁并等待,直到被 notify
}
queue.add(item);
notifyAll(); // 唤醒所有等待的消费者线程
}
public synchronized T take() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // 队列空时等待
}
T item = queue.poll();
notifyAll(); // 唤醒等待的生产者线程
return item;
}
}
等待条件判断必须用 while 循环,原因:(1) 虚假唤醒(Spurious Wakeup)——
线程可能在没有被 notify 的情况下意外醒来(OS 底层允许);
(2) 多个线程竞争时,被唤醒的线程重新获得锁后条件可能已经改变。
while 循环确保每次唤醒后都重新检查条件。
ReentrantLock
基本使用
ReentrantLock 是 synchronized 的显式替代,提供相同的互斥语义,但功能更丰富:
import java.util.concurrent.locks.*;
public class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // 加锁
try {
count++;
} finally {
lock.unlock(); // 必须在 finally 中解锁!
}
}
// 可中断加锁:等待期间可被 interrupt() 取消
public void incrementInterruptibly() throws InterruptedException {
lock.lockInterruptibly();
try {
count++;
} finally {
lock.unlock();
}
}
// 非阻塞尝试加锁:立即返回 true/false
public boolean tryIncrement() {
if (lock.tryLock()) {
try {
count++;
return true;
} finally {
lock.unlock();
}
}
return false; // 锁被占用,立即返回
}
// 超时尝试加锁
public boolean tryIncrementWithTimeout() throws InterruptedException {
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
count++;
return true;
} finally {
lock.unlock();
}
}
return false; // 100ms 内未获得锁
}
}
公平锁 vs 非公平锁
// 公平锁:按等待队列顺序分配锁,先到先得
ReentrantLock fairLock = new ReentrantLock(true);
// 非公平锁(默认):允许新到来的线程「插队」
ReentrantLock unfairLock = new ReentrantLock(false); // 或 new ReentrantLock()
Condition:比 wait/notify 更灵活的条件变量
public class BetterBoundedBuffer<T> {
private final ReentrantLock lock = new ReentrantLock();
// 两个条件变量(相当于两个等待集合),精确唤醒
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Object[] items;
private int head, tail, count;
public BetterBoundedBuffer(int capacity) { items = new Object[capacity]; }
public void put(T x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) notFull.await(); // 只有生产者等待
items[tail] = x;
if (++tail == items.length) tail = 0;
++count;
notEmpty.signal(); // 只唤醒消费者(精确),不唤醒生产者
} finally { lock.unlock(); }
}
@SuppressWarnings("unchecked")
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0) notEmpty.await(); // 只有消费者等待
T x = (T) items[head];
items[head] = null;
if (++head == items.length) head = 0;
--count;
notFull.signal(); // 只唤醒生产者(精确)
return x;
} finally { lock.unlock(); }
}
}
ReadWriteLock 读写锁
ReadWriteLock 允许多个线程同时读(读-读不互斥),但写操作必须独占(读-写、写-写互斥)。
适合「读多写少」的场景,可以显著提升并发读的吞吐量。
public class RWCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
public V get(K key) {
readLock.lock(); // 共享锁:允许多个线程同时读
try {
return cache.get(key);
} finally {
readLock.unlock();
}
}
public void put(K key, V value) {
writeLock.lock(); // 独占锁:写时排斥所有读写
try {
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
}
在读请求非常频繁时,写线程可能长时间等待(写锁饥饿)。
ReentrantReadWriteLock(true)(公平模式)可以缓解此问题,
但性能会下降。如果读写比例不够悬殊,反而不如直接用 ReentrantLock。
StampedLock(Java 8+)
StampedLock 是 Java 8 引入的更高性能的读写锁,增加了乐观读(Optimistic Read)模式,
允许读操作在不加任何锁的情况下执行,通过版本戳(stamp)检验数据是否被修改。
import java.util.concurrent.locks.*;
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() {
long stamp = sl.tryOptimisticRead(); // 获取版本戳,不加锁
double currentX = x, currentY = y; // 读取数据(可能被并发写修改)
if (!sl.validate(stamp)) { // 检查读取期间是否有写操作
// 验证失败:有写操作发生,升级为悲观读锁重新读
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
- 不可重入:同一线程不能重复获取写锁(会死锁),这与 ReentrantLock 不同。
- 不支持条件变量:不能创建 Condition。
- 不支持 isLocked() 等状态查询。
- 适合简单的读多写少场景,复杂同步逻辑仍推荐 ReentrantLock。
锁的选择指南
| 场景 | 推荐锁 | 理由 |
|---|---|---|
| 简单互斥,代码简洁优先 | synchronized |
JVM 自动释放,无法忘记 unlock |
| 需要可中断/超时/公平 | ReentrantLock |
显式锁提供更多控制选项 |
| 多条件变量(精确唤醒) | ReentrantLock + Condition |
比 wait/notify 更精确 |
| 读多写少(读:写 > 10:1) | StampedLock |
乐观读无锁,吞吐最高 |
| 读多写少(中等读写比) | ReentrantReadWriteLock |
功能完整,支持可重入 |
| 原子计数、CAS 操作 | AtomicXxx / LongAdder |
无锁算法,性能最优 |
死锁的预防
死锁(Deadlock)发生在多个线程循环等待对方持有的锁时。预防死锁的策略:
- 固定锁顺序:所有线程按相同顺序获取多把锁(最常用)。
- 使用 tryLock 超时:获取不到锁就放弃,避免无限等待。
- 减少锁的持有时间:只在必要时持锁,尽快释放。
- 使用并发工具类:优先使用 java.util.concurrent 提供的高级工具,而非手动管理多把锁。
// 死锁预防:使用 tryLock + 有序加锁
public static void transfer(Account from, Account to, double amount)
throws InterruptedException {
// 固定锁顺序:按账户 ID 排序,避免循环等待
Account first = from.getId() < to.getId() ? from : to;
Account second = from.getId() < to.getId() ? to : from;
boolean acquired = false;
while (!acquired) {
if (first.lock.tryLock(10, TimeUnit.MILLISECONDS)) {
try {
if (second.lock.tryLock(10, TimeUnit.MILLISECONDS)) {
try {
from.debit(amount);
to.credit(amount);
acquired = true;
} finally { second.lock.unlock(); }
}
} finally { first.lock.unlock(); }
}
}
}
本章小结
- synchronized 通过 JVM 监视器实现内置锁,经历偏向锁→轻量级锁→重量级锁的升级过程,JVM 自动管理加解锁。
wait()/notify()/notifyAll()配合 synchronized 实现条件等待,必须用 while 循环检查条件。- ReentrantLock 提供可中断、超时、公平性、多条件变量等高级特性,必须在 finally 中 unlock()。
- ReadWriteLock 允许读-读并发,适合读多写少场景;StampedLock 乐观读无需加锁,吞吐更高但不可重入。
- 死锁预防:固定锁顺序 + tryLock 超时 + 减少锁持有时间。