Java并发工具-2-同步工具(Tools)

一 计数信号量Semaphore

1 概念解释

semaphore [ˈseməfɔː(r)] 信号量

从 JDK1.5 开始提供,Java 官方就在 java.util.concurrent 并发包中提供了 Semaphore 工具类。

那什么是 "Semaphore" 呢?单词 "Semaphore" 在计算机世界中被解释为中文 "信号量" ,但更能表述其含义的叫法应该是 "许可证管理器"。不管叫什么中文名称,它就是一种计数信号量,用于管理一组资源,给资源的使用者规定一个量从而控制同一时刻的使用者数目。

这样的解释是不是很抽象?没关系,在此为大家举一个生活中通俗的例子,让大家先对 "信号量" 及其应用有一个感性的认识。

大家先观察一下下面过闸机的图例,回想一下我们平时过闸机的场景。

比如上图中过闸机就是信号量的基本运用。

上图中的乘客就类比是我们程序里面的各类线程,闸机就类比是一类线程需要使用的资源,而信号量就是某一时刻可用的闸机数量。

当某个时刻有乘客需要使用闸机过站时,首先他需要找到一台没有人使用的闸机,现实中他通过眼睛观察即可知道,在我们程序里面就是需要观察信号量,看能不能申请到代表可用闸机的信号量,如果能则表示有空闲闸机 (资源) 可用,否则需要等待其他乘客使用完毕 (信号量释放)后再使用。

概念我们已经了解了,那 Semaphore 工具类最基本的用法是怎样的呢?别急,看下面。

2 基本用法

java 复制代码
// 首先创建 Semaphore 对象
Semaphore semaphore = new Semaphore();
// 在资源操作开始之前,先获取资源的使用许可
semaphore.acquire();
...
// 在获取到资源后,利用资源进行业务处理
...
// 在资源操作完毕之后,释放资源的使用许可
semaphore.release();
...

是不是很简单,那 Semaphore 信号量在我们日常实践中,到底应该应用在哪些场合比较合适呢?下面我们给出最常用的场景说明。

3 常用场景

Semaphore 经常用于限制同一时刻获取某种资源的线程数量,最为典型的就是做流量控制

比如 WEB 服务器处理能力有限,需要控制网络请求接入的最大连接数,以防止过大的请求流量压垮我们的服务器,导致整个应用不能正常提供服务。

比如数据库服务器处理能力有限,需要控制数据库最大连接数,以防止大量某个应用过分占有数据库连接数,导致数据库服务器不能为其他的应用提供足够的连接请求。

当在研发过程中遇到类似这些场景时,就可以考虑直接应用 Semaphore 工具类辅助实现。

上面举的生活中过闸机的例子,如果用程序表达,该如何实现呢?在程序中如何使用 Semaphore 信号量达到控制和应用呢?最直接方式就是去感受最简单的例子,下面直接用最明了的代码说明例子中如何应用了信号量。

4 场景案例

java 复制代码
import java.util.concurrent.Semaphore;

public class SemaphoreTest {

    // 先定义一个Semaphore信号量对象
    private static Semaphore semaphore = new Semaphore(3);

    // 测试方法
    public static void main(String[] args) {

        // 定义10个人过闸机
        for(int i=0; i<10; i++) {
            Person person = new Person(semaphore, i);
            new Thread(person).start();
        }
    }
}

在上面的代码中,先创建了一个 Semaphore 信号量对象,然后赋给了每一位进站旅客 Person ,接下来每一位旅客如何动作呢,看下面的代码。

java 复制代码
import java.util.concurrent.Semaphore;

public class Person implements Runnable {

    private Semaphore semaphore;
    private String persionName;

    public Person(Semaphore semaphore, int persionNo) {
        this.semaphore = semaphore;
        this.persionName = "旅客" + persionNo;
    }

    public void run() {
        try {
            // 请求获得信号量,就是请求(寻找)是否有可用的闸机
            semaphore.acquire();
            // 已经等到了可用闸机
            System.out.println(this.persionName + "已经占有一台闸机");
            // 进站
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 已经进站
            System.out.println(this.persionName + "已经进站");
            // 让出闸机给别人用
            semaphore.release();
        }
    }
}

在 Person 类中,首先通过 acquire 获取了可用闸机,然后休眠 2 秒代表刷卡过闸机,最后在 finally 使用 release 方法让出闸机。我们观察一下运行结果。

复制代码
旅客0已经占有一台闸机  <---占据了一台
旅客1已经占有一台闸机  <---占据了一台
旅客6已经占有一台闸机  <---占据了一台
旅客0已经进站         <---0号旅客已经进站释放了闸机
旅客3已经占有一台闸机  <---3号旅客这个时候才拿到了可用闸机
旅客1已经进站         <---1号旅客已经进站释放了闸机
旅客2已经占有一台闸机  <---2号旅客这个时候才拿到了可用闸机
旅客6已经进站         <---6号旅客已经进站释放了闸机
旅客4已经占有一台闸机  <---4号旅客这个时候才拿到了可用闸机
旅客3已经进站
旅客4已经进站
旅客5已经占有一台闸机
旅客7已经占有一台闸机
旅客2已经进站
旅客8已经占有一台闸机
旅客7已经进站
旅客5已经进站
旅客9已经占有一台闸机
旅客8已经进站
旅客9已经进站

