今天有时间看了下《Java并发编程的艺术》这本书,看到并发用到的工具类,故记录之。
如果有朋友需要电子版的可以在下面百度网盘进行下载:

《Java并发编程的艺术》

接下来我们进入正题,JDK中的并发包里,提供了几个非常有用的并发工具类。
CountDownLatchCyclicBarrierSemaphore工具类提供了一种并发流程控制的手段。
Exchanger工具类则提供了在线程间交换数据的一种手段。

所以,既然是工具类,那么必然是离不开特定的场景的,于是相互之间没有谁优谁劣,只有谁更合适。

一、倒计时门栓 CountDownLatch

CountDownLatch允许一个或多个线程等待其他线程完成操作。

有这么一个常见的场景,我们一起来看看:

大家日常经常使用的拼多多,一件商品至少需要两到三人拼团,商家才会发货。

这里,我们不去研究它的商业模式,不管他是怎么实现盈利的,就这么一种场景,如果要用基本的并发 API 来实现,你可能会想到:

来一个线程阻塞一次,知道达到指定的数量后,全部唤醒。

对,没错,CountDownLatch 内部就是这样实现的,轮子已经帮你造好了,我们来看看该怎么实现上述的模型案例:

package com.concurrent.util;

import java.util.concurrent.CountDownLatch;

/**
 * @author riemann
 * @date 2019/07/27 22:28
 */
public class CountDownLatchDemo {

    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(3);
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " 拼团成功!");
                latch.countDown();
            }).start();
        }
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("三人拼团成功,商家发货!");
    }

}

多运行几次,你会发现结果不会错,拼团的人先后顺序可能不同,但商家一定是在三个人都准备好了之后才会发货。

除此之外,它还有更多的应用,比如百米赛跑,只有当所有运动员都准备好了之后,裁判员才会吹响哨子,等等。。。

实现原理也基本和显式锁类似,不同点依然在于对 state 的控制,CountDownLatch 只判断 state 是否等于零,不等于零就说明时机未到,阻塞当前线程。

而每一次的 countDown 方法调用都会减少一次倒计时资源,直至为零才唤醒阻塞的线程。

二、同步屏障 CyclicBarrier

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

其中:

CyclicBarrier(int parties):初始化定义需要等待的线程数parties。
CyclicBarrier(int parties, Runnable barrierAction):当屏障开放的时候,线程barrierAction的任务会执行。

我们来考虑这么一个场景:

公寓的班车总是在公寓楼下装满一车人之后,出发并开到地铁站,接着再回来接下一班人。

这种场景,我们考虑该怎么实现呢?下面我们来看一下:

package com.concurrent.util;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/**
 * @author riemann
 * @date 2019/07/27 22:51
 */
public class CyclicBarrierDemo {

