java并发编程(六)CountDownLatch和回环屏障CyclicBarrier

一、CountDownLatch源码分析

1.构造函数

java 复制代码
public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

2.内部类Sync

java 复制代码
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }

        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
//减完之后是0才会返回true
        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

3.await函数

java 复制代码
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
java 复制代码
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
  1. await() 调用 AQS 的 acquireSharedInterruptibly(1)。

  2. AQS 回调 Sync 重写的 tryAcquireShared(1)。

  3. 如果是 0: 如果 state == 0,则直接返回,不阻塞。

  4. 关键判断: 如果 state != 0,tryAcquireShared 返回 -1,表示获取失败,执行doAcquireSharedInterruptibly

  5. AQS 会将当前线程封装成 Node 放入等待队列,并将其挂起(LockSupport.park)。

java 复制代码
private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
//将当前节点加入等待队列
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
//如果前驱节点是 head,说明当前节点是队列中的第一个等待者,尝试获取锁
                if (p == head) {
                    int r = tryAcquireShared(arg);
//获取成功,设置自己为新的 head,并唤醒后续的共享模式节点
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
//阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

4.countDown()

java 复制代码
    public void countDown() {
        sync.releaseShared(1);
    }
java 复制代码
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
  1. countDown() 调用 AQS 的 releaseShared(1)。

  2. AQS 回调 Sync 重写的 tryReleaseShared(1)。

  3. 在 tryReleaseShared 中,使用 CAS + 自旋 的方式将 state 减 1。

  4. 唤醒时机: 只有当 state 从 1 变为 0 时,tryReleaseShared 才会返回 true。

  5. AQS 收到 true 后,调用 doReleaseShared() 唤醒同步队列中所有因为 await() 而阻塞的线程。

java 复制代码
 private void doReleaseShared() {

        for (;;) {
            Node h = head;
//不为空并且有等待节点
            if (h != null && h != tail) {
                int ws = h.waitStatus;
//后续节点等待被唤醒
                if (ws == Node.SIGNAL) {
// 重置头节点状态为 0。
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;
//唤醒下一个await节点           
                    unparkSuccessor(h);
                }
//尝试将其改为 PROPAGATE (-3),确保后续获取锁时能继续向后传播
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
// 如果在处理过程中 head 没有变,说明唤醒动作已经平稳传递,可以退出
// 如果 head 变了(说明被唤醒的线程已经抢占了 head),必须继续循环检查新 head 的后继
            if (h == head)                 
                break;
        }
    }

二、CutDownLatch与join的区别

|-----------|-----------------------------|----------------------------------------|
| 特性 | Thread.join() | CountDownLatch |
| 颗粒度 | 线程级。必须等待线程对象执行结束(死亡)。 | 任务级。可以在任务执行过程中的任何地方计数,不要求线程结束。 |
| 灵活性 | 较低。只能等待整个线程运行完。 | 较高。一个线程可以多次 countDown,或者一个任务完成后继续做别的事。 |
| 等待关系 | 通常是一个线程等待另一个线程。 | 可以是多个线程等待多个线程(多对多)。 |
| 原理 | 基于 Object.wait() 和线程存活状态检查。 | 基于 AQS 的共享模式状态(state)。 |
| 使用便利性 | 简单,直接对线程对象操作。 | 稍微复杂,需要手动管理计数器。 |

使用线程池来管理线程时一般都是直接添加Runable到线程池,这时候就没有办法再调用线程的join方法了,就是说countDownLatch相比join方法让我们对线程同步有更灵活的控制。

三、回环屏障 CyclicBarrier

上节介绍的CountDownLatch在解决多个线程同步方面相对于调用线程的join方法已经有了不少优化,但是CountDownLatch的计数器是一次性的,也就是等到计数器值变为0后,再调用CountDownIatch的await和countdown方法都会立刻返回,这就起不到线程同步的效果了。所以为了满足计数器可以重置的需要,JDK开发组提供了CyclicBarrier类,并且CyclicBarrier类的功能并不限于CountDownLatch的功能。从字面意思理解,CyclicBarrier是回环屏障的意思,它可以让一组线程全部达到一个状态后再全部同时执行。这里之所以叫作回环是因为当所有等待线程执行完毕,并重置CyclicBarrier的状态后它可以被重用。之所以叫作屏障是因为线程调用await方法后就会被阻塞,这个阻塞点就称为屏障点,等所有线程都调用了await 方法后,线程们就会冲破屏障,继续向下运行。

1.线程同步

java 复制代码
package cn.tx.sycbarri;

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CycleBarrierTest1 {

    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread()+"task1 merge result");
        }
    });

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(2);
        service.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread()+"task1-1");
                    System.out.println(Thread.currentThread()+"enter in barrier");
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread()+"enter out barrier");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        service.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread()+"task1-2");
                    System.out.println(Thread.currentThread()+"enter in barrier");
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread()+"enter out barrier");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        service.shutdown();
    }
}