观察结果发现,同一时刻最多只能有 3 位旅客占用闸机进站,其他旅客需要等待其进站后让出闸机才能刷卡进站。

至此,大家对信号量已经有了初步的理解,接下来我们继续丰富对 Semaphore 工具类的认识。

5 其他方法介绍

除过上面代码中使用的最基本的 acquire 方法和 release 方法之外,我们还需要掌握其他几个核心方法的使用。下面逐个介绍。

  1. Semaphore(int permits, boolean fair)

上面的例子中使用了 Semaphore (int permits) 构造方法。

此构造方法也是用于创建信号量对象,第二个参数表示创建的信号量是否秉持公平竞争特性。即对资源的申请使用严格按照申请的顺序给予允许。

一般情况下,我们使用 Semaphore (int permits) 构造方法就可以了。

  1. availablePermits()

返回当前还可用的许可数,即还允许多少个线程进行使用资源。套用在上面的例子中,就是返回当前还有多少台闸机空闲可用。

java 复制代码
int availablePermits = semaphore.availablePermits();
System.out.println("当前可用闸机数" + availablePermits);

>>运行结果:
当前可用闸机数2
旅客0已经占有一台闸机
当前可用闸机数1
旅客1已经占有一台闸机
......
  1. hasQueuedThreads()

返回是否有线程正在等待获取资源。也就是返回当前是否有人在排队等待过闸机。

java 复制代码
boolean hasQueuedThreads = semaphore.hasQueuedThreads();
System.out.println("当前是否有旅客等待闸机进站:"+hasQueuedThreads);

>>运行结果:
当前是否有旅客等待闸机进站:false
旅客0已经占有一台闸机
当前是否有旅客等待闸机进站:false
旅客1已经占有一台闸机
当前是否有旅客等待闸机进站:false
旅客2已经占有一台闸机
  1. acquire(int permits)

申请指定数目的信号量许可,在获取不到指定数目的许可时将一直阻塞。就好比一个旅客需要同时占用两个闸机过站。类似的 release (int permits) 方法用于释放指定数目的信号量许可。

acquire (int permits) 同上面例子中使用的 acquire () 最大的区别就是用于一次性申请多个许可,当参数 permits = 1 时,两者相同。release (int permits) 和 release () 也是类似。

  1. tryAcquire()

尝试申请信号量许可,无论是否申请成功都返回申请结果。当申请成功时返回 true , 否则返回 false 。程序里面根据申请结果决定后继的处理流程。和 acquire () 的主要区别在于,不会阻塞立刻返回。

同类功能的方法还有 tryAcquire (int permits) 、tryAcquire (long timeout, TimeUnit unit) 、tryAcquire (int permits, long timeout, TimeUnit unit) 。这些方法实现的功能一样,只是可以更加精细化地控制对资源申请,比如申请超时控制、申请许可数量。

6 工具对比Semaphore -synchronized

大家可能有一个疑问了,Semaphore 好像和 synchronized 关键字没什么区别,都可以实现同步。

其实不然,synchronized 真正用于并发控制,确保对某一个资源的串行访问;而 Semaphore 限制访问资源的线程数,其实并没有实现同步,只有当 Semaphore 限制的资源同时只允许一个线程访问时,两者达到的效果一样。

大家记住,Semaphore 和 synchronized 最主要的差别是 Semaphore 可以控制一个或多个并发,而 synchronized 只能是一个。这一点需要大家好好琢磨。

还是通过上面的例子的运行结果给大家做一下解释。

java 复制代码
>>运行结果:
旅客0已经占有一台闸机  <-------
旅客1已经占有一台闸机  | 观察发现同时有多个并发执行,而非串行的一个旅客过完闸机后才轮到下一个旅客。
旅客2已经占有一台闸机  <-------
......

二 同步计数器 CountDownLatch

1 概念解释

latch /lætʃ/ 闩锁

CountDownLatch 工具类从字面理解为 "倒计数锁",其内部使用一个计数器进行实现,计数器初始值为线程的数量。当每一个线程完成自己的任务后,计数器的值就会减一。当计数器的值为 0 时,表示所有的线程都已经完成了任务,然后在 CountDownLatch 上等待的线程就可以恢复继续执行后继任务。是不是很抽象,其实很简单,看下面的图例。

这就是 CountDownLatch 工具类的基本逻辑。概念已经了解了,CountDownLatch 工具类最基本的用法是怎样的呢?看下面。

2 基本用法

java 复制代码
// 创建一个 CountDownLatch 对象
CountDownLatch countDownLatch = new CountDownLatch(子线程个数);

// 子线程1开始处理逻辑
...
// 子线程执行完所有逻辑进行计数器减1
countDownLatch.countDown();

// 子线程n开始处理逻辑
...
// 子线程执行完所有逻辑进行计数器减1
countDownLatch.countDown();

// 主线程等待所有子线程执行完
countDownLatch.await();
// 主线程继续执行后继逻辑
...

是不是很简单,CountDownLatch 应用在哪些场合比较合适呢?下面我们给出最常用的场景说明。