    static Integer count = 0;

    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(20, () -> {
            if (count == 0) {
                System.out.println("班车准备开始运营!");
                count++;
            } else {
                System.out.println("车上座位已满,请等待下一班!");
                count++;
            }
        });
        //公寓有一百人
        for (int i = 0; i < 100 ; i++) {
            new Thread(() -> {
                try {
                    //模拟起床耗时
                    Thread.sleep((long) (Math.random() * 1000));
                    barrier.await();
                    System.out.println(Thread.currentThread().getName() + " 赶上了" + count + "趟班车。");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

}

输出结果:

班车准备开始运营!
Thread-57 赶上了1趟班车。
Thread-48 赶上了1趟班车。
Thread-89 赶上了1趟班车。
Thread-93 赶上了1趟班车。
Thread-84 赶上了1趟班车。
Thread-69 赶上了1趟班车。
Thread-6 赶上了1趟班车。
Thread-99 赶上了1趟班车。
Thread-1 赶上了1趟班车。
Thread-10 赶上了1趟班车。
Thread-20 赶上了1趟班车。
Thread-81 赶上了1趟班车。
Thread-98 赶上了1趟班车。
Thread-95 赶上了1趟班车。
Thread-41 赶上了1趟班车。
Thread-82 赶上了1趟班车。
Thread-56 赶上了1趟班车。
Thread-37 赶上了1趟班车。
Thread-74 赶上了1趟班车。
Thread-2 赶上了1趟班车。
车上座位已满,请等待下一班!
Thread-12 赶上了2趟班车。
Thread-13 赶上了2趟班车。
Thread-68 赶上了2趟班车。
Thread-80 赶上了2趟班车。
Thread-40 赶上了2趟班车。
Thread-39 赶上了2趟班车。
Thread-14 赶上了2趟班车。
Thread-87 赶上了2趟班车。
Thread-76 赶上了2趟班车。
Thread-28 赶上了2趟班车。
Thread-26 赶上了2趟班车。
Thread-51 赶上了2趟班车。
Thread-19 赶上了2趟班车。
Thread-32 赶上了2趟班车。
Thread-22 赶上了2趟班车。
Thread-34 赶上了2趟班车。
Thread-52 赶上了2趟班车。
Thread-9 赶上了2趟班车。
Thread-55 赶上了2趟班车。
Thread-78 赶上了2趟班车。
车上座位已满,请等待下一班!
Thread-23 赶上了3趟班车。
Thread-94 赶上了3趟班车。
Thread-3 赶上了3趟班车。
Thread-4 赶上了3趟班车。
Thread-11 赶上了3趟班车。
Thread-54 赶上了3趟班车。
Thread-30 赶上了3趟班车。
Thread-46 赶上了3趟班车。
Thread-33 赶上了3趟班车。
Thread-61 赶上了3趟班车。
Thread-58 赶上了3趟班车。
Thread-47 赶上了3趟班车。
Thread-65 赶上了3趟班车。
Thread-72 赶上了3趟班车。
Thread-5 赶上了3趟班车。
Thread-96 赶上了3趟班车。
Thread-92 赶上了3趟班车。
Thread-88 赶上了3趟班车。
Thread-18 赶上了3趟班车。
Thread-83 赶上了3趟班车。
车上座位已满,请等待下一班!
Thread-7 赶上了4趟班车。
Thread-64 赶上了4趟班车。
Thread-90 赶上了4趟班车。
Thread-62 赶上了4趟班车。
Thread-17 赶上了4趟班车。
Thread-49 赶上了4趟班车。
Thread-16 赶上了4趟班车。
Thread-45 赶上了4趟班车。
Thread-38 赶上了4趟班车。
Thread-15 赶上了4趟班车。
Thread-50 赶上了4趟班车。
Thread-66 赶上了4趟班车。
Thread-59 赶上了4趟班车。
Thread-25 赶上了4趟班车。
Thread-35 赶上了4趟班车。
Thread-43 赶上了4趟班车。
Thread-8 赶上了4趟班车。
Thread-77 赶上了4趟班车。
Thread-97 赶上了4趟班车。
Thread-53 赶上了4趟班车。
车上座位已满,请等待下一班!
Thread-27 赶上了5趟班车。
Thread-21 赶上了5趟班车。
Thread-71 赶上了5趟班车。
Thread-75 赶上了5趟班车。
Thread-85 赶上了5趟班车。
Thread-73 赶上了5趟班车。
Thread-67 赶上了5趟班车。
Thread-91 赶上了5趟班车。
Thread-79 赶上了5趟班车。
Thread-44 赶上了5趟班车。
Thread-36 赶上了5趟班车。
Thread-24 赶上了5趟班车。
Thread-0 赶上了5趟班车。
Thread-60 赶上了5趟班车。
Thread-70 赶上了5趟班车。
Thread-31 赶上了5趟班车。
Thread-63 赶上了5趟班车。
Thread-29 赶上了5趟班车。
Thread-42 赶上了5趟班车。
Thread-86 赶上了5趟班车。

CyclicBarrier 就像一个屏障,实例化的时候需要传入两个参数,第一个参数指定我们的屏障最多拦截多少个线程后就打开屏障,第二个参数指明最后一个到达屏障的线程需要额外做的操作。

一般而言,最后一个线程到达屏障后,屏障将会打开,释放前面所有的线程,并在最后重新关上屏障。

CyclicBarrier 只需要用到一个 await 就可以完成所有的功能,我们总结下该方法的实现逻辑:

1、首先,减少一次可用资源数量。
2、如果可用资源数为零,则说明自己是最后一个线程,于是会执行我们传入的额外操作,唤醒所有已经到达在等待的线程,并重新开启一个屏障计数。
3、否则说明自己不是最后一个线程,于是将自身线程在一个循环当中阻塞到一个条件队列上。

三、信号量 Semaphore

Semaphore又名信号量,是操作系统中的一个概念,在Java并发编程中,信号量控制的是线程并发的数量。
作用:Semaphore管理一系列许可证。每个acquire方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个release方法增加一个许可证,这可能会释放一个阻塞的acquire方法。然而,其实并没有实际的许可证这个对象,Semaphore只是维持了一个可获得许可证的数量,主要控制同时访问某个特定资源的线程数量,多用在流量控制。
注意:其他Semaphore的底层实现就是基于AQS的共享锁实现的。

如果一个线程要访问共享资源,必须先获得信号量,如果信号量的计数器值大于1,意味
着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。如果计数器值为0,线
程进入休眠。当某个线程使用完共享资源后,释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。

Semaphore 适用于什么样的使用场景呢,我们举个通俗的例子:

Semaphore可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,就可以使用Semaphore来做流量控制。

package com.concurrent.util;

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

/**
 * @author riemann
 * @date 2019/07/27 23:31
 */
public class SemaphoreDemo {

    private static final int THREAD_COUNT = 30;
    private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT);
    private static Semaphore semaphore = new Semaphore(10);

    public static void main(String[] args) {
        for (int i = 0; i < THREAD_COUNT; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName() + " 连接成功,保存数据。");
                        Thread.sleep((long) (Math.random() * 1000));
                        System.out.println(Thread.currentThread().getName() + " 释放连接。");
                        semaphore.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        threadPool.shutdown();
    }

}

输出结果:

pool-1-thread-2 连接成功,保存数据。
pool-1-thread-3 连接成功,保存数据。
pool-1-thread-1 连接成功,保存数据。
pool-1-thread-4 连接成功,保存数据。
pool-1-thread-6 连接成功,保存数据。
pool-1-thread-7 连接成功,保存数据。
pool-1-thread-8 连接成功,保存数据。
pool-1-thread-10 连接成功,保存数据。
pool-1-thread-5 连接成功,保存数据。
pool-1-thread-9 连接成功,保存数据。
pool-1-thread-10 释放连接。
pool-1-thread-14 连接成功,保存数据。
pool-1-thread-2 释放连接。
pool-1-thread-15 连接成功,保存数据。
pool-1-thread-9 释放连接。
pool-1-thread-12 连接成功,保存数据。
pool-1-thread-8 释放连接。
pool-1-thread-16 连接成功,保存数据。
pool-1-thread-3 释放连接。
pool-1-thread-11 连接成功,保存数据。
pool-1-thread-14 释放连接。
pool-1-thread-18 连接成功,保存数据。
pool-1-thread-18 释放连接。
pool-1-thread-19 连接成功,保存数据。
pool-1-thread-7 释放连接。
pool-1-thread-20 连接成功,保存数据。
pool-1-thread-5 释放连接。
pool-1-thread-17 连接成功,保存数据。
pool-1-thread-6 释放连接。
pool-1-thread-13 连接成功,保存数据。
pool-1-thread-13 释放连接。
pool-1-thread-21 连接成功,保存数据。
pool-1-thread-1 释放连接。
pool-1-thread-22 连接成功,保存数据。
pool-1-thread-4 释放连接。
pool-1-thread-23 连接成功,保存数据。
pool-1-thread-12 释放连接。
pool-1-thread-24 连接成功,保存数据。
pool-1-thread-22 释放连接。
pool-1-thread-25 连接成功,保存数据。
pool-1-thread-19 释放连接。
pool-1-thread-26 连接成功,保存数据。
pool-1-thread-17 释放连接。
pool-1-thread-27 连接成功,保存数据。
pool-1-thread-15 释放连接。
pool-1-thread-28 连接成功,保存数据。
pool-1-thread-11 释放连接。
pool-1-thread-30 连接成功,保存数据。
pool-1-thread-26 释放连接。
pool-1-thread-29 连接成功,保存数据。
pool-1-thread-24 释放连接。
pool-1-thread-16 释放连接。
pool-1-thread-28 释放连接。
pool-1-thread-29 释放连接。
pool-1-thread-20 释放连接。
pool-1-thread-27 释放连接。
pool-1-thread-21 释放连接。
pool-1-thread-25 释放连接。
pool-1-thread-30 释放连接。
pool-1-thread-23 释放连接。

在代码中,虽然有30个线程在执行,但是只允许10个并发执行。Semaphore的构造方法Semaphore(int permits)接受一个整型的数字,表示可用的许可证数量。Semaphore(10)表示允许10个线程获取许可证,也就是最大并发数是10。
Semaphore的用法也很简单,首先线程使用Semaphore的acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证。还可以用tryAcquire()方法尝试获取许可证。

Semaphore还提供一些其他方法,具体如下:

int availablePermits():返回此信号量中当前可用的许可证数。
int getQueueLength():返回正在等待获取许可证的线程数。
boolean hasQueuedThreads():是否有线程正在等待获取许可证。
void reducePermits(int reduction):减少reduction个许可证,是个protected方法。
Collection getQueuedThreads():返回所有等待获取许可证的线程集合,是个protected方法。

四、线程间交换数据的 Exchanger

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

下面来看一下Exchanger的应用场景:

1、Exchanger可以用于遗传算法,遗传算法里需要选出两个人作为交配对象,这时候会交换两人的数据,并使用交叉规则得出2个交配结果。
2、Exchanger也可以用于校对工作,比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水,为了避免错误,采用AB岗两人进行录入,录入到Excel之后,系统需要加载这两个Excel,并对两个Excel数据进行校对,看看是否录入一致。

package com.concurrent.util;

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

/**
 * @author riemann
 * @date 2019/07/27 23:49
 */
public class ExchangerDemo {

    private static final Exchanger<String> exchanger = new Exchanger<String>();
    private static ExecutorService threadPool = Executors.newFixedThreadPool(2);

    public static void main(String[] args) {
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    String A = "银行流水100";// A录入银行流水数据
                    String B = exchanger.exchange(A);
                    System.out.println("A的视角:A和B数据是否一致: " + A.equals(B) +
                            ",A录入的是: " + A + ",B录入是: " + B + "。");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    String B = "银行流水200";// B录入银行流水数据
                    String A = exchanger.exchange(B);
                    System.out.println("B的视角:A和B数据是否一致: " + A.equals(B) +
                            ",A录入的是: " + A + ",B录入是: " + B + "。");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

}

输出结果:

B的视角:A和B数据是否一致: false,A录入的是: 银行流水100,B录入是: 银行流水200。
A的视角:A和B数据是否一致: false,A录入的是: 银行流水100,B录入是: 银行流水200。

如果两个线程有一个没有执行exchange()方法,则会一直等待,如果担心有特殊情况发生,避免一直等待,可以使用exchange(V x,longtimeout,TimeUnit unit)设置最大等待时长。

五、CyclicBarrier和CountDownLatch的区别

第一个区别:

  • CountDownLatch 一旦被打开后就不能再次合上,也是说只要被调用了足够次数的 countDown,await 方法就会失效,它是一次性的。
  • CyclicBarrier 是循环发生的,当最后一个线程到达屏障,会优先重置屏障计数,屏障再次开启拦截阻隔。

第二个区别:

  • CountDownLatch 是计数器, 线程来一个就记一个,此期间不阻塞线程,当达到指定数量之后才会去唤醒外部等待的线程,也就是说外部是有一个乃至多个线程等待一个条件满足之后才能继续执行,而这个条件就是满足一定数量的线程,这样才能激活当前外部线程的继续执行。
  • CyclicBarrier 像一个栅栏,来一个线程阻塞一个,直到阻塞了指定数量的线程后,一次性全部激活,让他们同时执行,像一个百米冲刺一样。