Java并发包下常用工具类源码深度解析

一、ReentrantLock
在实际开发过程中,有时候会碰到多个线程同时修改共享变量的情况。这时候,就需要对共享变量做”保护”,防止产生线程不安全的情况,JDK的提供了相应的工具类,在java.util.concurrent包下提供了很多工具类,今天的主角之一,ReentrantLock 就可以实现对共享变量的保护,除此之外,通过synchronized关键字,也能实现该功能。现在,以ReentrantLock为例,来讲解其原理。代码如下:
在这里插入图片描述
创建ReentrantLock对象,调用ReentrantLock#lock()方法加锁,调用ReentrantLock#unlock()方法解锁,并且unlock()方法必须放在finally代码块中,防止程序在执行的过程中出异常,导致没有释放锁。这是简单的使用,具体原理,继续往下看。
首先,需要创建ReentrantLock对象,这是毋庸置疑的,看看ReentrantLock的又构造中做了啥,如下图所示:
在这里插入图片描述
会发现它有两个构造方法,一个是无参构造,一个是有参构造,需要传一个Boolean类型的值。从这里就可以看出,无参构造创建的是NonfairSync对象,即非公平锁,有参构造,传true,创建FairSync对象,即公平锁,false创建NonfairSync。因此总结一下,真正实现加锁/解锁功能的,实际上就是ReentrantLock的sync属性,而锁是公平锁还是非公平锁,则依赖创建的是哪个类。不管是NonfairSync还是FairSync,都有一个共同的父类,即Sync,而Sync的父类则是鼎鼎有名的AbstractQueuedSynchronizer,简称AQS,如下图所示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
以NonfairSync为例,看看核心的加锁逻辑,即ReentrantLock#lock()方法,如下图所示:
在这里插入图片描述
实际上就是调用的NonfairSync#lock()方法,如下图所示:
在这里插入图片描述
可以看出,在加锁的时候,首先通过CAS尝试设置AbstractQueuedSynchronizer的属性 state,该属性是被volatile关键字修饰的,将其由0改为1,如果修改成功,则说明加锁成功,并将AbstractQueuedSynchronizer的属性exclusiveOwnerThread设置为的当前线程,否则走else的逻辑,即调用即它的父类AbstractQueuedSynchronizer#acquire()方法,传入1,这个acquire()方法是一个公共方法,AbstractQueuedSynchronizer的其他实现类也会调用,因此将此单独抽取出来。看看该方法,如下图所示:
在这里插入图片描述
走if的逻辑,调用AbstractQueuedSynchronizer#tryAcquire()方法,这是一个抽象方法,是由其实现类实现的,进入该方法,如下图所示:
在这里插入图片描述
在这个方法中,就可以体现公平锁和非公平锁的实现,先看非公平锁:
在这里插入图片描述
在这里插入图片描述
可以看出:首先拿到state属性的值,判断是否为0,如果为0,则尝试CAS设置为传入的acquires,即1,CAS成功则将exclusiveOwnerThread属性设置为当前线程,返回true,加锁成功;如果state部位0,说明有线程已经加锁了,拿到exclusiveOwnerThread属性,判断其是否是当前线程,如果是,则state+1,返回true,表示加锁成功。这里就是可重入锁的逻辑,说明ReentrantLock是可重入锁;如果不满足以上两种条件,则加锁失败,返回false。
这就是非公平锁的加锁逻辑,再看看公平锁的加锁逻辑:如下图所示:
在这里插入图片描述
可以看出,跟非公平锁关键性的不同就是,公平锁调用了AbstractQueuedSynchronizer#hasQueuedPredecessors()方法,返回true表示队列中已经有阻塞的线程了,所以公平锁加锁的时候会先看阻塞队列是否有阻塞的线程,没有则尝试加锁,有则判断exclusiveOwnerThread属性是否是当前线程,是则state+1,不是则直接返回false。
回到调用AbstractQueuedSynchronizer#tryAcquire()方法的if逻辑中,由于加锁失败,tryAcquire()方法返回false,则会调用AbstractQueuedSynchronizer#acquireQueued()方法,在此之前会先调用AbstractQueuedSynchronizer#addWaiter()方法,传入一个参数,即Node.EXCLUSIVE。看看该方法,如下图所示:
在这里插入图片描述
可以看出,在该方法中,先创建了一个Node属性,传入当前线程和Node.EXCLUSIVE。Node类中有一个thread属性,用于存储线程,而Node.EXCLUSIVE表示这个节点是独占锁。继续往下,先解释一下:head和tail都是AbstractQueuedSynchronizer属性,是被volatile修饰的Node类,所谓的阻塞队列,实际上就是一个双向链表,head是阻塞队列的头部,tail,是阻塞队列的尾部,节点之间用prev和next连接起来的。拿到tail,判断tail是否为空,如果不为空,表示当前阻塞队列是有值的,则让前面创建的Node对象的prev指向tail,然后通过CAS将这个Node对象设置为tail,如果设置成功,则让原来的tail节点的next指向Node对象,完成入队操作。如果tail为空在,则进入AbstractQueuedSynchronizer#enq()方法,看看该方法,如下图所示:
在这里插入图片描述
在for循环中,还是先拿到tail,判断tail是否为空,如果为空的话,那此时阻塞队列都还没有,因此需要构建阻塞队列,首先创建一个空的Node对象,CAS设置这个Node对象为head,设置成功,就将Node对象也赋值给tail,这时候head=tail=创建的Node对象。由于是for死循环,会在再次循环,由于tail已经赋值过了,所以不为空,因此走else的逻辑,这时先将传入的Node对象那个的prev指向tail,然后再将传入的Node对象通过CAS设置给tail,设置成功,则原来的tail的next执行传入放入Node对象,就完成了入队操作,并返回原来的tail属性。回到AbstractQueuedSynchronizer#addWaiter()方法,最终返回Node
对象(存放有当前线程)。再看AbstractQueuedSynchronizer#acquireQueued()方法,入参就是Node对象和1,看看该方法,如下图所示:
在这里插入图片描述
最外层也是for死循环,首先是拿到传入的节点的前一个节点p,判单p是否是头节点,如果是,则调用AbstractQueuedSynchronizer#tryAcquire()方法,前面讲解过了,这里就不再赘述,假设加锁成功就跳出了循环,如果加锁失败,调用AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire()方法,看看该方法:
在这里插入图片描述
有两个入参:当前节点的前一个结点pred,以及当前节点node,拿到前一个节点waitStatus属性,判断该属性值是否为Node.SIGNAL,即-1,随便看看Node的几种状态:
在这里插入图片描述
如果是Node.SIGNAL,表示该节点的下一个节点可被唤醒。由于创建Node的时候,没有给waitStatus赋值,而waitStatus是int类型,因此waitStatus为0,不等于Node.SIGNAL。因此不会进入if中,在判断waitStatus是否大于0 ,如果是,则通过do/while循环,从这个节点开始向前遍历,去除掉所有waitStatus大于0的节点,为什么要这么做呢,waitStatus大于0表示,Node.CANCELLED,所以要被移除。如果 waitStatus不大于0 ,则走else逻辑,通过CAS将pred节点的waitStatus设置为-1.。这时候可以想象一下:head节点的thread属性为空,但是waitStatus为-1,它的下一个节点,假设叫Node1,就是前面创建的节点,但是此时它的waitStatus为0,thread属性不为空,是当前线程,如下图所示:
在这里插入图片描述
程序跳出AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire()方法,并返回false,再次回到AbstractQueuedSynchronizer#acquireQueued()方法,由于是for死循环,会再次尝试上面的逻辑,如果加锁失败,再次进入AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire()方法,由于当前节点的前一个结点已经设置过waitStatus为-1了,因此,直接返回true,由于AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire()方法true,程序会进入AbstractQueuedSynchronizer#parkAndCheckInterrupt ()方法,看看这个方法,如下图所示:
在这里插入图片描述
在这里,线程被阻塞。以上就是加锁逻辑,如果后续该线程被唤醒,会从这里开始继续执行。
再看看解锁逻辑,即ReentrantLock#unlock()方法,如下图所示:
在这里插入图片描述
在这里插入图片描述
看看AbstractQueuedSynchronizer#tryRelease ()方法,如下图所示:
在这里插入图片描述
传入1,拿到state,减去1,并赋值给c,校验:当前线程不等于exclusiveOwnerThread则抛异常。判断c是否为0为0表示解锁成功,将exclusiveOwnerThread设置为null并返回true。否则返回false,并将c重新设置给state(如果该线程多次加锁,则c不为0)。
再回到AbstractQueuedSynchronizer@reaease()方法,如果tryRelease ()方法返回true,则进入if代码块中:拿到head,head不能为空,且head的waitStatus属性不为0(其实为-1,前面设置过了)。进入AbstractQueuedSynchronizer#unparkSuccessor ()方法,传入head,如下图所示:
在这里插入图片描述
拿到head节点的waitStatus,通过CAS将其设置为0。拿到head节点的下一个节点,如果下一个节点不为null,则取出节点的thread,调用 LockSupport.unpark(),传入thread,线程被唤醒。就像我前面说的,线程被唤醒,会从AbstractQueuedSynchronizer#parkAndCheckInterrupt ()方法的LockSupport.park(this)这里开始执行。最终在进到这里。如下图所示:
在这里插入图片描述
如果这次调用AbstractQueuedSynchronizer#tryAcquire()方法获取锁成功,则进入if逻辑:调用AbstractQueuedSynchronizer#setHead()方法,传入这个Node,看看这个方法:
在这里插入图片描述
干了三件事:将传入的节点设置为新的head节点;将该节点的thread设置为null;将该节点的prev设置为null,也就是该节点不会再指向原来的head节点了。回到AbstractQueuedSynchronizer#tryAcquire()方法,原来的head节点的next设置为null,这时,就没有任何引用指向原来的head节点了,该系欸但会被GC当作垃圾回收掉,最后返回false,跳出AbstractQueuedSynchronizer#tryAcquire()方法,再回到AbstractQueuedSynchronizer#acquire()方法,如下图所示:
在这里插入图片描述
由于tryAcquire()方法返回false,不会进入if代码块,因此线程也不会被中断掉,最后就开始执行我们编写业务逻辑,业务执行完,再调用ReentrantLock#unloack()方法,唤醒下一个被阻塞的线程。以上就是从ReentrantLock的源码级别解析加锁解锁逻辑了。Synchronized关键字加锁解锁逻辑实际上和ReentrantLock差不多,由于JDK对Synchronized做了很多优化,现在Synchronized的效率跟ReentrantLock差不太多,所以选用ReentrantLock或者Synchronized都可以,而且我看某些框架源码,如Nacos,也有用了到Synchronized。
补充几点:
① 众所周知,阻塞、唤醒线程,都是 用户态和内核态的相互转换,因此比较耗性能,在 看ReentrantLock源码,的时候,他也不上上来就加锁,在调用LockSupport.park()方法之前其实尝试获取了几次锁,都是为了避免线程被阻塞,到最到没办到了,才会阻塞线程,我数了下,一共四次,尝试获取锁。大家也可以数数看。
② ReentrantLock为什么默认使用非公平锁?因为非公平锁相比于公平锁,效率略高,因为非公平锁不管阻塞队列有没有阻塞的线程,上来就尝试加一次锁,这是有小概率机会加锁成功的,如果加锁成功,那就可以避免一次线程被阻塞!