如上代码创建了一个CyclicBarrier对象,其第一 个参数为计数器初始值,第二个参数Runable是当计数器值为0时需要执行的任务。

在main函数里面首先创建了一个大小为2的线程池,然后添加2个子任务到线程池,每个子任务在执行完自己的逻辑后会调用await方法。

一开始计数器值为2,当第一个线程调用await方法时,计数器值会递减为1。由于此时计数器值不为0,所以当前线程就到了屏障点而被阻塞。然后第二个线程调用await时,会进入屏障,计数器值也会递减,现在计数器值为0,这时就会去执行CyclicBarrier构造函数中的任务,执行完毕后退出屏障点,并且唤醒被阻塞的第二个线程,这时候第一个线程也会退出屏障点继续向下运行。上面的例子说明了多个线程之间是相互等待的,假如计数器值为N,那么随后调用await方法的N-1个线程都会因为到达屏障点而被阻塞,当第N个线程调用await后,计数器值为0了,这时候第N个线程才会发出通知唤醒前面的N-1个线程。也就是当全部线程都到达屏障点时才能一块继续向 下执行。对于这个例子来说,使用CountDownLatch也可以得到类似的输出结果。

2.可复用性

java 复制代码
package cn.tx.sycbarri;

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CycleBarrierTest2 {

    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(2);
        service.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread()+"step-1");
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread()+"step-2");
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread()+"step-3");

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        service.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread()+"step-1");
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread()+"step-2");
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread()+"step-3");

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        service.shutdown();
    }
}

在如上代码中,每个子线程在执行完阶段 1 后都调用了 await 方法,等到所有线程都 到达屏障点后才会 - 块往下执行,这就保证了所有线程都完成了阶段 1 后才会开始执行阶 2 。然后在阶段 2 后面调用了 await 方法,这保证了所有线程都完成了阶段 2 后,才能 开始阶段 3 的执行。这个功能使用单个 CountDownLatch 是无法完成的。

相关推荐
星火开发设计2 小时前
命名空间 namespace:解决命名冲突的利器
c语言·开发语言·c++·学习·算法·知识
小北方城市网2 小时前
RabbitMQ 生产级实战:可靠性投递、高并发优化与问题排查
开发语言·分布式·python·缓存·性能优化·rabbitmq·ruby
爱学习的阿磊2 小时前
C++中的策略模式应用
开发语言·c++·算法
nbsaas-boot2 小时前
如何进行 Vibe Coding:从“灵感驱动”到“可交付工程”的方法论
java·ai编程
郝学胜-神的一滴2 小时前
Python中的bisect模块:优雅处理有序序列的艺术
开发语言·数据结构·python·程序人生·算法
Remember_9932 小时前
Spring 事务深度解析:实现方式、隔离级别与传播机制全攻略
java·开发语言·数据库·后端·spring·leetcode·oracle
看得见的风2 小时前
Claude Code + CCR配置(含OpenRouter、GLM、Kimi Coding Plan)
开发语言
L_09072 小时前
【Linux】进程状态
linux·开发语言·c++
roman_日积跬步-终至千里2 小时前
【Java并发】用 JMM 与 Happens-Before 解决多线程可见性与有序性问题
java·开发语言·spring