ReentrantLock 完全教程:从使用到源码深度解析
ReentrantLock 是 Java 并发包(java.util.concurrent.locks)中的核心类,实现了 可重入互斥锁,相比 synchronized 关键字,提供了更灵活的锁控制、可中断性、公平锁策略和多个条件变量支持。以下是其设计思想、实现原理及最佳实践的系统分析。
一、ReentrantLock 基础认知
1. 核心特性
- 可重入:同一线程可以多次获取同一把锁,锁的计数器会累加,释放时需对应次数的解锁。
- 独占性:同一时刻只有一个线程能持有锁。
- 公平 / 非公平:默认非公平锁(性能更高),也可指定公平锁(按等待队列顺序获取锁)。
- 可中断:支持通过
lockInterruptibly()响应线程中断。 - 超时获取:通过
tryLock(long timeout, TimeUnit unit)实现超时未获取则放弃。 - 条件变量:通过
newCondition()获取Condition对象,实现多条件等待 / 通知。
2. 与 synchronized 的对比
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 锁获取方式 | 手动调用 lock()/unlock() |
隐式获取释放(代码块 / 方法) |
| 公平锁支持 | 支持(构造函数指定) | 仅非公平锁 |
| 可中断锁获取 | 支持(lockInterruptibly()) |
不支持 |
| 超时获取锁 | 支持(tryLock(timeout)) |
不支持 |
| 条件变量 | 多个 Condition |
仅一个(wait()/notify()) |
| 锁状态查询 | 支持(isLocked()/getHoldCount()) |
不支持 |
| 性能 | 高并发下更优 | JDK 1.6 后优化,差距缩小 |
二、ReentrantLock 实战使用
1. 基本使用:加锁与解锁
核心要点:必须在 finally 块中解锁,避免异常导致锁无法释放。
import java.time.LocalDateTime;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockBasicDemo {
// 创建非公平锁(默认)
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
// 启动两个线程
new Thread(ReentrantLockBasicDemo::doWork, "Thread-1").start();
new Thread(ReentrantLockBasicDemo::doWork, "Thread-2").start();
new Thread(ReentrantLockBasicDemo::doWork, "Thread-3").start();
new Thread(ReentrantLockBasicDemo::doWork, "Thread-4").start();
new Thread(ReentrantLockBasicDemo::doWork, "Thread-5").start();
new Thread(ReentrantLockBasicDemo::doWork, "Thread-6").start();
}
private static void doWork() {
// 获取锁
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获取锁,开始执行任务" + lock.isLocked() + "~~~"
+ LocalDateTime.now());
Thread.sleep(2000); // 模拟任务执行
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放锁(必须在finally中)
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放锁,任务执行完成" + lock.isLocked() + "~~~"
+ LocalDateTime.now());
}
}
}
6. 条件变量(Condition)的使用
Condition 是 ReentrantLock 的条件变量,替代 Object.wait()/notify(),支持多条件等待(一个锁可关联多个 Condition)。
核心方法:
await():当前线程释放锁并进入条件队列等待。signal():唤醒条件队列中的一个线程。signalAll():唤醒条件队列中的所有线程。
示例:生产者 - 消费者模型(多条件)
import java.time.LocalDateTime;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockConditionDemo {
private static final int CAPACITY = 5; // 队列容量
private final Queue<Integer> queue = new LinkedList<>();
private final ReentrantLock lock = new ReentrantLock();
// 队列满的条件
private final Condition fullCondition = lock.newCondition();
// 队列空的条件
private final Condition emptyCondition = lock.newCondition();
// 生产者
public void produce(int num) throws InterruptedException {
lock.lock();
try {
// 队列满则等待
while (queue.size() == CAPACITY) {
System.out.println("队列满,生产者" + Thread.currentThread().getName() + "等待" + LocalDateTime.now());
fullCondition.await();
}
queue.offer(num);
System.out.println("生产者" + Thread.currentThread().getName() + "生产:" + num + LocalDateTime.now());
emptyCondition.signal(); // 唤醒消费者
} finally {
lock.unlock();
}
}
// 消费者
public int consume() throws InterruptedException {
lock.lock();
try {
// 队列空则等待
while (queue.isEmpty()) {
System.out.println("队列空,消费者" + Thread.currentThread().getName() + "等待" + LocalDateTime.now());
emptyCondition.await();
}
int num = queue.poll();
System.out.println("消费者" + Thread.currentThread().getName() + "消费:" + num + LocalDateTime.now());
fullCondition.signal(); // 唤醒生产者
return num;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockConditionDemo demo = new ReentrantLockConditionDemo();
// 启动3个生产者
for (int i = 0; i < 3; i++) {
int producerNum = i;
new Thread(() -> {
try {
for (int j = 0; j < 3; j++) {
demo.produce(producerNum * 10 + j);
Thread.sleep(500);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "P" + i).start();
}
// 启动2个消费者
for (int i = 0; i < 2; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 5; j++) {
demo.consume();
Thread.sleep(1000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "C" + i).start();
}
}
}
3. 公平锁 vs 非公平锁
- 非公平锁(默认):线程获取锁时直接尝试抢占,不遵守等待队列顺序,性能更高(减少上下文切换),但可能导致线程饥饿。
- 公平锁:线程按等待队列的先后顺序获取锁,不会出现饥饿,但性能较低(需维护队列顺序)。
公平锁创建:通过构造函数 new ReentrantLock(true) 指定。
package com.ty.inteplm.passport.controller;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockFairDemo {
private static final ReentrantLock fairLock = new ReentrantLock(true);
private static final ReentrantLock nonFairLock = new ReentrantLock(true);
public static void main(String[] args) {
// 测试公平锁
System.out.println("=== 公平锁测试 ===");
for (int i = 0; i < 100; i++) {
new Thread(() -> {
fairLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获取公平锁");
} finally {
fairLock.unlock();
}
}, "Fair-Thread-" + i).start();
}
// 当这里的循环次数改为10000时,公平锁的输出结果会比非公平锁的输出结果多一些,因为公平锁会按照FIFO的顺序获取锁,而非公平锁会尝试获取锁,如果获取失败则进入队列等待。
// 但是公平锁不是一定要找FIFO的, 也会出现一些错乱.
// 等待公平锁测试完成
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 测试非公平锁
System.out.println("\n=== 非公平锁测试 ===");
for (int i = 0; i < 100; i++) {
new Thread(() -> {
nonFairLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获取非公平锁");
} finally {
nonFairLock.unlock();
}
}, "NonFair-Thread-" + i).start();
}
}
}
5. 可中断的锁获取(lockInterruptibly)
lockInterruptibly() 允许线程在等待获取锁时响应中断,避免线程无限期阻塞。
import java.time.LocalDateTime;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockInterruptDemo {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
lock.lock();
try {
System.out.println("Thread-1 持有锁,执行任务..." + LocalDateTime.now());
Thread.sleep(3000); // 持有锁3秒
} catch (InterruptedException e) {
System.out.println("Thread-1 被中断" + LocalDateTime.now());
} finally {
lock.unlock();
System.out.println("Thread-1 释放锁" + LocalDateTime.now());
}
});
Thread thread2 = new Thread(() -> {
try {
// 可中断的锁获取
System.out.println("Thread-2 尝试获取锁..." + LocalDateTime.now());
lock.lockInterruptibly();
try {
System.out.println("Thread-2 获取锁成功" + LocalDateTime.now());
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("Thread-2 等待锁时被中断,退出" + LocalDateTime.now());
}
});
thread1.start();
Thread.sleep(1000); // 确保thread1先获取锁
thread2.start();
Thread.sleep(1000);
thread2.interrupt(); // 中断thread2的锁等待
}
}
2. 可重入性演示
ReentrantLock 允许同一线程多次获取锁,锁的持有计数器会累加,释放时需调用相同次数的 unlock()。
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockReentrantDemo {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
System.out.println(lock.isFair() + "~~~" + lock.isLocked());
try {
lock.lock();
System.out.println("第一次获取锁,持有计数:" + lock.getHoldCount());
doWork();
} finally {
lock.unlock();
System.out.println("最终释放锁,持有计数:" + lock.getHoldCount());
}
}
private static void doWork() {
lock.lock();
try {
System.out.println("第二次获取锁,持有计数:" + lock.getHoldCount());
} finally {
lock.unlock();
System.out.println("子任务释放锁,持有计数:" + lock.getHoldCount());
}
}
}
4. 尝试获取锁(tryLock)
tryLock() 方法提供非阻塞式锁获取:
tryLock():立即尝试获取锁,成功返回true,失败返回false(不阻塞)。tryLock(long timeout, TimeUnit unit):超时时间内尝试获取锁,超时未获取则返回false,支持中断。
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTryLockDemo {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
lock.lock();
try {
System.out.println("Thread-1 持有锁,执行任务中..." + LocalDateTime.now());
Thread.sleep(3000); // 持有锁2秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
System.out.println("Thread-1 释放锁" + LocalDateTime.now());
}
});
Thread thread2 = new Thread(() -> {
try {
// 尝试在1秒内获取锁
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("Thread-2 获取锁成功" + LocalDateTime.now());
} finally {
lock.unlock();
}
} else {
System.out.println("Thread-2 等待1秒后仍未获取锁,放弃" + LocalDateTime.now());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Thread-2 获取锁时被中断" + LocalDateTime.now());
}
});
thread1.start();
thread2.start();
}
}
ReentrantLock 源码解析
ReentrantLock 的核心实现依赖于 AQS(AbstractQueuedSynchronizer,抽象队列同步器),AQS 是 Java 并发包的基础,通过状态变量(state)和同步队列实现锁的管理。
1. ReentrantLock 类结构
ReentrantLock 内部封装了一个Sync抽象类(继承 AQS),并提供了 **NonfairSync(非公平锁)和FairSync(公平锁)** 两个实现:
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
// 抽象同步器,继承AQS
abstract static class Sync extends AbstractQueuedSynchronizer {
// 抽象方法:获取锁
abstract void lock();
// 非公平锁的尝试获取
final boolean nonfairTryAcquire(int acquires) { ... }
// 释放锁
protected final boolean tryRelease(int releases) { ... }
// ... 其他方法
}
// 非公平锁实现
static final class NonfairSync extends Sync { ... }
// 公平锁实现
static final class FairSync extends Sync { ... }
// 构造函数:默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 构造函数:指定公平/非公平
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// 加锁(调用Sync的lock方法)
public void lock() {
sync.lock();
}
// 解锁(调用AQS的release方法)
public void unlock() {
sync.release(1);
}
// ... 其他方法(newCondition、tryLock等)
}
2. AQS 核心基础
AQS 是 ReentrantLock 的底层支撑,需先理解其核心要素:
- 状态变量(state):
volatile int state,用于表示锁的持有状态。- ReentrantLock 中,
state=0表示锁未被持有;state>0表示锁被持有,数值为重入次数。
- ReentrantLock 中,
- 同步队列(CLH 队列):双向链表,存储等待获取锁的线程,节点类型为
Node(包含线程引用、等待状态、前驱 / 后继节点)。 - 条件队列:每个
Condition对应一个单向链表,存储调用await()的线程,等待被唤醒后转入同步队列。 - 核心方法:
acquire(int arg):独占式获取锁,失败则入队等待。release(int arg):独占式释放锁,唤醒队列中的后继节点。tryAcquire(int arg):尝试获取锁(子类实现)。tryRelease(int arg):尝试释放锁(子类实现)。
3. 非公平锁(NonfairSync)源码解析
非公平锁是 ReentrantLock 的默认实现,核心流程为:抢占锁 → 失败则入队等待。
3.1 加锁:lock () 方法
static final class NonfairSync extends Sync {
final void lock() {
// 1. 直接尝试CAS修改state为1(抢占锁)
if (compareAndSetState(0, 1))
// 成功:设置当前线程为独占线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 2. 失败:调用AQS的acquire方法继续获取
acquire(1);
}
// ... 其他方法
}
- CAS 抢占:
compareAndSetState(0, 1)是原子操作,直接尝试将 state 从 0 改为 1,成功则直接持有锁(非公平性体现:不排队,直接抢)。 - acquire(1):AQS 的核心方法,继续尝试获取锁,失败则入队。
3.2 AQS 的 acquire (int arg) 方法
public final void acquire(int arg) {
// 1. tryAcquire:再次尝试获取锁(子类实现)
// 2. addWaiter:创建节点并加入同步队列
// 3. acquireQueued:在队列中等待获取锁
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt(); // 若被中断,自我中断
}
3.3 非公平锁的 tryAcquire 方法
tryAcquire 由 NonfairSync 实现(调用父类 Sync 的 nonfairTryAcquire):
abstract static class Sync extends AbstractQueuedSynchronizer {
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 1. 锁未被持有(state=0)
if (c == 0) {
// 再次CAS抢占(非公平性:不检查队列)
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 2. 锁已被当前线程持有(可重入)
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // 溢出
throw new Error("Maximum lock count exceeded");
setState(nextc); // 累加state(重入次数)
return true;
}
// 3. 锁被其他线程持有,获取失败
return false;
}
}
核心逻辑:
- 锁未被持有:直接 CAS 抢占(非公平)。
- 锁被当前线程持有:累加 state(可重入)。
- 锁被其他线程持有:返回 false。
3.4 入队等待:addWaiter + acquireQueued
如果 tryAcquire 失败,会通过 addWaiter 将当前线程封装为 Node 加入同步队列,再通过 acquireQueued 自旋等待锁:
-
addWaiter(Node mode):创建节点并加入队列尾部
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // 快速尝试CAS加入尾部 Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 失败则通过enq入队(自旋CAS) enq(node); return node; } -
acquireQueued(Node node, int arg):节点在队列中自旋等待
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { // 自旋 final Node p = node.predecessor(); // 获取前驱节点 // 前驱是头节点,再次尝试获取锁 if (p == head && tryAcquire(arg)) { setHead(node); // 成为新的头节点 p.next = null; // 帮助GC failed = false; return interrupted; } // 检查是否需要阻塞,若需要则park当前线程 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); // 取消获取 } }
核心逻辑:节点在队列中自旋,只有前驱是头节点时才尝试获取锁,否则阻塞等待被唤醒。
4. 公平锁(FairSync)源码解析
公平锁与非公平锁的核心差异在 tryAcquire 方法:公平锁会检查同步队列是否有前驱节点,只有队首节点才能获取锁。
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 公平性关键:hasQueuedPredecessors()检查是否有前驱节点
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 可重入逻辑(与非公平锁一致)
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
// AQS的hasQueuedPredecessors方法:检查是否有前驱节点
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 1. 头节点 != 尾节点
// 2. 头节点的后继节点为null,或后继节点的线程不是当前线程
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
公平性体现:hasQueuedPredecessors() 确保只有队列中第一个等待的线程才能获取锁,不会出现抢占。
5. 锁释放:unlock () 方法
ReentrantLock 的 unlock() 调用 AQS 的 release(1) 方法,最终由 Sync 的 tryRelease 实现释放:
5.1 unlock () 入口
public void unlock() {
sync.release(1);
}
5.2 AQS 的 release 方法
public final boolean release(int arg) {
// 1. 尝试释放锁
if (tryRelease(arg)) {
Node h = head;
// 2. 头节点不为null且状态不为0,唤醒后继节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
5.3 Sync 的 tryRelease 方法
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 减少重入计数
// 当前线程不是持有锁的线程,抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // 重入计数为0,真正释放锁
free = true;
setExclusiveOwnerThread(null); // 清空独占线程
}
setState(c); // 更新state(c>0时仅减少计数,未真正释放)
return free;
}
核心逻辑:
- 释放锁时先减少 state 计数,只有计数为 0 时才真正释放锁(清空独占线程)。
- 释放成功后,唤醒同步队列中的后继节点,使其继续尝试获取锁。
6. 可重入性的源码体现
可重入性的核心在 tryAcquire 方法中:当锁已被当前线程持有,只需累加 state 计数,无需重新获取锁。
以非公平锁为例:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 首次获取锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 锁已被当前线程持有,累加state
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
释放时,tryRelease 会逐次减少 state 计数,只有计数为 0 时才真正释放锁,保证了重入次数与释放次数的匹配。
7. Condition 条件变量源码解析
ReentrantLock 的 newCondition() 返回 ConditionObject(AQS 的内部类),实现了 Condition 接口:
public Condition newCondition() {
return sync.newCondition();
}
// Sync的newCondition方法
final ConditionObject newCondition() {
return new ConditionObject();
}
7.1 ConditionObject 核心结构
- 条件队列:单向链表,存储调用
await()的线程节点(Node类型)。 - await():当前线程释放锁,加入条件队列并阻塞,被唤醒后转入同步队列。
- signal():将条件队列的头节点转移到同步队列,等待获取锁。
7.2 await () 方法核心逻辑
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 1. 将当前线程加入条件队列
Node node = addConditionWaiter();
// 2. 释放锁(重入次数清零)
int savedState = fullyRelease(node);
int interruptMode = 0;
// 3. 检查是否在同步队列中,不在则park
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 4. 被唤醒后,尝试获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 5. 清理条件队列的取消节点
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
7.3 signal () 方法核心逻辑
public final void signal() {
// 检查当前线程是否持有锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter; // 条件队列头节点
if (first != null)
doSignal(first); // 转移头节点到同步队列
}
private void doSignal(Node first) {
do {
// 移除条件队列的头节点
if ((firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) && (first = firstWaiter) != null);
}
// 将条件节点转移到同步队列
final boolean transferForSignal(Node node) {
// CAS修改节点状态为CONDITION
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 加入同步队列尾部
Node p = enq(node);
int ws = p.waitStatus;
// 唤醒节点线程
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
核心逻辑:signal() 将条件队列的节点转移到同步队列,await() 被唤醒后在同步队列中等待获取锁。
一、先明确:公平锁的 “公平” 到底指什么?
ReentrantLock 公平锁的设计目标是 **「同步队列中的等待线程,按照 “先到先得” 的顺序获取锁」**,而非 “所有线程的锁请求必须严格按时间顺序执行”。
公平锁的核心约束通过 hasQueuedPredecessors() 方法实现:
java
运行
// AQS中公平锁的关键检查:是否有前驱节点在队列中等待
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 条件1:头节点 != 尾节点(队列中有等待线程)
// 条件2:头节点的后继节点为null,或后继节点的线程不是当前线程
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
只有当同步队列为空,或者当前线程是同步队列的第一个等待节点时,公平锁才会允许线程获取锁。
简单说:公平锁的 “公平” 是对排队的线程公平,而非对 “所有发起请求的线程公平”。
二、为什么公平锁测试中会出现 “乱序”?
你观察到的 “偶尔 2-3 个线程未按顺序获取锁”,本质是测试场景中的线程调度、队列状态、代码设计等因素,导致锁请求的 “实际竞争时机” 与你预期的 “线程启动顺序” 不一致。具体原因有以下 4 点:
1. 线程启动与调度的延迟(最主要原因)
Java 中 Thread.start() 只是将线程提交给操作系统调度器,线程实际执行 lock() 方法的时间由操作系统决定,而非严格按 start() 的顺序。
比如你循环启动 100 个线程:
for (int i = 0; i < 100; i++) {
new Thread(() -> {
fairLock.lock(); // 实际执行lock的时间可能乱序
try { /* 业务逻辑 */ } finally { fairLock.unlock(); }
}, "Thread-" + i).start();
}
- 你预期
Thread-0→Thread-1→ ... →Thread-99依次执行lock(),但操作系统可能先调度Thread-5执行lock(),此时同步队列为空,Thread-5直接获取锁,看似 “乱序”。 - 这种 “乱序” 不是公平锁的问题,而是线程启动的调度延迟导致锁请求的实际发起顺序与你预期的不一致。
2. 同步队列为空时的新线程抢占
公平锁的 “排队” 仅针对同步队列中已有等待线程的场景。如果某个线程释放锁后,同步队列变为空,此时恰好有新线程发起锁请求,这个新线程会直接获取锁,而非创建节点排队。
举个例子:
Thread-0获取锁并执行,同步队列为空。Thread-0释放锁后,同步队列仍为空。- 此时
Thread-5刚好发起锁请求(因调度延迟,它比Thread-1先执行lock()),由于同步队列为空,Thread-5直接获取锁。 - 后续
Thread-1发起请求时,同步队列已空(Thread-5持有锁),Thread-1进入队列等待,看似 “Thread-5插队”。
这是公平锁的正常逻辑:公平锁只保证 “队列中的线程按顺序获取”,不禁止 “无队列时的新线程直接获取”。
3. 线程的中断 / 取消导致队列节点变化
如果队列中的某个线程因中断(lockInterruptibly())或超时(tryLock(timeout))取消了锁等待,该线程节点会被移出同步队列,后续线程会提前获取锁,导致你观察到的 “顺序跳跃”。
比如队列顺序是 Thread-1 → Thread-2 → Thread-3,若 Thread-2 被中断并取消等待,Thread-3 会成为 Thread-1 的后继节点,Thread-1 释放锁后,Thread-3 直接获取锁,跳过了 Thread-2。
4. 测试代码的设计缺陷
如果测试中线程获取锁后执行任务的时间过短,锁的释放和获取会频繁交替,操作系统的线程调度随机性会被放大,更容易出现 “看似乱序” 的情况。
比如线程获取锁后仅打印一行日志就释放,此时调度器可能优先唤醒刚释放锁的线程(或其他新线程),而非队列中的等待线程(但这并非公平锁的逻辑问题,而是调度器的短期行为)。
三、为什么还要使用公平锁?(公平锁的实际价值)
尽管公平锁在测试中看似 “不那么严格”,但在对 “线程饥饿” 敏感的业务场景中,公平锁是不可替代的,核心价值体现在两点:
1. 解决线程饥饿问题
非公平锁中,线程可能通过 “抢占” 反复获取锁,导致某些等待时间长的线程一直抢不到锁(饥饿)。而公平锁通过 “队列顺序” 保证:只要线程进入同步队列,最终一定会按顺序获取锁,不会出现永久饥饿。
2. 满足业务的 “顺序性” 要求
在某些对执行顺序有严格要求的场景,必须用公平锁:
- 任务调度:比如按用户提交的顺序执行任务,避免后提交的任务先执行。
- 金融交易:比如按交易发起的时间顺序处理订单,防止插队导致的业务逻辑混乱。
- 资源分配:比如按请求顺序分配有限资源,保证公平性。
3. 公平锁的性能取舍
公平锁的缺点是性能低于非公平锁(需维护队列顺序、减少抢占),但在上述场景中,业务的公平性 / 正确性优先级高于性能,因此必须使用公平锁。
四、如何正确测试公平锁,让其体现 “严格顺序”?
要让公平锁的 “顺序性” 更明显,需消除线程启动调度延迟的影响,可通过 CountDownLatch 让所有线程 “统一启动” 后再发起锁请求:
java
运行
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;
public class FairLockStrictTest {
private static final ReentrantLock fairLock = new ReentrantLock(true);
private static final int THREAD_COUNT = 100;
// 用于让所有线程统一启动
private static final CountDownLatch startLatch = new CountDownLatch(1);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < THREAD_COUNT; i++) {
int threadNum = i;
new Thread(() -> {
try {
// 等待统一启动信号
startLatch.await();
fairLock.lock();
try {
System.out.println("线程" + threadNum + "获取锁");
Thread.sleep(10); // 模拟少量任务,放大顺序性
} finally {
fairLock.unlock();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "FairThread-" + i).start();
}
// 所有线程已启动,释放启动信号
startLatch.countDown();
}
}
测试效果:此时线程会按 0→1→2→...→99 的顺序获取锁(偶尔因系统调度有 1-2 个偏差,但整体顺序严格),公平锁的特性会充分体现。
四、总结与最佳实践
1. 核心总结
- ReentrantLock 基于 AQS 实现,通过
state变量管理重入次数,通过同步队列管理等待线程。 - 非公平锁性能更高(默认),公平锁更公平但性能低。
- 可重入性通过累加 / 减少
state实现,释放时需匹配重入次数。 - Condition 条件变量实现了多条件等待 / 通知,比
Object.wait()更灵活。
2. 最佳实践
- 优先使用 synchronized:JDK 1.6 后
synchronized已优化,简单场景下更简洁、安全。 - 使用 ReentrantLock 的场景:需要公平锁、可中断锁、超时锁、多条件变量时。
- 必须在 finally 中解锁:避免异常导致锁泄漏。
- 减少锁的持有时间:仅在必要代码块加锁,提升并发性能。
- 避免重入次数过多:防止
state溢出或逻辑混乱。
更多推荐




所有评论(0)