二、Semaphore
Semaphore这个工具类,就有点像为微服务中的sentinel,也可以起到类似限流的作用,只不过它是JVM级别的限流,不能跨JVM,如果是对单体应用中某个接口限流,Semaphore未尝不是一个好的选择,话不多说,直入主题。先看一段代码,如下图所示:
在这里插入图片描述
首先是调用有参构造,创建Semaphore对象,传入一个具体的数字,比如5,表示这个资源的数量,实际上就是设置state为5。ReentrantLock被称为独占锁,Semaphore则被称之为共享锁,如果调用他的有参构造,传入的是1,他也能变成独占锁。先看看它的构造,类似ReentrantLock,也可以指定是公平锁还是非公平锁,默认是非公平锁,如下图所示:
在这里插入图片描述
然后调用Semaphore#acquire()方法,传入一个具体的数字,表示要所取得资源的数量,如果获取失败,线程会被阻塞,传入的数字在[1,5}区间,一般是1假设传入的是2,则相应的调用Semaphore#release()方法,传入的也应是2。以上是简单的使用,具体原理看代码,主要看Semaphore#acquire()方法,如下图所示:
在这里插入图片描述
接着看Semaphore#acquireSharedInterruptibly()方法,入参是1,如下图所示:
在这里插入图片描述
再看看Semaphore#tryAcquireShared()方法,如下图所示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里,首先拿到state的值,就是5,然后是for的死循环,在循环中,减去传入的1,得到的remaining为4,两个条件,跳出循环:
① remaining小于0;
② CAS设置state为4成功(因为存在多线程的竞争,必须使用CAS+for死循环,保证最终CAS能成功);
如果是①这种情况,没有共享资源可供获取了,这种情况加锁失败,我们主要讨论的也是情况①,②则表示加锁成功。回到Semaphore#acquireSharedInterruptibly()方法中,由于Semaphore#tryAcquireShared()方法,得到的结果小于1,则进入Semaphore#doAcquireSharedInterruptibly()方法,如下图所示 :
在这里插入图片描述
首先还是调用AbstractQueuedSynchronizer#addWaiter()方法,传入Node.SHARED,和ReentrantLock一样,在该方法中,创建一个Node节点,调用有参构造,传入Node.SHARED和当前线程,Node.SHARED表明是共享锁,然后给Node的thread属性赋值为当前线程。然后进行Node的入队操作,若阻塞队列为空,则构建之,不为空,则将创建的Node节点放入队尾,具体代码参考ReentrantLock(因为都是调用的AbstractQueuedSynchronizer#addWaiter()方法,逻辑一模一样,这里不再赘述)。然后进入for死循环,拿到创建的Node对象的前一个节点,判断前一个结点是否是head节点,如果是,则调用AbstractQueuedSynchronizer#tryAcquireShared()方法,尝试获取资源,前面讲过如何获取的,这里不再赘述。如果得到的结果 r >= 0,表示获取锁成功,则进入if逻辑,调用
AbstractQueuedSynchronizer#setHeadAndPropagate()方法,传入创建的Node对象和r,如下图所示:
在这里插入图片描述
在该方法中,首先获取head节点,调用AbstractQueuedSynchronizer#setHead()方法,传入Node对象,和ReentrantLock一样,也是向Node对象设置为头节点,并把Node的prev设为null,他的属性thread也设置为null。然后判断if中的条件,如果满足if的条件,并且head的下一个节点为空或者head的下一节点是Node.SHARED,则调用AbstractQueuedSynchronizer#doReleaseShared()方法,继续唤醒下一个节点,如下图所示:
在这里插入图片描述
因此,Semaphore中 阻塞的线程被唤醒,具有传递性,这是不同于ReentrantLock的,ReentrantLock必须调用ReentrantLock#unlock()方法才能唤醒下一个节点,这样做的的好处是,提高效率,而不是只有调用了Semaphore#release()方法才能唤醒线程,因为它不是独占锁。接着回到Semaphore#doAcquireSharedInterruptibly()方法中 ,如下图所示:
在这里插入图片描述
如果获取锁失败,则不会进入if逻辑,不会调用Semaphore#setHeadAndPropagate()方法,而是调用
AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire()方法,这个方法不用我多少吧,ReentrantLock中讲的很详细了,最终就会调用到AbstractQueuedSynchronizer#parkAndCheckInterrupt()方法,阻塞当前线程。这就是Semaphore的加锁逻辑,跟ReentrantLock相比,还是有一些不同的。再看看Semaphore#release()方法,传入1,如下图所示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在Sync#tryReleaseShared()方法中,就是一个for死循环,拿到当前state,+1,通过CAS设置回state,由于是存在多个线程竞争,因此采用for死循环+CAS保证设置state一定成功。如果成功,则返回true。再回到AbstractQueuedSynchronizer#releaseShared()方法中 ,如下图所示 :
在这里插入图片描述
由于AbstractQueuedSynchronizer#tryReleaseShared() 方法返回的是true,则进入AbstractQueuedSynchronizer#doReleaseShared()方法,如下图所示:
在这里插入图片描述
就是前面说的,唤醒下一个线程的逻辑,即调用AbstractQueuedSynchronizer#unparkSuccessor()方法,如下图所示:
在这里插入图片描述
最终就是拿到head节点的下一个节点,如果下一个节点不为空,则拿到这个节点中的thread属性的值,调用LockSupport.unpark()方法,传入线程,欢迎该线程。该线程被被唤醒,就会从被阻塞的地方开始,往下执行。具体就不说了,类似ReentrantLock。这就是Semaphore的解锁唤醒阻塞线程的逻辑。
实际上,如果你对前面ReentrantLock的加锁解锁逻辑看懂了,想必对Semaphore的源码读起来会非常轻松,说白了,不管是ReentrantLock还是Semaphore,加锁/解锁功能的实现都是借助于AbstractQueuedSynchronize类。
这里多补充一点:如果我们自己想写一个类似ReentrantLock的独占锁,其实很简单,写一个类,继承AbstractQueuedSynchronizede类,并实现AbstractQueuedSynchronizede几个方法即可。
实现AbstractQueuedSynchronizede#tryAcquire()方法,实现加锁逻辑:
在这里插入图片描述
实现AbstractQueuedSynchronizede#tryRelease()方法,实现解锁逻辑:
在这里插入图片描述
如果是类似Semaphore的共享锁,则实现实现AbstractQueuedSynchronizede#tryAcquireShared()方法和AbstractQueuedSynchronizede#tryReleaseShared()方法,如下图所示:
在这里插入图片描述

