1. 为什么需要深入理解Java Lock体系?
在Java并发编程的世界里,锁机制就像交通信号灯,协调着多个线程对共享资源的有序访问。我清楚地记得第一次遇到线程安全问题时的那种挫败感——明明逻辑正确的代码,在高并发场景下却产生了莫名其妙的结果。这就是为什么每个Java开发者都需要彻底吃透Lock体系的原因。
Java传统的synchronized关键字虽然简单易用,但在复杂的并发场景下显得力不从心。比如无法实现公平锁、不可中断的获取锁、超时等待等高级特性。而java.util.concurrent.locks包下的Lock接口及其实现类ReentrantLock,则提供了更灵活、更强大的线程同步机制。
提示:如果你还在面试中被问到"synchronized和ReentrantLock的区别",说明你对并发基础的理解还不够深入。真正的价值在于理解它们背后的实现原理和应用场景。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ReentrantLock的核心机制剖析
2.1 可重入性的实现原理
ReentrantLock的可重入特性是其最基础也是最重要的特性之一。所谓可重入,指的是同一个线程可以多次获取同一把锁而不会导致死锁。这种机制是通过一个计数器实现的:
java复制final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
这段代码展示了非公平锁的获取逻辑。当state为0时表示锁未被占用,通过CAS操作尝试获取;如果当前线程已经是锁的持有者,则简单地将state计数器增加,实现重入。
2.2 公平锁与非公平锁的选择
ReentrantLock提供了公平和非公平两种模式,这是synchronized无法实现的特性。公平锁严格按照请求顺序分配锁,而非公平锁允许"插队":
java复制// 公平锁实现
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && // 关键区别:检查是否有等待更久的线程
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// ...重入逻辑与非公平锁相同
}
在实际应用中,非公平锁的吞吐量通常更高,因为减少了线程切换的开销。但在需要严格保证公平性的场景(如防止线程饥饿),公平锁是更好的选择。
注意:不要盲目使用公平锁。在我的性能测试中,公平锁在高竞争场景下的吞吐量可能只有非公平锁的1/10。
3. AQS(AbstractQueuedSynchronizer)深度解析
3.1 AQS的核心设计思想
AQS是Java并发包中最基础也最复杂的框架之一,它采用模板方法模式,将同步器的公共行为抽象出来,留给子类实现特定的获取/释放逻辑。其核心是一个FIFO的等待队列和一个volatile的state变量:
java复制// AQS的等待队列节点
static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
}
AQS的精妙之处在于它用单一的int状态(state)表示不同的同步状态。比如在ReentrantLock中,state表示重入次数;在Semaphore中,state表示可用许可数;在CountDownLatch中,state表示剩余计数。
3.2 独占模式与共享模式
AQS支持两种同步模式:
- 独占模式:同一时刻只有一个线程能获取资源(如ReentrantLock)
- 共享模式:多个线程可以同时获取资源(如Semaphore、CountDownLatch)
这两种模式通过不同的方法模板实现:
java复制// 独占模式
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }
// 共享模式
protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException(); }
protected boolean tryReleaseShared(int arg) { throw new UnsupportedOperationException(); }
3.3 条件变量的实现
ConditionObject是AQS的内部类,实现了Condition接口,为锁提供了更灵活的等待/通知机制。与Object的wait/notify相比,它的优势在于:
- 一个锁可以创建多个Condition
- 支持不响应中断的条件等待
- 提供精确的通知机制(signal vs signalAll)
java复制// 典型的生产者-消费者模式使用示例
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putPtr] = x;
if (++putPtr == items.length) putPtr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
// ...类似的take方法
}
4. CAS(Compare-And-Swap)底层原理
4.1 CAS操作的本质
CAS是现代并发编程的基石,它是一种无锁算法,包含三个操作数:
- 内存位置(V)
- 预期原值(A)
- 新值(B)
当且仅当V的值等于A时,处理器才会用B更新V的值,否则不执行任何操作。整个操作是一个原子指令。
在Java中,CAS操作通过sun.misc.Unsafe类提供支持:
java复制public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
4.2 CAS的典型应用场景
- 原子类(AtomicInteger等)的实现:
java复制public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
- 自旋锁的实现:
java复制while (!compareAndSet(state, 0, 1)) {
// 自旋等待
}
- 乐观锁的实现:
java复制do {
oldValue = atomicRef.get();
newValue = computeNewValue(oldValue);
} while (!atomicRef.compareAndSet(oldValue, newValue));
4.3 CAS的ABA问题及解决方案
ABA问题是CAS操作中的一个经典问题:如果一个值原来是A,变成了B,又变回了A,那么CAS检查时会认为它没有被修改过。这在某些场景下会导致问题。
解决方案是使用版本号或时间戳。Java提供了AtomicStampedReference:
java复制AtomicStampedReference<Integer> atomicRef = new AtomicStampedReference<>(100, 0);
int[] stampHolder = new int[1];
int oldValue = atomicRef.get(stampHolder);
int newStamp = stampHolder[0] + 1;
atomicRef.compareAndSet(oldValue, 200, stampHolder[0], newStamp);
5. 实战中的经验与陷阱
5.1 锁的最佳实践
- 始终在finally块中释放锁:
java复制Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
-
避免嵌套锁:如果必须使用多个锁,确保以一致的顺序获取它们,防止死锁。
-
使用tryLock避免死锁:
java复制if (lock1.tryLock()) {
try {
if (lock2.tryLock()) {
try {
// 操作共享资源
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
5.2 性能调优技巧
-
减小锁的粒度:将一个大锁拆分为多个小锁,减少竞争。
-
使用读写锁(ReentrantReadWriteLock)提升读多写少场景的性能:
java复制ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
- 考虑使用StampedLock的乐观读:
java复制StampedLock lock = new StampedLock();
long stamp = lock.tryOptimisticRead();
// 读取共享变量
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
// 重新读取
} finally {
lock.unlockRead(stamp);
}
}
5.3 常见问题排查
-
死锁诊断:使用jstack或VisualVM检测死锁线程。
-
锁争用分析:通过JFR(Java Flight Recorder)监控锁的等待时间。
-
内存泄漏:确保Condition等待的线程能被正确唤醒,防止因异常导致锁未被释放。
-
性能瓶颈:使用JMH进行基准测试,比较不同锁实现的性能差异。
6. 从源码角度理解Lock体系
6.1 ReentrantLock的加锁流程
以非公平锁为例,完整的加锁流程如下:
- 首先尝试快速获取锁(compareAndSetState)
- 如果失败,调用acquire方法
- acquire会先调用tryAcquire再次尝试获取锁
- 如果仍然失败,将当前线程包装为Node加入等待队列
- 在队列中自旋或阻塞,等待被唤醒
java复制// 非公平锁的加锁流程
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
6.2 AQS的等待队列管理
AQS使用CLH队列的变体管理等待线程。当一个线程获取锁失败时,会被包装为Node加入队列尾部:
java复制private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
队列中的线程会自旋检查前驱节点是否为头节点,如果是则尝试获取锁;否则可能被挂起。
6.3 Condition的等待/通知机制
Condition的await()方法会释放锁并将线程加入条件队列,signal()方法会将节点从条件队列转移到同步队列:
java复制public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter(); // 加入条件队列
int savedState = fullyRelease(node); // 完全释放锁
while (!isOnSyncQueue(node)) {
LockSupport.park(this); // 挂起线程
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 被唤醒后重新获取锁
}
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first); // 将节点转移到同步队列
}
7. 现代并发编程的发展趋势
虽然Java的Lock体系已经非常成熟,但并发编程仍在不断发展。一些值得关注的新趋势包括:
-
协程(虚拟线程):Java 19引入的虚拟线程可以大幅减少线程切换开销,可能改变传统的锁使用模式。
-
无锁数据结构:随着硬件发展,CAS-based的无锁算法在更多场景下取代传统锁。
-
响应式编程:Project Reactor等框架提供了更高级的并发抽象。
-
结构化并发:Java 19的StructuredTaskScope尝试提供更安全的并发编程模型。
然而,无论技术如何发展,理解底层原理(如AQS、CAS)仍然是成为高级Java开发者的必经之路。这些知识不仅能帮助你在面试中脱颖而出,更重要的是能让你在面对复杂并发问题时游刃有余。
