java多线程系列:一 并发工具类的使用_1 ( CountDownLatch 、 CyclicBarrier、 Semaphore 、 Exchanger )
- 前言- 本系列随笔 会深入浅出,解析java多线程的各种技术及实现。
- 随笔主要根据 《java并发编程的艺术》一书作为参考。 本系列以使用为主要目的,本人理解有限,还望读者辩证采纳,没有过多涉及源码的讨论,重在初学者的使用,理解伪码。
- 预备知识:1. volatile 关键字需要有一定理解 2. AQS 队列同步器有一定认知 (后续我会专门讲解AQS,先瞎听就够了,不懂就装懂!)
- 可能新手对下面部分内容完全不理解,我后续会继续更博,并最终将该系列排序。
- 全文 根据常用的 CountDownLatch 、 CyclicBarrier、 Semaphore 、 Exchanger 进行讲述如何使用
一:CountDownLatch 等待多线程完成的工具类
- CDL使用场景很多,比如赛马,必须所有马都准备好了,说了 OK!才能发枪并计时。
- 使用在代码中就类似于线程间协作, A B 两个线程需要准备(执行代码), C需要等他俩给了通知之后,才能执行。
- CountDownLatch提供了一个有参构造和两个方法:
- public CountDownLatch(int count) : 填入一个参数count, 此参数被volatile 修饰,保证多线程下的安全性。
- await():使当前线程进入同步队列中阻塞,知道有通知 或者中断才能返回 继续执行。
- countDown():每调用一次 count 减1,直到减到0, 就会让所有正在执行 await()方法的线程从同步队列中返回,继续执行方法。
1. 其实死板的 join就能完成 先上代码
1 package E08工具类;
2
3 public class JoinCountDownLatchTest {
4 public static void main(String[] args) throws InterruptedException {
5 Thread parser1 = new Thread(new Runnable() {
6 @Override
7 public void run() {
8 System.out.println("parser1 finish"); //A
9 }
10 });
11
12 Thread parser2 = new Thread(new Runnable() {
13 @Override
14 public void run() {
15 System.out.println("parser2 finish"); //B
16 }
17 });
18 parser1.start();
19 parser2.start();
20 parser1.join();
21 parser2.join();
22 System.out.println("all parser finish"); //C
23 }
24 }
上述代码,当 thread.join() 时 会阻塞调用 join方法的线程(main线程), 此处main方法 需要等待 parser1、parser2 都执行完毕,才能继续,输出如下:
但是很明显, join是非常不灵活的,实际使用也早被 wait/notify 等待-通知机制给取代了。
parser1 finish
parser2 finish
all parser finish
2、 使用CountDownLatch 代替 join
package E08工具类; import java.util.concurrent.CountDownLatch; /** * 等待多线程完成的工具类 可以取代 thread.join(); */ public class CountDownLatchTest { static CountDownLatch c = new CountDownLatch(2); public static void main(String[] args) throws InterruptedException { new Thread(new Runnable() { @Override public void run() { System.out.println("1号马准备完毕"); c.countDown(); System.out.println("2号马准备完毕"); c.countDown(); } }).start(); new Thread(new Runnable() { @Override public void run() { try { System.out.println("先无视我"); //请先无视此线程,后续给您讲解我设置的意义 c.await(); System.out.println(""); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); System.out.println("等待马儿准备完毕"); c.await(); System.out.println("计时完毕,比赛结束"); System.out.println("3"); } }
结果如下: 很明显 countdownlatch 就是这么简单
1号马准备完毕 2号马准备完毕 等待马儿准备完毕 计时完毕,比赛结束 3 先无视我
3、解析源码的实现原理
1、CountDownLatch (CDL) 是根据队列同步器 AbstractQueuedSynchronizer 的要求来实现的,我后续会专门讲解。 其实就是个锁
AQS就是实现java1.5以后Lock的核心类,强调使用者(你)只需要会调用几个方法(lock unlock等)即可, 实现者(CDL的作者)也只需要写几个实现方法(tryAcquire tryRelease tryAcquireShared等)即可,其余细节的AQS全部帮忙搞定!
CDL是个比重入锁 ReetrantLock更简单的结构!
CDL 整体的结构 如下:
public class CountDownLatch { // 类!
//1、关键! CDL下有一个静态类,Sync继承了AQS,不要激动,每个锁其实都是这么搞的,这是AQS的要求! 你需要实现部分方法! 继续阅读 private static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 4982264981922014374L; Sync(int count) { //构造器,填入 count具体数值 setState(count); } int getCount() { return getState(); }
//2、重要! 需要CDL开发者自己实现的方法,目的是申请加锁(说白了就是自定义一个关于state变量的规则,state被volatile修饰)。
//此方法也很简单,CDL的意思是, state==0,就返回1,否则返回0,很重要,记住! protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; } //3、重要,需要开发者实现的方法,目的是 解锁(其实也是围绕state进行的逻辑)
//此方法,只有一个情况会返回true,就是state原来是1,被改成了0,其他情况一律返回false,这是有用意的! 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; } } }
2、研究具体的方法
上面只是让CDL开发者实现的俩方法,接下来我们看AQS怎么调用,
你没听错, 实现的 tryAcquireShared 是AQS 抽象方法 abstract tryAcquireShared 的重写,具体的逻辑都是AQS在执行,调用关于加锁、解锁的判断时,使用上面的方法。
2.1、await()方法,实际上是一个加锁方法,原理说的浅白点就是: 如果更改state值失败,就会被放入一个同步队列,阻塞,等待别人来通知它。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
-->>>
进入sync.acquireSharedInterruptibly(1);看一眼,它是AQS写好的方法,sync只是继承了而已。
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
看到没,如果 CDL开发者写的 加锁方法 返回值<0,则进入doAcquireSharedInterruptibly,这方法目的是把 当前线程给放入同步队列,让他等待。
如果 >=0,等于啥也没发生。由于state基本上都会设置为正整数N,
不管是哪个线程,只要调用了c.await();通通进入同步队列里候着。
继续分析:
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.SHARED); //由于锁分为 独占锁(只有一个线程能成功获取锁)和共享锁, 这里使用的是Shared,共享模式(允许多个线程加锁成功) 后续文章我再慢慢解读差别。
//AQS有一个 静态类,Node,形成了链表,也就是我们说的同步队列,没获取锁成功的线程都会被包装到这个链表里候着。 boolean failed = true; try { for (;;) { //无限循环,每次被唤醒 都会来获取一次,称之为自旋。 final Node p = node.predecessor(); //查看自己的前驱结点,AQS的逻辑是,当自己的前驱结点是头节点,就去获取一次锁,否则继续睡觉。 if (p == head) { int r = tryAcquireShared(arg); // 当前驱结点是头节点,获取,CDL开发这自己写的,state=0,则1,否则-1 ,所以说基本上刚开始都是 -1,调用await()的线程都去睡觉。 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); } }
理一下逻辑:
1、state被我们设置成了2, 所以调用c.await()的线程都会进入同步队列,阻塞。
2、当自己被唤醒,检查下前驱结点,如果为链表的头节点,则获取一次锁,tryAcquireShared,将自己设置为头节点 ;失败继续睡觉。
3、setHeadAndPropagate 中
if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared()) doReleaseShared(); //居然有 释放锁的方法!理解这里非常重要 这和接下来要说的 c.countDown() 是一个意思。 }
4、当countDown方法把 state给减到了0,那么 队列里的线程再tryAcquireShared 就是1了,所以会从队列中返回,结束阻塞。
至此 c.await()解析了90% 结束了
2.2 解析 c.countDown
其实就是个解锁方法,用来减少state的值,直到0。
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { //CDL 自己写的解锁方法,之前提到过,只有 state被减到了0,才能返回true,其他情况都是false. doReleaseShared(); return true; } return false; } --> private void doReleaseShared() { //此方法也比较复杂,我没有完全理解,大概意思就是对下一个节点进行唤醒,让它去尝试获取锁,当然下一个节点也一定是成功的,最后传播下去,调用了c.await()的线程都会从队列中返回 for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }
2.3* 可以看到 由于releaseShared 的逻辑,只有在state = 1时, c.dountDown才回去真正解锁一次,唤醒同步队列的首节点,其他时候都是直接返回了false。
那么问题来了,解锁一次,如何让我队列里那么多的节点都能解锁呢?
解决的方法正是 setHeadAndPropagate(node, r); 此方法不仅把自己(A)设置为头节点,让原来的头节点被GC回收,并且进行了传播,唤醒下一个节点B。
下一个节点B会继续把自己(B)当作头节点, 唤醒C。
整个 CountDownLatch就解析完毕了。
tips:实际上 CountDownLatch的源码解析就是 java1.5 Lock共享锁的实现原理的简化,甚至获取和释放锁的过程一模一样,ReetranLock只是多了重入的过程,以及它是独占锁,
而CDL是共享的。

浙公网安备 33010602011771号