
公平锁的测试失败主因是线程提交顺序不等于实际加锁等待顺序,导致队列状态不可控;需通过 lock.getQueueLength() 精确等待线程入队,再释放锁以确保 FIFO 行为可复现。
公平锁的测试失败主因是线程提交顺序不等于实际加锁等待顺序,导致队列状态不可控;需通过 `lock.getqueuelength()` 精确等待线程入队,再释放锁以确保 fifo 行为可复现。
在 Java 并发编程中,ReentrantLock(true) 声明的公平锁(fair lock)承诺:当多个线程竞争同一把锁时,先调用 lock() 的线程将优先获得锁——即遵循严格的先进先出(FIFO)等待队列。但这一保证仅适用于线程已进入同步队列(AQS CLH queue)后的调度顺序,而非“任务提交顺序”或“线程启动时间”。你原始测试失败的根本原因,正在于此。
? 问题剖析:为什么“提交顺序 ≠ 等待顺序”?
在你的代码中:
List<Future<Integer>> futures = IntStream.rangeClosed(1, waitingThreadsCount)
.mapToObj(j -> executor.submit(() -> {
lock.lock(); // ⚠️ 此处可能立即成功(若锁空闲),或短暂阻塞后入队
// ...
}))
.toList();虽然按 j=1,2,3,4,5 提交任务,但线程调度、JVM 指令重排、锁释放时机等不确定性,会导致:
- 某些线程在
lockingFuture尚未lock()成功前就已执行到lock.lock(); - 若此时锁恰好被释放(如
lockingFuture已 unlock),该线程可能直接抢锁成功,跳过排队; - 结果:
j=3的线程比j=1更早获取锁 → 违反公平性预期,sharedState.get()≠5。
这并非 ReentrantLock 实现有缺陷,而是测试逻辑未强制线程按序进入等待队列。
立即学习“Java免费学习笔记(深入)”;
✅ 正确验证方式:基于队列长度的确定性同步
修复核心思想:确保第 j 个任务在恰好 j 个线程处于 lock 等待队列中时,才被允许尝试加锁。利用 ReentrantLock#getQueueLength()(线程安全、无副作用)实现精确等待:
List<Future<Integer>> futures = IntStream.rangeClosed(1, waitingThreadsCount)
.mapToObj(j -> {
Future<Integer> future = executor.submit(() -> {
try {
lock.lock();
System.out.println("Thread " + j + " acquired lock");
return sharedState.updateAndGet(k -> j);
} finally {
lock.unlock();
}
});
// ✅ 关键修复:等待恰好 j 个线程进入 AQS 队列
await().atMost(200, TimeUnit.MILLISECONDS)
.until(() -> lock.getQueueLength() == j);
return future;
})
.toList();?
getQueueLength()返回当前在lock()调用中阻塞、且已加入同步队列的线程数(不包括已唤醒但尚未完成加锁的线程)。它与公平性语义强一致——只有真正排队的线程才被计数。
? 完整可靠测试结构(精简版)
private boolean wasAnyThreadUnfair(ReentrantLock lock, int iterations, int threadCount)
throws InterruptedException {
Semaphore unlockSignal = new Semaphore(1);
for (int i = 0; i < iterations; i++) {
unlockSignal.acquire(); // Block until main locker is ready
ThreadPoolExecutor exec = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadCount + 1);
// Main locker: holds lock, then waits on semaphore
exec.submit(() -> {
lock.lock();
try { unlockSignal.acquire(); }
finally { lock.unlock(); }
});
AtomicInteger state = new AtomicInteger(0);
List<Future<Integer>> tasks = IntStream.rangeClosed(1, threadCount)
.mapToObj(j -> {
Future<Integer> f = exec.submit(() -> {
lock.lock();
try { return state.updateAndGet(k -> j); }
finally { lock.unlock(); }
});
// Wait until exactly j threads are queued → ensures FIFO order
await().atMost(300, TimeUnit.MILLISECONDS)
.until(() -> lock.getQueueLength() == j);
return f;
})
.toList();
unlockSignal.release(); // Let main locker release the lock
exec.shutdown();
exec.awaitTermination(2, TimeUnit.SECONDS);
if (state.get() != threadCount) {
System.err.println("Fairness violated at iteration " + i + ": got " + state.get());
return true;
}
}
return false;
}⚠️ 注意事项与最佳实践
-
勿依赖
Thread.sleep()或模糊等待:Awaitility的until()是声明式、自旋+回调机制,精度远高于固定休眠。 -
getQueueLength()是估算值:文档注明其返回“近似”长度,但在单线程控制释放锁的测试场景下,它是足够精确且稳定的判定依据。 -
避免锁外竞争:确保
sharedState更新逻辑极简(如本例的updateAndGet),防止因临界区过长引入额外调度干扰。 -
公平锁性能代价真实存在:开启公平模式会显著增加
lock()开销(需遍历队列),生产环境应权衡必要性。 -
不公平锁的验证需不同策略:如答案中
should_be_unfair()失败,是因为“非公平”仅表示 允许插队,不保证 必然插队;需大量迭代 + 统计显著性(如 1000 次中 >95% 出现非 FIFO),而非单次断言。
✅ 总结
公平锁的正确性验证,本质是对同步队列行为的白盒观测。放弃对“提交顺序”的直觉依赖,转而使用 getQueueLength() 锚定线程排队状态,配合 Awaitility 实现确定性时序控制,即可 100% 复现并验证公平性语义。这不仅是测试技巧,更是深入理解 AQS 和 Java 锁机制的关键实践。


















