多线程编程原理分析之-Synchronized\ReentranLock实现

181 阅读15分钟

高性能编程面试点

1、java程序运行原理分析

2、java线程状态

2.1 线程的6种状态

1627571163631.png

1627571420693.png

2.2 线程中止/复位-interrupt/使用自定义标识符

告诉线程可以进行中断,是否中断取决于线程本身。保证线程安全性,处于阻塞状态则通过抛异常。。来提示调用者信息,线程会复位,依旧执行继续的逻辑。

使用自定义标识符进行线程中止。

两种复位重置:我有事情没干完呢,不能就这么停了

  • 可以通过Therad.interuupted(); 重置

  • InterruptedException来进行重置

2.3 CPU的多级缓存和缓存同步协议

**缓存的同步协议:**volatile的底层实现,通过对缓存标识位的修改实现内存的可见性。 1627573316366.png CPU指令重排序:(可以使用volatile、fianllly 禁止指令重排序) 1627573486259.png **内存屏障:**java 编译器在生成指令序列时会插入内存屏障来实现禁止指令的重排序 1627573741171.png

2.4 为什么阻塞方法都会抛出一个异常?

阻塞的方法的结束都需要事件的触发进行结束。阻塞的未能结束正常告知调用者,这个事情怎么做(强制结束、继续运行)提供一个入口。

wait\sleep\join 阻塞方法,正常结束都需要一些条件的执行

notify\时间结束\notify

3、Synchronized关键词

3.1 锁有作用范围那么锁存贮在哪里呢?

锁存贮在java的对象中

3.2 java对象的组成结构
  • 锁的状态:区分当前是那种锁

image-20210801183855455

  • java 对象头的mark word
  • 锁升级:无锁-->{编向锁}--》轻量级锁--》重量级锁

image-20210801184011767

image-20210801193817679

3.3 Synchroinzed锁的优化
  • 控制Synchroinzed 的锁的粒度

  • 无锁化

  • **偏向锁:**CAS --compare and swaper (value ,except,update);

    ​ value --主内存中的值,except --当前内存值 ,value==except ,更新update数据

  • **轻量级锁:**偏向锁升级成轻量级锁时候会清除锁的头信息,使用CAS 进行比较和加锁。cas:自旋。

  • **重量级锁:**自旋会耗费CPU资源,n 多次自旋后还没获得锁资源,锁升级成重量级。锁膨胀----》阻塞线程,挂起。

    • 自适应,jdk1.6 以后。。根据上一个自旋锁进行动态判断

    • 设置锁的自旋次数

    • 监视器对象的实现,monitor

      image-20210801211519117

3.4 锁升级的过程

工作中可将,编向锁关闭进行优化,自己写的多线程最少用的轻量级锁,a\b 线程交换获取的锁资源。可在jvm 中进行关闭。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SNzeZq3f-1628501370047)(pic.wangt.cc/download/pi…)]

4、wait、notify/notifyall ---线程的通信

image-20210801230542385

线程A :进行wait

线程B :唤醒线程A

4.1 为什么使用wait和notify 需要进行加锁?

线程间需要进行通信和线程间同步,通信是指线程间以何种机制交换信息,同步是指:程序中不同线程间操作顺序发生的相对次序。。。线程间通常用两种进行通信的方法:共享内存和消息传递。

java 中的通信采用共享内存模式,通过写-读公共的状态进行隐式通信,显示同步(程序员必须指明那些代码是在线程间需要互斥进行的)。

消息传递并发模式中:线程间没有中间状态,必须通过发消息进行显示通信。隐式同步

5、JMM内存模型

5.1 可见性的根本原因:高速缓存、重排序
5.2 happens-before原则
  • 单线程的线程顺序
  • voliate 关键词(内存屏障:强制建数据从分片cpu的高速缓存刷新的主内存中)
  • 传递性规则(a before b, b before c ,则a before c)
  • 线程start规则(start 前的before start )
  • 线程join规则(本质是wait和notfiy)
  • synchroized 监视器规则(前一解锁的数据before后面加锁的)

这些工作场景中不需要考虑可见性问题。

5.3 缓存一致性协议

MESI 表示缓存行的四种状态,分别是

  1. M(Modify) 表示共享数据只缓存在当前 CPU 缓存中,并且是被修改状态,也就是缓存的数据和主内存中的数据不一致
  2. E(Exclusive) 表示缓存的独占状态,数据只缓存在当前CPU 缓存中,并且没有被修改
  3. S(Shared) 表示数据可能被多个 CPU 缓存,并且各个缓存中的数据和主内存数据一致
  4. I(Invalid) 表示缓存已经失效。在 MESI 协议中,每个缓存的缓存控制器不仅知道自己的读写操作,而且也监听(snoop)其它 Cache 的读写操作

