概述
- 关注点
- 条件队列转同步队列的过程。
- 线程上锁和解锁的时机。
- 代码准备
public class CyclicBarrierTest1 {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
for(int i=0;i<5;i++){
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()
+ "开始等待其他线程");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + "开始执行");
//TODO 模拟业务处理
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
- debug分析过程
首先我们先来看下CyclicBarrier的构造函数,count属性为计数,parties属性为副本,用来循环获取锁重置count。
我们进入CyclicBarrier的wait方法。最醒目的就是我们的独占锁,看代码结构我们首先能够得出的就是条件队列trip(Condition)的await()方法是需要在锁的代码块中使用的。
然后看到计数器count减一,进入trip的await()方法。
看到 addConditionWaiter()方法,从名字上看就感到比较熟悉。我们在前面的reentrantlock,semaphore和countDownLatch中都能看到AQS的addWaiter()方法,是构建同步等待队列的双向链表的。可以猜测addConditionWaiter()方法应该就是构建条件队列单向链表的。
可以看到我们创建了一个first waiter和last waiter均为以该线程为属性,waitStatus为CONDITION(-2)的node节点并返回该节点。
我们构建单向链表返回后,走到下一语句。通过名字可以感受到可能是要释放锁。
进入该方法后发现是先获取锁状态state,因为当前是加锁中所以返回1,更多逻辑还要深入release()方法中。
跟进去就会发现走的是单纯的reentrantlock的解锁操作。
fullyRelease()方法就是返回1,此时锁状态state以变为0。
await()方法走到while循环,isOnSyncQueue()方法就是判断node节点是否为CONDITION(-2),是的话false。然后线程阻塞。
此时第控制第二个线程thread1。区别只有在入队条件队列时,入队操作上稍有不同,在条件队列的单链表中加入node节点并返回。
在第三个线程thread2进入wait()方法后就比较关键了。他要实现线程从条件队列到同步队列的转换。此时count为0,进入if判断。如果在CyclicBarrier创建时传入了后需要执行的逻辑,则直接执行,要不进入 nextGeneration()方法。
进入方法后可见后两行代码是count计数重置和生成新一代,可见thread0和thread1的队列转移在条件队列trip的 signalAll()方法中。
跟进doSingnalAll()方法,传入的是thread0所在的node。
首先置空lastWaiter和firstWaiter,可见要出队了。这又是一个经典的构造,和addWaiter()方法和enq()方法配合组件双向链表及入队操作一样。我们跟入transferForSignal()方法,来看看其实怎么和doSingnalAll()配合的。
我们看到先是通过cas操作将node(thread0)节点的waitStatus又-2变为0,然后看到了我们熟悉的enq()方法可知道,此时同步队列已经构建完成,thread0所在的节点入队。
first变为thread1所在的node节点,再次循环进入 transferForSignal()方法,将thread1所在的node入队同步队列。此时,同步队列中thread0和thread1均已入队。
再次回到while循环的判断,此时条件队列以到最后,所以跳出循环。
再次回到我们最外层的dowait()方法。此时通过nextGeneration()方法,我们完成了将条件队列的节点转移到了同步队列中,并完成了count计数器的重置以及生成新的generation。然后返回0,直接到finally代码块。就是我们熟悉的reentrantlock的释放锁部分。
这部分可详见reentrantlock解读,我就不详细截图了。
tryRelease()方法将锁状态state改为0,同时将绑定的线程置空释放锁。进入 unparkSuccessor(h)方法。
将头结点的waitStatus改为0,然后释放head节点的后一个节点(thread0)
然后thread0立刻被唤醒,跳出while循环。继续往下执行进入我们熟悉的acquireQueued()方法。
在tryAcquire方法中,获取锁成功,锁状态state改为0,绑定线程。返回上一层的await()方法无任何的操作。
返回index为2,再次进入finally中的unlock代码块,唤醒thread1。
- countDownLatch与CyclicBarrier的区别
- CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset() 方法重置。
- CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。
- CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同。CountDownLatch一般用于一个或多个线程,等待其他线程执行完任务后,再执行。CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行。
- CyclicBarrier 还可以提供一个 barrierAction,合并多线程计算结果。
- CyclicBarrier是通过ReentrantLock的"独占锁"和Conditon来实现一组线程的阻塞唤醒的,而CountDownLatch则是通过AQS的“共享锁”实现
最后
以上就是虚拟泥猴桃为你收集整理的AQS之CyclicBarrier源码解析的全部内容,希望文章能够帮你解决AQS之CyclicBarrier源码解析所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复