三、CountDownLatch
CountDownLatch这个类,在实际开发的过程中用得也挺多的,就相当于是比赛赛跑一样,当所有的选手各就各位了,只要听到裁判的哨声响起,就开始跑。这里的选手们,就相当于是一个个的线程,因此,我们在定义CountDownLatch的时候,需要告诉它有多少个线程,即调用他的有参构造。CountDownLatch的具体使用如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以看出,核心的就是两个方法,即:CountDownLatch#await()方法和CountDownLatch#countDown()方法。如果如图中的例子,有两个线程,则创建CountDownLatch对象的时候,调用有参构造,传入2,也就是设置state属性为2。再看看CountDownLatch#await()方法,如下图所示:
在这里插入图片描述
类似ReentrantLock和Semaphore,ReentrantLock里面也有一个sync属性,也是继承自AbstractQueuedSynchronizer,但是它就是一个就没有子类了,不存在所谓的公平锁和非公平锁。再看看Sync#acquireSharedInterruptibly()方法,如下图所示:
在这里插入图片描述
继续看AbstractQueuedSynchronizer#tryAcquireShared()方法,如下图所示:
在这里插入图片描述
在这里插入图片描述
Sync#tryAcquireShared()方法实现非常简单:就是判断state是否为0,为0返回1;不为0返回-1;回到AbstractQueuedSynchronizer#acquireSharedInterruptibly()方法。因为构造中传入的state等于2,不等于0,因此Sync#tryAcquireShared()返回的是-1,是小于0的,所以进入if逻辑,即调用AbstractQueuedSynchronizer#doAcquireSharedInterruptibly()方法,如下图所示:
在这里插入图片描述
这一块代码就不用我多少了吧,参考前面提到的Semaphore,所以本质上CountDownLatch也是一把共享锁,然后线程会被阻塞,只不过这里阻塞的是主线程,线程将会在CountDownLatch#await()方法出陷入等待。
再看CountDownLatch#countDown()方法,如下图所示:
在这里插入图片描述
在这里插入图片描述
再看AbstractQueuedSynchronizer#tryReleaseShared()方法,传入的是1。如下图所示:
在这里插入图片描述
在这里插入图片描述
在该方法中那个,也是一个for死循环+CAS,当设置state成功,且state为0则返回true,反之返回false。假设当第二个线程也进入到了该方法并且CAS成功,这是state为0,返回true,这是就会进入AbstractQueuedSynchronizer#doReleaseShared()方法,如下图所示:
在这里插入图片描述
这一块的代码就不用我多少了吧,具体参考Semaphore。最后的结果就是主线程被唤醒,从CountDownLatch.await()方法处,继续往下执行。
以上,就是CountDownLatch的核心逻辑。