对于 MESI 协议,从 CPU 读写角度来说会遵循以下原则: CPU 读请求:缓存处于 M、E、S 状态都可以被读取,I 状态 CPU 只能从主存中读取数据 CPU 写请求:缓存处于 M、E 状态才可以被写。对于 S 状态的写,需要将其他 CPU 中缓存行置为无效才可写 使用总线锁和缓存锁机制之后,CPU 对于内存的操作大概可以抽象成下面这样的结构。从而达到缓存一致性效果

image-20210807000418826

6、Lock

6.1 ReentranLock (重入互斥锁)类图

image-20210807082811461

image-20210807082937351

7、 AQS 同步队列

7.1 同步工具实现的功能
  • 独占--》互斥

  • 共享--》读写锁

7.2 AQS 的基本实现:一个双向链表

锁的基本要素:

  • 一个共享数据记录锁的状态(无锁、有锁)--state (0-无锁状态,>0 有锁状态(可重入行值+1))
  • node 表示未获得到锁而被封装成的线程信息,线程获得锁后node被当前链表移除

image-20210807100827123

7.3 ReentrantLock 上锁过程分析
NonfairSync 上锁代码    State 当前资源的锁状态
final void lock() {
            if (compareAndSetState(0, 1))//使用乐观锁逻辑进行上锁操作,0 预期值,1 成功时要设置的值。
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
}
//aqs 中acquire 非公平锁tryAcquire 实际调用方法
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();//当前锁的状态,无锁为0
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//线程时同一个线程
                int nextc = c + acquires;//线程state 值添加 
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

//aqs 中对当前没事获得到锁的线程封装成node节点添加到链表中
//封装node
    static final Node SHARED = new Node();//共享锁    
    static final Node EXCLUSIVE = null;//独占锁(aqs上个acquire()使用独占)
 private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {//链表存在,添加到链表中
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);//自锁
        return node;
    }
//构建链表
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // 初始化链表
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
// 线程B 添加到链表后,你在试试抢锁,要不就停了吧(阻塞)。
//再试试抢锁的
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();//获得当前节点(封装线程信息)上一个节点信息
                if (p == head && tryAcquire(arg)) {//在尝试获取
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;(falsetrue)for循环的唯一出口
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())//将线程阻塞到这里
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

image-20210807170235809

image-20210807112606942

signal=-1 (表示下一个线程挂起状态),线程B 加入试试失败后将上一个状态改成-1,线程C 加入将B改成-1.

线程A 进行unLock()操作唤醒队列中的线程B(非公平是指其他新添加的线程可能进行插队,而不是队列中的值进行插队)

image-20210807225935292

FairSync 上锁代码 
 final void lock() {
      acquire(1);
 }
//公平锁上锁
protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //hasQueuedPredecessors()对列中有线程挂起,不进行插队
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
7.4 ReentrantLock 解锁过程分析

ReentrantLock.unlock 中,调用release()释放锁

public final boolean release(int arg) {
if (tryRelease(arg)) { //释放锁成功
		Node h = head; //得到 aqs 中 head 节点
	if (h != null && h.waitStatus != 0)//如果 head 节点不为空并且状态不为0
		unparkSuccessor(h);//唤醒后续节点
		return true;
	  }
	return false;
}

ReentrantLock .tryRelease() 这个方法可以认为是一个设置锁状态的操作,通过将 state 状态减掉传入的参数值(参数是 1),如果结果状态为 0,就将排它锁的 Owner 设置为 null,以使得其它的线程有机会进行执行。 在排它锁中,加锁的时候状态会增加 1(当然可以自己修改这个值),在解锁的时候减掉 1,同一个锁,在可以重入后,可能会被叠加为 2、3、4 这些值,只有 unlock()的次数与 lock()的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下才会返回 true。

