在多线程并发编程中,锁(Lock) 是保证线程安全、解决资源竞争的核心机制。Java 提供了丰富多样的锁实现,从底层的 synchronized 到 JUC 包下的 ReentrantLock、ReadWriteLock 等。
很多开发者对锁的理解停留在“加锁解锁”的层面,但实际上,理解锁的分类、特性以及底层优化机制,对于编写高性能并发代码至关重要。本文将结合原理与代码,系统梳理 Java 中的各种锁。
一、 锁的分类维度
Java 中的锁并非单一概念,而是可以从不同维度进行分类。理解这些分类有助于我们在不同场景下选择最合适的锁。
1. 乐观锁 vs 悲观锁
这是基于对并发冲突的态度进行的分类 。
- 悲观锁 (Pessimistic Lock):
- 核心思想:假设每次访问数据时都会有其他线程修改数据,因此每次操作前都先加锁。
- 典型实现:
synchronized、ReentrantLock。 - 适用场景:写多读少,竞争激烈。
- 乐观锁 (Optimistic Lock):
- 核心思想:假设数据一般不会发生冲突,只在更新时检查数据是否被修改过(通常通过 CAS 或版本号机制)。
- 典型实现:
AtomicInteger、LongAdder。 - 适用场景:读多写少,竞争较少。
2. 公平锁 vs 非公平锁
这是基于线程获取锁的顺序策略进行的分类 。
- 公平锁 (Fair Lock):
- 机制:严格按照线程申请锁的时间顺序(FIFO)来获取锁。
- 优点:所有线程都能获得锁,不会饥饿。
- 缺点:吞吐量低,因为需要维护队列和上下文切换。
- 实现:
new ReentrantLock(true)。
- 非公平锁 (Non-Fair Lock):
- 机制:新来的线程可以直接尝试获取锁(插队),如果失败再排队。
- 优点:吞吐量高,减少了线程唤醒的开销。
- 缺点:可能导致某些线程长期等待(饥饿)。
- 实现:
synchronized(仅支持非公平)、new ReentrantLock()(默认非公平)。
为什么默认是非公平锁?
因为在实际场景中,刚释放锁的线程很可能再次请求锁,或者新线程正在 CPU 运行,直接让它们获取锁可以避免昂贵的上下文切换,从而提升整体性能 。
3. 独享锁 vs 共享锁
这是基于锁能否被多个线程持有进行的分类 。
- 独享锁 (Exclusive Lock):同一时刻只能被一个线程持有。例如
synchronized、ReentrantLock、读写锁中的写锁。 - 共享锁 (Shared Lock):同一时刻可以被多个线程持有。例如读写锁中的读锁。
4. 可重入锁 vs 不可重入锁
- 可重入锁 (Reentrant Lock):同一个线程可以多次获取同一把锁而不会死锁。
synchronized和ReentrantLock都是可重入锁。其原理是在锁内部维护一个计数器,获取锁时+1,释放时-1,归零时真正释放 。 - 不可重入锁:线程必须释放锁后才能再次获取,否则死锁。Java 标准库中极少使用。
二、 synchronized 的锁升级机制
synchronized 是 Java 中最基础的内置锁。在 JDK 1.6 之前,它直接依赖操作系统的 Mutex Lock,性能较差。JDK 1.6 之后,JVM 引入了锁升级机制,根据竞争程度动态调整锁的状态,以平衡性能与安全性。
锁的状态由对象头中的 Mark Word 记录,升级过程如下:
1. 无锁 (No Lock)
对象创建初期,未被任何线程锁定。Mark Word 存储对象的 HashCode、分代年龄等信息。
2. 偏向锁 (Biased Locking)
- 场景:只有一个线程访问同步块。
- 原理:JVM 将对象头的 Mark Word 标记为偏向模式,并记录当前线程 ID。当该线程再次进入同步块时,只需比较线程 ID,无需进行 CAS 或原子操作,性能极高 。
- 注意:如果对象调用了
hashCode(),偏向锁通常会失效,因为 HashCode 需要占用 Mark Word 空间。
3. 轻量级锁 (Lightweight Locking)
- 场景:存在轻微竞争,多个线程交替执行同步块。
- 原理:
- 线程在栈帧中创建锁记录 (Lock Record)。
- 通过 CAS 操作将对象头的 Mark Word 替换为指向锁记录的指针。
- 如果 CAS 成功,获得锁;如果失败,说明有竞争,线程进行自旋 (Spin) 尝试获取 。
- 优势:避免了用户态到内核态的切换,比重量级锁轻。
4. 重量级锁 (Heavyweight Locking)
- 场景:竞争激烈,自旋超过阈值或有多个线程同时竞争。
- 原理:
- 轻量级锁膨胀为重量级锁。
- Mark Word 指向堆中的 ObjectMonitor 对象。
- 未获取锁的线程进入阻塞状态,依赖操作系统 Mutex 实现,涉及用户态与内核态切换,开销最大 。
重要提示:锁升级是单向不可逆的(无锁 -> 偏向 -> 轻量 -> 重量),一旦升级为重量级锁,即使后续无竞争,也不会降级 。
三、 JUC 包下的常用锁实战
除了 synchronized,java.util.concurrent.locks 包提供了更灵活的锁实现。
1. ReentrantLock:灵活的手动锁
ReentrantLock 是 synchronized 的功能增强版,支持公平/非公平、可中断、超时获取等特性 。
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock(); // 默认非公平
private int count = 0;
public void increment() {
lock.lock(); // 手动加锁
try {
count++;
} finally {
lock.unlock(); // 必须在 finally 中释放,防止死锁
}
}
// 尝试获取锁,支持超时
public boolean tryIncrement() {
if (lock.tryLock()) {
try {
count++;
return true;
} finally {
lock.unlock();
}
}
return false;
}
}
2. ReentrantReadWriteLock:读写分离
适用于读多写少的场景。读锁是共享锁,写锁是独享锁 。
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private String data = "Initial";
// 多个线程可同时读
public String read() {
rwLock.readLock().lock();
try {
return data;
} finally {
rwLock.readLock().unlock();
}
}
// 同一时刻只能有一个线程写,且不能有读
public void write(String newData) {
rwLock.writeLock().lock();
try {
this.data = newData;
} finally {
rwLock.writeLock().unlock();
}
}
}
3. 乐观锁实战:AtomicInteger
利用 CAS 机制实现无锁并发,性能在高并发下通常优于锁 。
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
// CAS 自旋更新,无需阻塞
count.incrementAndGet();
}
}
四、 总结与选型建议
| 锁类型 | 特点 | 适用场景 |
|---|---|---|
| synchronized | JVM 内置,自动加解锁,支持锁升级 | 简单同步场景,代码量少,竞争不极端激烈 |
| ReentrantLock | API 级别,支持公平/非公平、可中断、超时 | 需要高级控制(如超时、中断)或公平性要求 |
| ReentrantReadWriteLock | 读写分离,读共享写独占 | 读多写少,且读操作耗时较长 |
| Atomic 类 | CAS 无锁编程,高吞吐 | 简单的计数器、状态标志更新 |
选型指南:
- 首选
synchronized:代码简洁,JVM 优化越来越好,大多数场景足够用。 - 需要灵活控制选
ReentrantLock:如需要尝试获取锁、响应中断、公平锁等。 - 读多写少选
ReadWriteLock:大幅提升读并发性能。 - 简单原子操作选
Atomic:避免锁开销,极致性能。
理解锁的本质不是目的,目的是根据业务场景选择最合适的工具,在安全性与性能之间找到最佳平衡点。
评论区