You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java公平ReentrantLock多轮测试失准:问题排查与修正

测试问题分析与修复方案

原测试存在的核心问题

  1. 全局复用锁和Semaphore导致状态残留
    原代码中ReentrantLock和Semaphore在循环外初始化,所有迭代共用同一实例。公平锁的等待队列绑定到锁实例本身,前一轮迭代的线程可能未完全从队列中清理,直接干扰后续迭代的任务排队顺序。

  2. 未确保所有等待任务进入锁的等待队列
    主线程释放Semaphore后,持有锁的任务会立即释放锁,但此时部分带序号的任务可能还未被线程池调度执行lock()调用。这些任务会在锁可用时直接获取锁(而非进入等待队列),公平锁"按等待顺序获取"的规则无法生效,最终导致共享状态的更新顺序混乱。

  3. 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();
}

关键修改点说明

  1. 每轮重新初始化锁和Semaphore
    彻底隔离各轮迭代的锁状态,避免前一轮的等待队列残留干扰后续测试。

  2. 用CountDownLatch控制任务就绪时机
    强制所有带序号的任务在尝试获取锁前同步就绪,确保它们都会进入公平锁的等待队列,严格遵循排队顺序获取锁。

  3. 调整锁与Semaphore的释放顺序
    在持有锁的任务中先释放锁再释放Semaphore,消除两者释放的竞态风险,保证锁释放的时机可控。

  4. 等待线程池完全终止
    通过awaitTermination确保本轮线程池的线程全部终止,避免线程复用带来的潜在干扰。

内容的提问来源于stack exchange,提问作者Andrey Mironov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 13:13:08