protected final boolean tryRelease(int releases)
{
    int c = getState() - releases;//可重入性值--
    if (Thread.currentThread() !=
        getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

unparkSuccessor //唤醒下一个线程

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;//获得 head 节点的状态
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);// 设置 head 节点
    状态为 0
        Node s = node.next;//得到 head 节点的下一个节点
    if (s == null || s.waitStatus > 0) {
        //如果下一个节点为 null 或者 status>0 表示 cancelled 状态.
        //通过从尾部节点开始扫描,找到距离 head 最近的一个
        waitStatus<=0 的节点
            s = null;
        for (Node t = tail; t != null && t != node; t =
             t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null) //next 节点不为空,直接唤醒这个线程即可
        LockSupport.unpark(s.thread);
}
7.5 公平锁和非公平的锁的区别:

非公平锁上来就进行比较,插队。公平锁上来先看队列中有没有线程挂起,有挂起直接排到队列后面。

8、Condition ---await/sing/singall

8.1 condition 流程图

await:把当前的线程阻塞挂起

signal:唤醒阻塞的线程

image-20210808181958499

8.2 condition await()阻塞方法源码分析

调用Condition的await()方法(或者以await开头的方法),会使当前线程进入**Condition 的等待队列(单向链表)**并释放锁,同时线程状态变为等待状态。当从 await()方法返回时,当前线程一定获取了Condition 相关联的锁

public final void await() throws InterruptedException {
    if (Thread.interrupted()) //表示 await 允许被中断
        throw new InterruptedException();
    Node node = addConditionWaiter(); //创建一个新的节点,节点状态为 condition,采用的数据结构仍然是链表
        int savedState = fullyRelease(node); //释放当前的锁,得到锁的状态,并唤醒 AQS 队列中的一个线程
        int interruptMode = 0;
    //如果当前节点没有在同步队列上,即还没有被 signal,则将当前线程阻塞
        while (!isOnSyncQueue(node)) {//判断这个节点是否在 AQS 队列上,第一次判断的是 false,因为前面已经释放锁了
                LockSupport.park(this); //通过 park 挂起当前线程
                if ((interruptMode =
                     checkInterruptWhileWaiting(node)) != 0)
                    break;
        }
    // 当这个线程醒来,会尝试拿锁, 当 acquireQueued 返回 false 就是拿到锁了.
        // interruptMode != THROW_IE -> 表示这个线程  没有成功将 node 入队,但 signal 执行了 enq 方法让其入队了.
        // 将这个变量设置成 REINTERRUPT.
        if (acquireQueued(node, savedState) &&
            interruptMode != THROW_IE)
            interruptMode = REINTERRUPT;
    // 如果 node 的下一个等待者不是 null, 则进行清理,清理 Condition 队列上的节点.
        // 如果是 null ,就没有什么好清理的了.
        if (node.nextWaiter != null) // clean up if
            cancelled
            unlinkCancelledWaiters();
    // 如果线程被中断了,需要抛出异常.或者什么都不做
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

构建Condition 的单向链表---addConditionWaiter

将当前线程封装成node,添加到等待队列中,从而形成一个单向链表

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // 如 果 lastWaiter 不 等 于 空 并 且waitStatus 不等于 CONDITION 时,把冲好这个节点从链表中移除
        if (t != null && t.waitStatus !=
            Node.CONDITION) {
            unlinkCancelledWaiters();
            t = lastWaiter;
        }
    //构建一个Node,waitStatus=CONDITION。 这里的链表是一个单向的,所以相比 AQS 来说会简单很多
        Node node = new
        Node(Thread.currentThread(),
             Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

image-20210808234917827

释放当前调用await()方法线程的锁--fullyRelease

fullyRelease :彻底释放锁,不管当前线程具有重入了多少次锁,一次性释放掉所有的锁。此时,同步队列会触发锁的释放和重新竞争。ThreadB 获得了锁

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState();
        //获得重入的次数
        if (release(savedState)) {//释放锁并且唤醒下一个同步队列中的线程
                failed = false;
            return savedState;
        } else {
            throw new
                IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus =
            Node.CANCELLED;
    }
}

判断当前线程是否在AQS 队列中--isOnSyncQueue

如果不在 AQS 同步队列,说明当前节点没有唤醒去争抢同步锁,所以需要把当前线程阻塞起来,直到其他的线程调用 signal 唤醒如果在 AQS 同步队列,意味着它需要去竞争同步锁去获得执行程序执行权限

  • 为什么要做这个判断呢?

    原因是在 condition 队列中的节点会重新加入到 AQS 队列去竞争锁。也就是当调用 signal的时候,会把当前节点从 condition 队列转移到 AQS 队列

  • 如何去判断ThreadA 这个节点是否存在于 AQS 队列中呢?

    • 1、**在Codition 的同步队列中。**如果 ThreadA 的 waitStatus 的状态为 CONDITION,说明它存在于 condition 队列中,不在 AQS 队列。因为AQS 队列的状态一定不可能有 CONDITION
    • 2、**当前线程时获得锁的线程。**如果 node.prev 为空,说明也不存在于 AQS 队列,原因是 prev=null 在 AQS 队列中只有一种可能性,就是它是head 节点,head 节点意味着它是获得锁的节点。
    • 3、AQS 的特性。如果 node.next 不等于空,说明一定存在于 AQS 队列中,因为只有 AQS 队列才会存在 next 和 prev 的关系
    • 4、**遍历AQS 队列查找判断node信息是否相等。**findNodeFromTail,表示从 tail 节点往前扫描 AQS 队列,一旦发现 AQS 队列的节点和当前节点相等,说明节点一定存在于 AQS 队列中
final boolean isOnSyncQueue(Node node) {
    if (node.waitStatus ==
        Node.CONDITION || node.prev == null)
        return false;
    if (node.next != null) // If has
        successor, it must be on queue
        return true;
    return findNodeFromTail(node);
}
8.3 Condition.signal 唤醒方法

signal()唤醒等待队列中的节点

public final void signal() {
    if (!isHeldExclusively()) //先判断当前线程是否获得了锁,这个判断比较简,直接用获得锁的线程和当前线程相比即可
        throw new  IllegalMonitorStateException();
    Node first = firstWaiter; // 拿到 Condition队列上第一个节点
        if (first != null)
            doSignal(first);//唤醒队列中的第一节点
}

doSignal() :将node节点从Condition的等待队列中移动AQS 队列中

对 condition 队列中从首部开始的第一个 condition 状态的节点,执行 transferForSignal 操作,将 node 从 condition队列中转换到 AQS 队列中,同时修改 AQS 队列中原先尾节点的状态

private void doSignal(Node first) {
    do {
        //从 Condition 队列中删除 first 节点
        if ( (firstWaiter = first.nextWaiter)  == null)
            lastWaiter = null; // 将 next 节点设置成 null
            first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

AQS.transferForSignal():修改node节点状态,添加到AQS队列中。唤醒该节点

final boolean transferForSignal(Node node)
{
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))//更新节点的状态为 0,如果更新失  败,只有一种可能就是节点被 CANCELLED 了
        return false;
    Node p = enq(node);//调用 enq,把当前节点添加到 AQS 队列。并且返回返回按当前节点的上一个节点,也就是原 tail 节点
    int ws = p.waitStatus;// 如果上一个节点的状态被取消了, 或者尝试设置上一 个节点的状态为 SIGNAL 失败了(SIGNAL 表示: 他的 next节点需要停止阻塞),
    if (ws > 0|| !compareAndSetWaitStatus(p, ws,Node.SIGNAL))
        LockSupport.unpark(node.thread); // 唤醒节点上的线程.
    return true; //如果 node 的 prev 节点已经是signal 状态,那么被阻塞的 ThreadA 的唤醒工作由 AQS 队列来完成
}
8.4 被阻塞后队列的唤醒操作

​ 前面在分析 await 方法时,线程会被阻塞。而通过 signal被唤醒之后又继续回到上次执行的逻辑中标注为红色部分的代码。checkInterruptWhileWaiting 这个方法是干嘛呢?其实从名字就可以看出来,就是 ThreadA 在 condition 队列被阻塞的过程中,有没有被其他线程触发过中断请求

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new        InterruptedException();
    Node node = addConditionWaiter();
    int savedState =  fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode =
             checkInterruptWhileWaiting(node)) !
            = 0)//阻塞,被唤醒后继续执行代码位置
            break;
    }
    if (acquireQueued(node,
                      savedState) && interruptMode !=
        THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) //clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptM
                                 ode);
}