四、CyclicBarrier
CyclicBarrier,也叫循环栅栏或者循环屏障,功能有点类似于CountDownLatch,每次只能允许特定数量的的线程通过栅栏,与CountDownLatch不同的是,它的限制是可以被重复利用的,而CountDownLatch只能一次性同时让特定的线程通过。并且,实现原理也和CountDownLatch完全不同。那是怎么个不同法呢?且听我细细道来…
首先,看一个Demo,如下图所示:
在这里插入图片描述
还是一样的,构建CyclicBarrier对象,调用有参构造,传入数量和一个Runnable(Runnable对象可不传,有其他的构造方法)。点进去构造中看看,如下图所示:
在这里插入图片描述
这里给它的三个成员属性赋了值,count表示允许通过的线程数,parties也count的值一样,它是实现循环的关键,barrierCommand则是传入的Runnable对象,后续会被调用。
CyclicBarrier的关键方法只有一个,是CyclicBarrier#await()方法,点进去看看,如下图所示:
在这里插入图片描述
在这里插入图片描述
在该方法中,首先得到lock属性,它是ReentrantLock对象,已经初始化过。调用lock.lock()方法,进行加锁。接着往下看,对于两个if判断,一般是不会进的,也不是主线逻辑,过掉。接着就是count减1,并赋值给index,由于上面的例子设置的count是11,因此此时index为10,不等于0,走下面的for死循环逻辑:如下图所示:
在这里插入图片描述
设置了超时,则timed为truw,反之为false,在调用CyclicBarrier#doAwait()方法的时候,传入的是false,因此进入if逻辑,调用 trip.await()方法,这里的trip,实际上是这样得来的,如下图所示:
在这里插入图片描述
trip就是一个条件队列。看看ConditionObject#await()方法,如下图所示:
在这里插入图片描述
看看ConditionObject#addConditionWaiter()方法,如下图所示:
在这里插入图片描述
可知在该方法中,构建的是一个Node对象,调用有参构造,传入当前线程和Node.CONDITION。再拿到lastWaiter属性,判断lastWaiter是否为空,如果过为空的话将Node对象设置成firstWaiter;如果不为空给,则设置原来的lastWaiter的nextWaiter设置为Node对象,最后将Node对象设置为新的lastWaiter。简而言之,就是构建了一个单向链表,头结点是firstWaiter,尾节点是firstWaiter,通过nextWaiter属性来指向它的下一个节点。如果有新的Node对象来,放入尾部。最后返回创建的Node对象。再回到ConditionObject#await()方法,调用AbstractQueuedSynchronizer#fullyRelease()方法,如下图所示:
在这里插入图片描述
拿到state,即1,因为前面调用lock.lock()方法,加锁了。调用AbstractQueuedSynchronizer#release()方法,传入1,如下图所示:
在这里插入图片描述
在该方法中,就是释放锁,即将state设置为0,并唤醒阻塞的线程(如果有阻塞的线程话),并返回true,表示释放锁成功。再回到AbstractQueuedSynchronizer#fullyRelease()方法中,返回的是1。再回到ConditionObject#await()方法中,调用while循环中的AbstractQueuedSynchronizer#isOnSyncQueue()方法,传入Node对象,如下图所示:
在这里插入图片描述
判断Node对象是否已经在同步阻塞队列中了,当然是不在的,取反,得true,因此进入while循环体中,调用 LockSupport.park(this)方法,当前线程被阻塞。
回到CyclicBarrier#await()方法中,如下图所示:
在这里插入图片描述
在index等于0之前,其他进入的线程都会被阻塞在trip.await()方法这里。当第十一个线程进入该方法的时候,这时,index就为0 了满足if的条件,进入if代码块中,判断barrierCommand是否为空,不为空则调用command.run()方法(并不会开启一个线程,就只是当一个普通的run()方法调用而已)。最终进入CyclicBarrier#nextGeneration()方法,如下图所示:
在这里插入图片描述
该方法干了这么几个事:
① trip.signalAll()方法;
② 重新将count设置为11;
③ 重新创建一个新的Generation对象那个并赋值给generation属性。
重点看看trip.signalAll()方法干了些啥,如下图所示:
在这里插入图片描述
拿到firstWaiter,即单向链表的头节点,头节点不为空则进入if代码块中,调用ConditionObject#doSignalAll()方法,如下图所示:
在这里插入图片描述
就是do/while循环,条件为拿到当前节点的下一个节点 ,不为空就继续循环…,重点看
AbstractQueuedSynchronizer#transferForSignal()方法,传入Node对象,如下图所示:
在这里插入图片描述
将传入的Node节点的waitStatus通过CAS设置为0,成功,则调用AbstractQueuedSynchronizer#enq()方法,如下图所示:
在这里插入图片描述
这个方法还不熟吗?不就是执行入队操作,只不过入的这个队,是同步阻塞队列罢了,具体逻辑参看ReentrantLock这里不再赘述。该方法最后返回原来的tail,即同步阻塞队列现在尾节点的前一个节点。再回到AbstractQueuedSynchronizer#transferForSignal()方法,如下图所示:
在这里插入图片描述
通过CAS设置,将p(tail节点的前一个结点)的waitStatus属性设置为Node.SIGNAL,即-1,表示它的下一个节点可被唤醒。只有CAS设置失败了,才会调用LockSupport.unpark(node.thread);否则直接返回true。按照这个逻辑,在条件队列中的十个被阻塞的线程(单项链表),都会被转移到同步队列中去。再回到CyclicBarrier#await()方法中,如下图所示:
在这里插入图片描述
然后第11个线程执行完CyclicBarrier#nextGeneration()方法后,就返回了0了,由于lock.unlock()方法是在finally代码块中,该方法必定被执行。就会唤醒同步阻塞队列中被阻塞的线程,线程被唤醒,就会从ConditionObject#await()方法的LockSupport.park(this)方法的这一行,继续往下执行,由于是while循环,又会进入AbstractQueuedSynchronizer#isOnSyncQueue()方法,判断该节点是否在同步阻塞队列中,这是是在的,因此跳出while循环体,继续往下执行,进入AbstractQueuedSynchronizer#acquireQueued()方法如下图所示:
在这里插入图片描述
这个方法是干啥的,我就不多说了(调这个方法的原因是让该线程获取锁,因为后续会调用lock.unlock()方法解锁,加锁/解锁是成对出现的),参看ReentrantLock中的讲解。然后执行ConditionObject#await()方法后,程序回到CyclicBarrier#await()方法,继续从trip.await()方法的这一行代码,继续往下执行。在下面的if逻辑中,判断g 是否等于 generation,这时是不相等的,因为g是最开始创建的Generation对象,而generation在CyclicBarrier#nextGeneration()方法中,被重新赋值了,因此不等。所以跳出for死循环,最终走到finally的lock.unlock()方法,释放锁,唤醒同步阻塞队列的下一个线程,直到十个线程全部被唤醒为止,再进入下一轮循环…
在这里插入图片描述
以上就是CyclicBarrier执行逻辑,如有错误恳,请批评指正!

