通过CountDownLatch实现更准确的并发

本文通过实验展示了Java中ArrayList在多线程环境下可能出现的线程安全问题,并提出使用CountDownLatch来确保线程真正并发执行的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我们平时写并发测试代码,一般会使用一个for循环,比如启20个线程大概就是这样

for (int i = 0; i < 20; i++) {
	TASK_POOL.execute(task);
}

实际来讲这个写法并不是很严格,应为我们使用的是for循环,所有的线程都是按顺序创建的,并不符合我们想要的并发(至少在启动那一刻)

比如我们常用的java.util.ArrayList,这是一个非线程安全的类,比如add方法如下

/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
	ensureCapacityInternal(size + 1);  // Increments modCount!!
	elementData[size++] = e;
	return true;
}

每次新增之前都会判断容量,如果在并发情况下多个线程一起判断应该都能成功,但是下一步可能就会出现下标越界的问题,或者一个线程的数据覆盖另一个线程刚刚新增的数据

假如现在我们想用一段代码,证明在ArrayList多线程情况下存在问题,如下:

@Test
public void doTest() throws InterruptedException {
    final List<Integer> datas = new ArrayList<>();
    final ExecutorService TASK_POOL = Executors.newFixedThreadPool(20);
    Runnable task = () -> {
        for (int i = 0; i < 100; i++) {
            datas.add(i);
        }
    };
    for (int i = 0; i < 20; i++) {
        TASK_POOL.execute(task);
    }
    TASK_POOL.shutdown();
    TASK_POOL.awaitTermination(5, TimeUnit.SECONDS);
    System.out.println(datas.size());
}

代码很简单,就是启动20个线程往ArrayList里面增加数据,每个线程增加100个,最后输出这个集合的长度;如果这个类是线程安全的,最后的结果应该是2000,但是ArrayList并非线程安全,我们期待的结果应该是小于2000或出现下标越界的

然而情况是大部分情况下这个方法都能正常结束,输出结果是2000;

偶尔能达到我们想要的结果,出现下标越界或者最终的列表长度小于2000

Exception in thread "pool-1-thread-2" java.lang.ArrayIndexOutOfBoundsException: 10
	at java.util.ArrayList.add(ArrayList.java:463)
	at com.calix.test.ConcurrentTest.lambda$doTest$0(ConcurrentTest.java:20)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
1895

出现这个问题主要是我们创建的线程并没有我们预期的那样“并行”,没有发生很激烈的竞争。很多时候后一个线程执行时,前一个已经执行完成了。

想要达到更准确的并发,我们可以使用java.util.concurrent.CountDownLatch这个类,这个工具可以让我们的线程在一开始都处于“同一起跑线”,下面对之前的测试类进行调整

@Test
public void doTestWithCountDown() throws InterruptedException {
    final List<Integer> datas = new ArrayList<>();
    final ExecutorService TASK_POOL = Executors.newFixedThreadPool(20);
    final CountDownLatch countDownLatch = new CountDownLatch(20);
    Runnable task = () -> {
        try {
            countDownLatch.await();//这里等待其他线程就绪后开始放行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 100; i++) {
            datas.add(i);
        }
    };
    for (int i = 0; i < 20; i++) {
        TASK_POOL.execute(task);
        countDownLatch.countDown();//每个任务提交完毕后执行
    }
    TASK_POOL.shutdown();
    TASK_POOL.awaitTermination(5, TimeUnit.SECONDS);
    System.out.println(datas.size());
}

CountDownLatch的用法很简单,在倒计时结束前,await将一直阻塞,保证不会有那个线程先执行

改进过的测试代码可以很容易达到我们想要的测试结果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值