checkInterruptWhileWaiting

如果当前线程被中断,则调用transferAfterCancelledWait 方法判断后续的处理应该是 抛出 InterruptedException 还是重新中断

private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ?
         THROW_IE : REINTERRUPT) :0;
}
final boolean transferAfterCancelledWait(Node node) {
    //使用 cas 修改节点状态,如果还能修改成功, 说明线程被中断时, signal 还没有被调 用。
        // 这里有一个知识点,就是线程被唤醒,并不一定是在 java 层面执行了  locksupport.unpark,也可能是调用了线程的interrupt()方法,这个方法会更新一个中 断标识,并且会唤醒处于阻塞状态下的线程。
        if
            (compareAndSetWaitStatus(node,
                                     Node.CONDITION, 0)) {
           	    enq(node); //如果cas成功,则把 node 添加到AQS 队列
                return true;
        }
    //如果 cas 失败,则判断当前 node 是否已经在 AQS 队列上,如果不在,则让给其他线程 执行
        //当 node 被触发了 signal 方法时, node 就 会被加到 aqs 队列上
        while (!isOnSyncQueue(node))//循 环检测 node 是否已经成功添加到 AQS 队列中。如果没有,则通过 yield,
            Thread.yield();
    return false;
}

acquireQueued :当前被唤醒的线程去重新抢占锁,并且恢复原本的重入次数状态