五、阻塞队列
对于阻塞队列而言,相比大家都不会很陌生,工作中或多或少会用到,即便你工作中用不到,但是你有一颗学习的心,喜欢研究开源框架的源码,那你一定能看到,作者在其框架中使用到,至于作用嘛,无非是同步转异步,将任务仍到队列,后台再起一个线程专门读取阻塞队中的数据,有则处理,没有则阻塞,对于这个请求而言,我可以快速给用户返回一个结果,提高了请求的速度,进而提高了程序的并发能力。而阻塞队列呢,其实也有很多种,话不多少,先看看ArrayBlockingQueue,具体使用如下图所示:
在这里插入图片描述
它的核心功能就是任务的存取,因此就有成对出现的存取方法,以put()/take()方法为例,其他的无非是加上超时时间,超时没有结果则份返回null。ArrayBlockingQueue#put()方法的代码如下:
在这里插入图片描述
看上面的代码,其实就可以知道,ArrayBlockingQueue的阻塞也是依赖条件队列,条件队列的实现,参考对CyclicBarrier源码的解读,这里不再赘述了。当然,按照惯例肯定还是拿到其lock属性,这个属性的类型是什么就不用我多说了,然后就是调用lock#lock()方法,在while循环条件中判断count是否等于item的长度,这里解释一下:这个count是阻塞队列中的任务数,往队列中放入一个任务,count++;反之从队列中取一个任务,则count–,item是一个数组,用于存放传入的任务。如果count等于item的长度,说明此时阻塞队列放满了,因此需要调用notFull.await()方法,阻塞放入任务的线程;如果没有满,则调用ArrayBlockingQueue#enqueue()方法,传入任务,将任务放入item中,具体是怎么放进item的呢,如下图所示:
在这里插入图片描述
上图中,先是拿到item,再将数组的putIndex的位置设置为该任务,完成任务的放入。然后就是putIndex+1,得到putIndex的新值,判断putIndex是否等于item的长度,如果是,则将putIndex置为0。并且将count+1,,再调用notEmpty.signal()方法,这个后面讲。再看ArrayBlockingQueue#take()方法,具体代码如下图所示:
在这里插入图片描述
take()方法中,逻辑正好和put()方法相反,while循环中判断count是否为0,如果是,说明数组中此时没有数据,因此调用notEmpty.await()方法,阻塞取任务的线程,再结合上面调用ArrayBlockingQueue#enqueue()方法的notEmpty.signal(),是因为已经有元素被放入数组的某个位置了,就可以唤醒被阻塞的取任务的线程,继续开始取任务了。再看看ArrayBlockingQueue#dequeue()方法,如下图所示:
在这里插入图片描述
在上图中可知,dequeue()方法中会takeIndex的位置取值,再将takeIndex+1,用takeIndex的新值判断其是否为item的长度,如果是,则将takeIndex设置为0,然后count-1,最后返回取到的任务。
以上就是存/取任务的逻辑。总结一下:
① ArrayBlockingQueue底层是用数组来存储任务的,并且会维护两个游标,即putIndex和takeIndex,以putIndex为例,从零开始,每往数组增加一个元素,则putIndex+1,直到putIndex等于数组的长度,则putIndex变为0,重新开始增长。
② 在存取任务的时候,会用到两个条件队列,即notFull、notEmpty,数组满了,调用notFull.await();数组空了,调用notEmpty.await()。同理,数组不满,调用notFull.signal();数组不空了,调用notEmpty.signal()。
③ ArrayBlockingQueue在进行元素的存取的时候,在进行加锁/解锁,使用的是同一把锁,即lock。这样会导致一个问题:存的时候,不能取;取的时候,不能存。其结果就是性能会有一定的降低。

  • 21
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值