3 常用场景

CountDownLatch 经常用于某一线程在开始运行前等待其他关联线程执行完毕的场合。

比如我们制作一张复杂报表,报表的各部分可以安排对应的一个线程进行计算,只有当所有线程都执行完毕后,再由最终的报表输出线程进行报表文件生成。

下面我们使用 CountDownLatch 实现这个例子。假设这张报表有 5 个部分,我们总共安排 5 个子线程分别计算,再设置 1 个报表输出线程用于最终生成报表文件。请看下面代码。

4 场景案例

java 复制代码
import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class CountDownLatchTest {
    // 创建一个 CountDownLatch 对象,初始化为 5, 代表需要控制同步的子线程个数
    static int threadCount = 5;
    private static CountDownLatch countDownLatch = new CountDownLatch(threadCount);

    // 报表生成主线程
    public static void main(String[] args) throws InterruptedException {
        // 定义报表子线程
        for (int i = 1; i <= threadCount; i++) {
            // 开始报表子线程处理
            new Thread(new Runnable() {
                public void run() {
                    // 模拟报表数据计算时间
                    try {
                        Thread.sleep(new Random().nextInt(5000));
                    } catch (Exception e) {
                    }
                    System.out.println(Thread.currentThread().getName() + "已经处理完毕");
                    countDownLatch.countDown();
                }
            }, "报表子线程" + i).start();
        }
        // 主线程等待所有子线程运行完毕后输出报表文件
        countDownLatch.await();
        System.out.println("报表数据已经全部计算完毕,开始生成报表文件...");
    }
}

运行上面代码,我们观察一下运行结果。

复制代码
报表子线程3已经处理完毕
报表子线程5已经处理完毕
报表子线程1已经处理完毕
报表子线程4已经处理完毕
报表子线程2已经处理完毕
报表数据已经全部计算完毕,开始生成报表文件...

观察结果,和我们的预期一致。注意体会 CountDownLatch 提供的多线程共同协作的模型。

5 其他方法介绍

除过上面代码中使用的最基本的 countDown ()、await () 方法之外,还有两个方法大家可以了解一下。

  1. await (long, TimeUnit) 方法
    此方法提供了更灵活的等待参数,可以设置等待超时时间。当等待超过了设定的时限,则不再阻塞直接开始后继处理。
  2. getCount () 方法
    调用此方法,可获得当前计数器的数值,了解整体处理进度。

6 工具对比CountDownLatch-join

应用场景1

假设一条流水线上有三个工作者:worker0,worker1,worker2。有一个任务的完成需要他们三者协作完成,worker2可以开始这个任务的前提是worker0和worker1完成了他们的工作,而worker0和worker1是可以并行他们各自的工作的。

如果我们要编码模拟上面的场景的话,我们大概很容易就会想到可以用join来做。当在当前线程中调用某个线程 thread 的 join() 方法时,当前线程就会阻塞,直到thread 执行完成,当前线程才可以继续往下执行。补充下:join的工作原理是,不停检查thread是否存活,如果存活则让当前线程永远wait,直到thread线程终止,线程的this.notifyAll 就会被调用。

我们首先用join来模拟这个场景:

java 复制代码
public class Worker implements Runnable {

    //工作者名
    private String name;
    //工作时间
    private long time;

    public Worker(String name, long time) {
        this.name = name;
        this.time = time;
    }