reportInterruptAfterWait :根据checkInterruptWhileWaiting 返回的中断标识来判断线程的一个处理逻辑,是重新响应中断还是抛出中断异常。

8.5 整体流程图

image-20210809161132864

阻塞: await()方法中,在线程释放锁资源之后,如果节点不在 AQS 等待队列,则阻塞当前线程,如果在等待队列,则自旋等待尝试获取锁 释放: signal()后,节点会从 condition 队列移动到 AQS等待队列,则进入正常锁的获取流程

9、CountDownLatch ---倒计数器

9.1 CountDownLatch 使用到AQS 的共享锁

倒计数器:允许一个或多个线程一直等待,直到其他的线程操作执行完毕后再执行。

image-20210809161703083

9.2 CountDownLatch 倒计数器--countDown()/await()

countDown() 方法每次调用都会将 state 减 1,直到state 的值为 0;而 await 是一个阻塞方法,当 state 减为 0 的时候, await 方法才会返回。 await 可以被多个线程调用,大家在这个时候脑子里要有个图:所有调用了await 方法的线程阻塞在 AQS 的阻塞队列中,等待条件满足(state == 0),将线程从队列中一个个唤醒过来。

9.3 实现原理分析

1、await 方法

  • 使用 Sync 继承AQS 重写共享锁方法
  • 判断当前线程是否需要添加到的共享锁队列中
  • (构建队列)添加线程节点(共享模式的节点)到队列中
  • 自旋-判断当前节点是否获取到锁,未获取锁--阻塞线程
  • image-20210809162353085

2、countDown

  • 只有当 state 减为 0 的时候,tryReleaseShared 才返回 true, 否则只是简单的 state = state - 1
  • 如果state=0, 则调用doReleaseShared 唤醒处于await状态下的线程
  • 将标识为共享状态的锁(PRPAGATE)的节点进行唤醒传播
  • image-20210809163254208
9.4 共享锁的释放和独占锁释的不同

​ 共享锁的释放和独占锁的释放有一定的差别 前面唤醒锁的逻辑和独占锁是一样,先判断头结点是不是SIGNAL状态,如果是,则修改为0,并且唤醒头结点的下一个节点 PROPAGATE: 标识为PROPAGATE状态的节点,是共享锁模式下的节点状态,处于这个状态下的节点,会对线程的唤醒进行传播

10、Semaphore --限流

**作用:**控制同时访问的线程个数,通过 acquire 获取一个许可,如果没有就等待,通过 release 释放一个许可。有点类似限流的作用。

**实现原理:**从 Semaphore 的功能来看,我们基本能猜测到它的底层实现一定是基于 AQS 的共享所,因为需要实现多个线程共享一个领排池 。创建 Semaphore 实例的时候,需要一个参数 permits,这个基本上可以确定是设置给 AQS 的 state 的,然后每 个线程调用 acquire 的时候,执行 state = state - 1, release 的时候执行 state = state + 1,当然, acquire 的时候,如果 state = 0,说明没有资源了,需要等待其他线程 release。 Semaphore 分公平策略和非公平策略 :区别公平先判断是否有线程正在排队,然后进行CAS 减操作。

11、CyclicBarrier --循环栅栏

​ CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续工作。 ​ CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 当前线程已经到达了屏障,然后当前线程被阻塞

**使用场景:**当存在需要所有子任务都完成时,才能执行主任务,可多次重入,倒计数器只能使用一次。

使用注意事项:

)对于指定计数值 parties,若由于某种原因,没有足够的线程调用 CyclicBarrier 的 await,则所有调用 await 的线程都会被阻塞; 2)同样的 CyclicBarrier 也可以调用 await(timeout, unit),设置超时时间,在设定时间内,如果没有足够线程到达,则解除阻塞状态,继续工作; 3)通过 reset 重置计数,会使得进入 await 的线程出现BrokenBarrierException; 4 ) 如 果 采 用 是 CyclicBarrier(int parties, RunnablebarrierAction) 构造方法,执行 barrierAction 操作的是最后一个到达的线程

实现原理:

CyclicBarrier 相比 CountDownLatch 来说,要简单很多,源码实现是基于 ReentrantLock 和 Condition 的组合使用。看如下示意图, CyclicBarrier 和 CountDownLatch 是不是很像,只是 CyclicBarrier 可以有不止一个栅栏,因为 它的栅栏(Barrier)可以重复使用(Cyclic)

image-20210809172424328