Java公平ReentrantLock多轮测试失准:问题排查与修正
测试问题分析与修复方案
原测试存在的核心问题
全局复用锁和Semaphore导致状态残留
原代码中ReentrantLock和Semaphore在循环外初始化,所有迭代共用同一实例。公平锁的等待队列绑定到锁实例本身,前一轮迭代的线程可能未完全从队列中清理,直接干扰后续迭代的任务排队顺序。未确保所有等待任务进入锁的等待队列
主线程释放Semaphore后,持有锁的任务会立即释放锁,但此时部分带序号的任务可能还未被线程池调度执行lock()调用。这些任务会在锁可用时直接获取锁(而非进入等待队列),公平锁"按等待顺序获取"的规则无法生效,最终导致共享状态的更新顺序混乱。Semaphore与锁的释放顺序存在竞态
原代码中持有锁的任务在finally块中同时释放Semaphore和锁,可能出现Semaphore释放后锁仍被持有的情况,进一步加剧任务调度的不确定性。
修改后的测试代码
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.util.List; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.IntStream; @Test void should_be_fair() throws InterruptedException, ExecutionException { int iterationsCount = 100; int waitingThreadsCount = 5; boolean wasAnyThreadUnfair = false; for (int i = 0; i < iterationsCount; i++) { // 每轮迭代重新创建锁和Semaphore,确保状态完全干净 ReentrantLock lock = new ReentrantLock(true); Semaphore unlockingSemaphore = new Semaphore(1); // 用CountDownLatch确保所有等待任务都已到达锁等待前的就绪状态 CountDownLatch allTasksReadyLatch = new CountDownLatch(waitingThreadsCount); ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(waitingThreadsCount + 1); // 持有锁的任务:先获取锁,再等待Semaphore释放 Future<?> lockingFuture = executor.submit(() -> { try { lock.lock(); unlockingSemaphore.acquire(); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { // 先释放锁,再释放Semaphore,避免竞态 lock.unlock(); unlockingSemaphore.release(); } }); AtomicInteger sharedState = new AtomicInteger(); List<Future<Integer>> futures = IntStream.rangeClosed(1, waitingThreadsCount) .sequential() .mapToObj(j -> executor.submit(() -> { try { // 标记当前任务已就绪 allTasksReadyLatch.countDown(); // 等待所有任务都就绪后,再尝试获取锁 allTasksReadyLatch.await(); lock.lock(); return sharedState.updateAndGet(k -> j); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } })) .toList(); // 等待所有带序号的任务都进入就绪状态,确保它们都会进入锁的等待队列 allTasksReadyLatch.await(); // 释放Semaphore,触发持有锁的任务释放锁 unlockingSemaphore.release(); // 等待所有任务执行完成 lockingFuture.get(); for (Future<Integer> f : futures) { f.get(); } // 确保线程池完全终止,避免线程复用影响下一轮迭代 executor.shutdown(); executor.awaitTermination(1, TimeUnit.SECONDS); System.out.println("Ended " + i + "-th cycle with the last index=" + sharedState.get()); if (sharedState.get() != waitingThreadsCount) { wasAnyThreadUnfair = true; break; } } Assertions.assertThat(wasAnyThreadUnfair).isFalse(); }
关键修改点说明
每轮重新初始化锁和Semaphore
彻底隔离各轮迭代的锁状态,避免前一轮的等待队列残留干扰后续测试。用CountDownLatch控制任务就绪时机
强制所有带序号的任务在尝试获取锁前同步就绪,确保它们都会进入公平锁的等待队列,严格遵循排队顺序获取锁。调整锁与Semaphore的释放顺序
在持有锁的任务中先释放锁再释放Semaphore,消除两者释放的竞态风险,保证锁释放的时机可控。等待线程池完全终止
通过awaitTermination确保本轮线程池的线程全部终止,避免线程复用带来的潜在干扰。
内容的提问来源于stack exchange,提问作者Andrey Mironov
相关产品推荐
相关产品推荐