    @Override
    public void run() {
        try {
            System.out.println(name + "开始工作");
            Thread.sleep(time);
            System.out.println(name + "工作完成,耗费时间=" + time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


class Test {

    public static void main(String[] args) throws InterruptedException {

        Worker worker0 = new Worker("worker0", (long) (Math.random() * 1000));
        Worker worker1 = new Worker("worker1", (long) (Math.random() * 1000));
        Worker worker2 = new Worker("worker2", (long) (Math.random() * 1000));

        Thread thread0 = new Thread(worker0);
        Thread thread1 = new Thread(worker1);
        Thread thread2 = new Thread(worker2);

        thread0.start();
        thread1.start();

        thread0.join();
        thread1.join();
        System.out.println("准备工作就绪");

        thread2.start();
    }
}

运行test,观察控制台输出的顺序,我们发现这样可以满足需求,worker2确实是等worker0和worker1完成之后才开始工作的:

复制代码
worker0开始工作
worker1开始工作
worker0工作完成,耗费时间=230
worker1工作完成,耗费时间=513
准备工作就绪
worker2开始工作
worker2工作完成,耗费时间=954

除了用join外,用CountDownLatch 也可以完成这个需求。需要对worker做一点修改

java 复制代码
import java.util.concurrent.CountDownLatch;

public class Worker implements Runnable {

    //工作者名
    private String name;
    //工作时间
    private long time;
    // 同步计数器
    private CountDownLatch countDownLatch;

    public Worker(String name, long time, CountDownLatch countDownLatch) {
        this.name = name;
        this.time = time;
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        try {
            System.out.println(name + "开始工作");
            Thread.sleep(time);
            System.out.println(name + "工作完成,耗费时间=" + time);
            countDownLatch.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


class Test {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);

        Worker worker0 = new Worker("worker0", (long) (Math.random() * 1000), countDownLatch);
        Worker worker1 = new Worker("worker1", (long) (Math.random() * 1000), countDownLatch);
        Worker worker2 = new Worker("worker2", (long) (Math.random() * 1000), countDownLatch);

        Thread thread0 = new Thread(worker0);
        Thread thread1 = new Thread(worker1);
        Thread thread2 = new Thread(worker2);

        thread0.start();
        thread1.start();

        countDownLatch.await();
        System.out.println("准备工作就绪");

        thread2.start();
    }
}

我们创建了一个计数器为2的 CountDownLatch ,让Worker持有这个CountDownLatch 实例,当完成自己的工作后,调用countDownLatch. countDown() 方法将计数器减1。countDownLatch.await() 方法会一直阻塞直到计数器为0,主线程才会继续往下执行。观察运行结果,发现这样也是可以的:

复制代码
worker1开始工作
worker0开始工作
worker0工作完成,耗费时间=161
worker1工作完成,耗费时间=854
准备工作就绪
worker2开始工作
worker2工作完成,耗费时间=542

那么既然如此,CountDownLatch与join的区别在哪里呢?事实上在这里我们只要考虑另一种场景,就可以很清楚地看到它们的不同了。

应用场景2

假设worker的工作可以分为两个阶段,work2 只需要等待work0和work1完成他们各自工作的第一个阶段之后就可以开始自己的工作了,而不是场景1中的必须等待work0和work1把他们的工作全部完成之后才能开始。

试想下,在这种情况下,join是没办法实现这个场景的,而CountDownLatch却可以,因为它持有一个计数器,只要计数器为0,那么主线程就可以结束阻塞往下执行。我们可以在worker0和worker1完成第一阶段工作之后就把计数器减1即可,这样worker0和worker1在完成第一阶段工作之后,worker2就可以开始工作了。

java 复制代码
import java.util.concurrent.CountDownLatch;

public class Worker implements Runnable {

    //工作者名
    private String name;
    //工作时间
    private long time;
    // 同步计数器
    private CountDownLatch countDownLatch;

    public Worker(String name, long time, CountDownLatch countDownLatch) {
        this.name = name;
        this.time = time;
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        try {
            System.out.println(name + "开始工作");
            Thread.sleep(time);
            System.out.println(name + "第一阶段工作完成");

            countDownLatch.countDown();

            Thread.sleep(2000); //这里就姑且假设第二阶段工作都是要2秒完成
            System.out.println(name + "第二阶段工作完成");
            System.out.println(name + "工作完成,耗费时间=" + (time + 2000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


class Test {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);

        Worker worker0 = new Worker("worker0", (long) (Math.random() * 1000), countDownLatch);
        Worker worker1 = new Worker("worker1", (long) (Math.random() * 1000), countDownLatch);
        Worker worker2 = new Worker("worker2", (long) (Math.random() * 1000), countDownLatch);

        Thread thread0 = new Thread(worker0);
        Thread thread1 = new Thread(worker1);
        Thread thread2 = new Thread(worker2);

        thread0.start();
        thread1.start();

        countDownLatch.await();
        System.out.println("准备工作就绪");

        thread2.start();
    }
}

观察控制台打印顺序,可以发现这种方法是可以模拟场景2的:

复制代码
worker0开始工作
worker1开始工作
worker1第一阶段工作完成
worker0第一阶段工作完成
准备工作就绪
worker2开始工作
worker2第一阶段工作完成
worker1第二阶段工作完成
worker1工作完成,耗费时间=2343
worker0第二阶段工作完成
worker0工作完成,耗费时间=2469
worker2第二阶段工作完成
worker2工作完成,耗费时间=2819

总结

最后,总结下CountDownLatch与join的区别:

调用thread.join() 方法必须等thread 执行完毕,当前线程才能继续往下执行,而CountDownLatch通过计数器提供了更灵活的控制,只要检测到计数器为0当前线程就可以往下执行而不用管相应的thread是否执行完毕。

三 循环屏障 CyclicBarrier

1 概念解释

cyclical /ˈsaɪklɪk/ 循环的 barrier /ˈbæriə(r)/ 屏障

所谓 Cyclic 即循环的意思,所谓 Barrier 即屏障的意思。所以综合起来,CyclicBarrier 指的就是循环屏障,虽然这个叫法很奇怪,但是却能很好地表达其含义。

CyclicBarrier 工具类允许一组线程相互等待,直到所有线程都到达一个公共的屏障点,然后这些线程一起继续执行后继逻辑。之所以称之为 "循环",是因为在所有线程都释放了对这个屏障的使用后,这个屏障还可以重新使用。我们通过一张图可以直观了解其表达的控制模型。

现在我们已经了解了基本概念逻辑,CyclicBarrier 工具类最基本的用法是怎样的呢?看下面。

2 基本用法

java 复制代码
// 创建一个 CyclicBarrier 对象,初始化相互等待的线程数量
CyclicBarrier cyclicBarrier = new CyclicBarrier(线程个数);

// 线程1开始处理逻辑
...
// 线程1等待其他线程执行到屏障点
cyclicBarrier.await();
// 线程1等到了其他所有线程达到屏障点后继续处理后继逻辑
...

// 线程n开始处理逻辑
...
// 线程n等待其他线程执行到屏障点
cyclicBarrier.await();
// 线程n等到了其他所有线程达到屏障点后继续处理后继逻辑
...

是不是很简单,CyclicBarrier 应用在哪些场合比较合适呢?下面我们给出最常用的场景说明。

3 常用场景

CyclicBarrier 最适合一个由多个线程共同协作完成任务的场合。

这样描述很抽象,我们还是举一个生活中的例子说明:某学习班总共 5 位同学,约定周末一起乘坐大巴出游,约定了共同的集合地点,雇佣了 1 位司机。请看下面代码。

4 场景案例

java 复制代码
public class CyclicBarrierTest {
    // 创建一个 Runnable 对象,用于屏障解除时处理全局逻辑,在此例子中代表大巴司机
    private static Runnable driver = new Runnable() {
        public void run() {
            System.out.println("所有同学已经集合完毕,开始启动车辆出发。");
        }
    };

    // 创建一个 CyclicBarrier 对象,初始化为 5, 代表需要控制同步的线程个数,在此例子中代表 5 位同学
    static int threadCount = 5;
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(threadCount, driver);

    public static void main(String[] args) throws InterruptedException {
        // 模拟同学
        for (int i = 1; i <= threadCount; i++) {
            // 模拟某个同学的动作
            new Thread(new Runnable() {
                @SneakyThrows
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "已经开始出门...");
                    // 模拟同学出门赶往集合点的用时
                    try {
                        Thread.sleep(new Random().nextInt(10000));
                    } catch (Exception e) {
                    }
                    System.out.println(Thread.currentThread().getName() + "已经到达集合点");
                    // 等待其他同学到达集合点(等待其他线程到达屏障点)
                    cyclicBarrier.await();
                    //后续操作
                    System.out.println(Thread.currentThread().getName() + "真好玩666");
                }
            }, i + "号同学").start();
        }
    }
}

运行上面代码,我们观察一下运行结果。

复制代码
1号同学已经开始出门...
4号同学已经开始出门...
5号同学已经开始出门...
3号同学已经开始出门...
2号同学已经开始出门...
4号同学已经到达集合点
5号同学已经到达集合点
2号同学已经到达集合点
3号同学已经到达集合点
1号同学已经到达集合点
所有同学已经集合完毕,开始启动车辆出发。
1号同学真好玩666
3号同学真好玩666
2号同学真好玩666
5号同学真好玩666
4号同学真好玩666

观察结果,和我们的预期一致。注意体会 CyclicBarrier 提供的多线程共同协作的模型。

5 其他方法介绍

除过上面代码中使用的最基本的 await()方法之外,还有下面几个方法大家可以了解一下。

  1. CyclicBarrier(int parties)
    相比案例中使用的 CyclicBarrier(int parties, Runnable barrierAction) 构造方法,此方法只用于控制并发线程,不做屏障点到达后的其他动作。
  2. await(long timeout, TimeUnit unit) 方法
    此方法可以设置等待的时限,当时限过后还未被唤起,则直接自行唤醒继续执行后继任务。
  3. getNumberWaiting() 方法
    调用此方法,可以获得当前还在等待屏障点解除的线程数,一般用于了解整体处理进度。

7 工具对比CyclicBarrier-CountDownLatch

  • 触发条件不同

    • CyclicBarrier在等待的线程数量达到指定值时,会触发一个屏障操作,所有线程都会被释放。并且屏障会重置,可以被重用;

    • CountDownLatch是通过计数器来触发等待操作,计数器的初始值为等待的线程数量,每当一个线程完成任务,计数器减1,直到计数器为0时,所有等待的线程将被释放;

  • 重用性不同

    • CyclicBarrier可以被重用。也就是一个线程通过屏障以后可以再次使用,我们可以通过reset()方法区重置CyclicBarrier的状态;
    • CountDownLatch不能被重用。一旦计数器为0就不能再使用;
  • 线程协作方式不同

    • CyclicBarrier适合在一组线程相互等待达到共同的状态(屏障点),然后同时开始或继续执行后续操作。可以额外设置一个Runnable参数,当一组线程达到屏障点的时候,可以优先触发;

    • CountDownLatch适合用在一个或多个线程等待其他线程执行完成某个操作后再继续执行。比如在获取某个连接之前,需要等待连接初始化完毕这个场景,就可以使用CountDownLatch来完成。

四 移相器 Phaser

1 概念解释

phaser /ˈfeɪzə(r)/ 移相器;相位器

Phaser 表示 "阶段器",一个可重用的同步 barrier,与 CyclicBarrier 相比,Phaser 更灵活,而且侧重于 "重用"。Phaser 中允许 "注册的同步者(parties)" 随时间而变化。Phaser 可以通过构造器初始化 parties 个数,也可以在 Phaser 运行期间随时加入新的 parties,以及在运行期间注销 parties。

可以协调多个线程在多个阶段上同步,当所有分组中的线程都执行完当前阶段之后,Phaser 会自动进入下一个阶段

是不是又强大又抽象,没关系,我们通过一张图可以直白了解其提供的逻辑模型。

概念已经了解了,Phaser 工具类最基本的用法是怎样的呢?看下面。

2 基本用法

java 复制代码
// 创建一个 Phaser 对象
Phaser phaser = new Phaser();

// 将线程 m 作为同步者之一进行同步控制注册
phaser.register();
// 线程 m 开始处理逻辑
...
// 线程 m 等待同一周期内,其他线程到达,然后进入新的周期,并继续同步进行
phaser.arriveAndAwaitAdvance();
...

// 线程 m 执行完毕后做同步控制注销
phaser.arriveAndDeregister();
java 复制代码
import java.util.concurrent.Phaser;

public class Test {
    public static void main(String[] args) {

        Phaser phaser = new Phaser() {
            @Override
            protected boolean onAdvance(int phase, int registeredParties) {
                System.out.println("阶段" + phase + "完成" + "\n");
                return phase == 4;
            }
        };

        //Runnable类型对象,内部输出开始执行语句
        Runnable task1 = () -> {
            System.out.println(Thread.currentThread().getName() + "开始执行阶段0");
            phaser.arriveAndAwaitAdvance(); //等待其他线程到达,进入下一阶段
            System.out.println(Thread.currentThread().getName() + "开始执行阶段1");
            phaser.arriveAndAwaitAdvance(); //等待其他线程到达,进入下一阶段
            System.out.println(Thread.currentThread().getName() + "开始执行阶段2");
            phaser.arriveAndAwaitAdvance(); //等待其他线程到达,进入下一阶段
            System.out.println(Thread.currentThread().getName() + "开始执行阶段3");
            phaser.arriveAndAwaitAdvance(); //等待其他线程到达,任务结束
            System.out.println(Thread.currentThread().getName() + "完成全部任务");
            phaser.arriveAndAwaitAdvance();
        };
        Runnable task2 = () -> {
            System.out.println(Thread.currentThread().getName() + "开始执行阶段0");
            phaser.arriveAndAwaitAdvance(); //等待其他线程到达,进入下一阶段
            System.out.println(Thread.currentThread().getName() + "开始执行阶段1");
            phaser.arriveAndAwaitAdvance(); //等待其他线程到达,进入下一阶段
            System.out.println(Thread.currentThread().getName() + "开始执行阶段2");
            phaser.arriveAndAwaitAdvance(); //等待其他线程到达,进入下一阶段
            System.out.println(Thread.currentThread().getName() + "开始执行阶段3");
            phaser.arriveAndAwaitAdvance(); //等待其他线程到达,任务结束
            System.out.println(Thread.currentThread().getName() + "完成全部任务");
            phaser.arriveAndAwaitAdvance();
        };
        Runnable task3 = () -> {
            System.out.println(Thread.currentThread().getName() + "开始执行阶段0");
            phaser.arriveAndAwaitAdvance(); //等待其他线程到达,进入下一阶段
            System.out.println(Thread.currentThread().getName() + "开始执行阶段1");
            phaser.arriveAndAwaitAdvance(); //等待其他线程到达,进入下一阶段
            System.out.println(Thread.currentThread().getName() + "开始执行阶段2");
            phaser.arriveAndAwaitAdvance(); //等待其他线程到达,进入下一阶段
            System.out.println(Thread.currentThread().getName() + "开始执行阶段3");
            phaser.arriveAndAwaitAdvance(); //等待其他线程到达,任务结束
            System.out.println(Thread.currentThread().getName() + "完成全部任务");
            phaser.arriveAndAwaitAdvance();

        };

        // 定义3个线程,分别对应前面定义的Runnable对象
        Thread t1 = new Thread(task1, "线程1");
        phaser.register();
        Thread t2 = new Thread(task2, "线程2");
        phaser.register();
        Thread t3 = new Thread(task3, "线程3");
        phaser.register();

        t1.start();
        t2.start();
        t3.start();
    }
}

结果:

复制代码
线程1开始执行阶段0
线程3开始执行阶段0
线程2开始执行阶段0
阶段0完成

线程1开始执行阶段1
线程3开始执行阶段1
线程2开始执行阶段1
阶段1完成

线程1开始执行阶段2
线程3开始执行阶段2
线程2开始执行阶段2
阶段2完成

线程2开始执行阶段3
线程3开始执行阶段3
线程1开始执行阶段3
阶段3完成

线程1完成全部任务
线程2完成全部任务
线程3完成全部任务
阶段4完成

这个工具类相对而言比较复杂,大家不要着急,结合后面的案例仔细体会。Phaser 应用在哪些场合比较合适呢?下面我们给出最常用的场景说明。

3 常用场景

Phaser 适合用于具有多阶段处理的任务,在每个阶段有多个线程并行处理的场景。这样描述很抽象,我们举一个生活中的例子:有一个开发小组总共 4 个人,约定一起去旅游。计划一起出发,先去景点 A 自有活动,3 个小时后去景点 B 自有活动,2 个小时候后活动结束统一集合。这个场景中 4 个人相当于 4 个线程,分了 4 个阶段完成了整个计划。像类似这样的场景很适合用 Phaser 解决。请看下面代码。

4 场景案例

java 复制代码
public class PhaserTest {
	// 先构建一个阶段器对象
    private static TravelPhaser travelPhaser = new TravelPhaser();
	// 主逻辑
    public static void main(String[] args) throws InterruptedException {
	    // 创建 5 个线程代表每一位同事
        for (int i = 1; i < 5; i++) {
            // 对每一个需要同步控制的线程进行同步控制注册
            travelPhaser.register();
            // 模拟每一位同事开始旅游行动
            Thread thread = new Thread(new Colleague(travelPhaser), "同事" + i);
            thread.start();
        }
    }
}

上述代码在注册好需要同步控制的所有线程之后,开启了每一个线程(每位同事)的处理。每一个线程(每位同事)如何行动呢,代码如下:

java 复制代码
import java.util.Random;

/**
 * 模拟人以及旅游的各类状态
 */
public class Colleague implements Runnable {
    private TravelPhaser travelPhaser;
	public Colleague(TravelPhaser travelPhaser) {
        this.travelPhaser = travelPhaser;
    }

	/**
	 * 模拟每位同事的动作
	 */
    @Override
    public void run() {
        doAnything();
        System.out.println(Thread.currentThread().getName() + "到达出发集合地");
        travelPhaser.arriveAndAwaitAdvance();

        doAnything();
        System.out.println(Thread.currentThread().getName() + "已经在景点 A 自由活动结束");
        travelPhaser.arriveAndAwaitAdvance();

        doAnything();
        System.out.println(Thread.currentThread().getName() + "已经在景点 B 自由活动结束");
        travelPhaser.arriveAndAwaitAdvance();

        doAnything();
        System.out.println(Thread.currentThread().getName() + "到达返程集合地");
        travelPhaser.arriveAndAwaitAdvance();
    }

	/**
	 * 模拟用时
	 */
    private void doAnything() {
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (Exception e) {}
    }
}

上述代码模拟了每位同事的旅游过程。代码中使用了 arriveAndAwaitAdvance () 进行每个旅游阶段的控制。我们再接着看对旅游各个阶段的自定义控制:

java 复制代码
import java.util.concurrent.Phaser;

/**
 * 对每一个阶段进行自定义控制
 */
public class TravelPhaser extends Phaser {

    protected boolean onAdvance(int phase, int registeredParties) {
        switch (phase) {
            // 第1阶段,旅游前的集合
            case 0:
                System.out.println("出发前小组人员集合完毕,总人数:"+getRegisteredParties());
                return false;
            // 第2阶段,景点 A 游玩
            case 1:
                System.out.println("景点 A 游玩结束");
                return false;
            // 第3阶段,景点 B 游玩
            case 2:
                System.out.println("景点 B 游玩结束");
                return false;
            // 第4阶段,旅游结束返程集合
            case 3:
                System.out.println("所有活动结束后小组人员集合完毕,总人数:"+getRegisteredParties());
                return true;
            default:
                return true;
        }
    }
}

上述代码只是在各个阶段打印了一些描述信息,实际中可以做更多的逻辑控制。运行上面代码,我们观察一下运行结果。

复制代码
同事1到达出发集合地
同事4到达出发集合地
同事2到达出发集合地
同事3到达出发集合地
出发前小组人员集合完毕,总人数:4
同事3已经在景点 A 自由活动结束
同事2已经在景点 A 自由活动结束
同事1已经在景点 A 自由活动结束
同事4已经在景点 A 自由活动结束
景点 A 游玩结束
同事4已经在景点 B 自由活动结束
同事2已经在景点 B 自由活动结束
同事1已经在景点 B 自由活动结束
同事3已经在景点 B 自由活动结束
景点 B 游玩结束
同事2到达返程集合地
同事3到达返程集合地
同事1到达返程集合地
同事4到达返程集合地
所有活动结束后小组人员集合完毕,总人数:4

观察结果,和我们的预期一致。注意体会 Phaser 提供的多线程共同协作的模型。

5 其他方法介绍

除过上面代码中使用的最基本的 register ()、arriveAndAwaitAdvance ()、arriveAndDeregister ()、getRegisteredParties () 方法之外,还有下面几个方法大家可以了解一下。

  1. awaitAdvance (int phase) 方法。
    具有阻塞功能,等待 phase 周期数下其他所有的 parties 都到达后返回。如果指定的 phase 与当前的 phase 不一致,则立即返回。
  2. awaitAdvanceInterruptibly (int phase) 方法。
    同 awaitAdvance 类似,但支持中断响应,即 waiter 线程如果被外部中断,则此方法立即返回。
  3. forceTermination () 方法。
    用于强制终止 phase,此后 Phaser 对象将不可用,即 register 等将不再有效。

五 交换者 Exchanger

1 概念解释

Exchanger 表示 "交换者",此工具类提供了两个线程在某个时间点彼此交换信息的功能。使用 Exchanger 的重点是成对的线程使用 exchange () 方法,当一对线程都到达了同步点时,彼此会进行信息交换。我们通过一张图可直观了解其逻辑。

2 基本用法

java 复制代码
// 创建一个 Exchanger 对象
Exchanger exchanger = new Exchanger();

// 线程1开始处理逻辑
...
// 线程1将自己的信息交换给线程2,并一直等待线程2的交换动作
exchanger.exchange("待交换信息");
// 线程1等到了线程2的交换结果后继续处理后继逻辑
...

// 线程2开始处理逻辑
...
// 线程2将自己的信息交换给线程2,并一直等待线程1的交换动作
exchanger.exchange("待交换信息");
// 线程2等到了线程1的交换结果后继续处理后继逻辑
...

3 常用场景

Exchanger 工具类提供了成对的线程彼此同步数据的场合。我们举一个生活中的例子说明:快递员为客户派送物品,客户要求订单采用货到付款的方式进行支付。当快递员送货上门后,出示收款二维码(或者 POM 刷卡支付),客户当面扫码(或刷卡)支付。在这个例子中,快递员交换出去的是货物收到的是款项,而客户正好相反。我们用 Exchanger 工具类简单实现这个场景,请看下面代码。

4 场景案例

java 复制代码
import java.util.Random;
import java.util.concurrent.Exchanger;

public class ExchangerTest {

    // 创建一个 Exchanger 对象
    private static Exchanger<Object> exchanger = new Exchanger();

    public static void main(String[] args) throws InterruptedException {
        // 模拟快递员
        new Thread(() -> {
            System.out.println( Thread.currentThread().getName() + "送货上门中...");
            // 模拟快递送货用时
            try {
                Thread.sleep(new Random().nextInt(10000));
            } catch (Exception e) {}
            System.out.println( Thread.currentThread().getName() + "货物已经送到,等待客户付款");
            // 进行货款交换
            try {
                Object money = exchanger.exchange("快递件");
                // 收到货款
                System.out.println("已经收到货款" + money + ",继续下一单派送...");
            } catch (Exception e) {}
        }, "快递员").start();

        // 模拟客户
        new Thread(() -> {
            System.out.println( Thread.currentThread().getName() + "工作中...");
            // 模拟工作中用时
            try {
                Thread.sleep(new Random().nextInt(10000));
            } catch (Exception e) {}
            System.out.println( Thread.currentThread().getName() + "接到快递员取件电话,货物已经送到");
            try {
                // 进行货款交换
                Object packagz = exchanger.exchange("1000元");
                // 收到货款
                System.out.println("已经收到货物" + packagz + "...");
            } catch (Exception e) {}
        }, "客户").start();

    }
}

运行上面代码,我们观察一下运行结果。

java 复制代码
快递员送货上门中...
客户工作中...
客户接到快递员取件电话,货物已经送到
快递员货物已经送到,等待客户付款
已经收到货款1000元,继续下一单派送...
已经收到货物快递件...

观察结果,和我们的预期一致

5 其他方法介绍

Exchanger 工具类的用法比较简单,其提供了两个 exchange 方法,除过上面代码中使用的方法之外,其还对进行了重载。

exchange (V, timeout, TimeUnit) 方法。

允许设置交换等待的超时时间,当时间过后还未交换到需要的对方数据,则不再等待,继续后继逻辑执行。

,继续下一单派送...");

} catch (Exception e) {}

}, "快递员").start();

复制代码
    // 模拟客户
    new Thread(() -> {
        System.out.println( Thread.currentThread().getName() + "工作中...");
        // 模拟工作中用时
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (Exception e) {}
        System.out.println( Thread.currentThread().getName() + "接到快递员取件电话,货物已经送到");
        try {
            // 进行货款交换
            Object packagz = exchanger.exchange("1000元");
            // 收到货款
            System.out.println("已经收到货物" + packagz + "...");
        } catch (Exception e) {}
    }, "客户").start();

}

}

复制代码
运行上面代码,我们观察一下运行结果。

```java
快递员送货上门中...
客户工作中...
客户接到快递员取件电话,货物已经送到
快递员货物已经送到,等待客户付款
已经收到货款1000元,继续下一单派送...
已经收到货物快递件...

观察结果,和我们的预期一致

5 其他方法介绍

Exchanger 工具类的用法比较简单,其提供了两个 exchange 方法,除过上面代码中使用的方法之外,其还对进行了重载。

exchange (V, timeout, TimeUnit) 方法。

允许设置交换等待的超时时间,当时间过后还未交换到需要的对方数据,则不再等待,继续后继逻辑执行。

相关推荐
BillKu1 小时前
Java + Spring Boot + Mybatis 插入数据后,获取自增 id 的方法
java·tomcat·mybatis
全栈凯哥1 小时前
Java详解LeetCode 热题 100(26):LeetCode 142. 环形链表 II(Linked List Cycle II)详解
java·算法·leetcode·链表
chxii1 小时前
12.7Swing控件6 JList
java
全栈凯哥1 小时前
Java详解LeetCode 热题 100(27):LeetCode 21. 合并两个有序链表(Merge Two Sorted Lists)详解
java·算法·leetcode·链表
YuTaoShao1 小时前
Java八股文——集合「List篇」
java·开发语言·list
PypYCCcccCc1 小时前
支付系统架构图
java·网络·金融·系统架构
华科云商xiao徐1 小时前
Java HttpClient实现简单网络爬虫
java·爬虫
扎瓦1 小时前
ThreadLocal 线程变量
java·后端
BillKu2 小时前
Java后端检查空条件查询
java·开发语言
jackson凌2 小时前
【Java学习笔记】String类(重点)
java·笔记·学习