跳转至

CountDownLatch 计数器

​ 允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。

​ 常用的方法有:

CountDownLatch(int count);//实例化一个倒计数器,count指定计数个数
countDown();//计数减一
await();//等待当前计数器减到0时,所有线程并行执行;

​ 对于倒计数器,一种经典的场景就是火箭发射。在火箭发射前,为了保证万无一失,往往还要进行各项设备、仪器的检测。只有等到所有的检查完毕后,引擎才能点火。那么在检测环节当然是多个检测项同时进行的。

CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < latch.getCount(); i++) {
    final int t = i;
    new Thread(()->{
        System.out.println("检测第"+Thread.currentThread().getName()+"项设备完成...");
        latch.countDown();
    },String.valueOf(t)).start();
}
latch.await();
System.out.println("准备就绪,火箭发射");

结果:

检测第9项设备完成...
检测第6项设备完成...
检测第5项设备完成...
检测第8项设备完成...
检测第1项设备完成...
检测第0项设备完成...
检测第2项设备完成...
检测第4项设备完成...
检测第3项设备完成...
检测第7项设备完成...
准备就绪,火箭发射
  1. 初始化次数: CountDownLatch latch = new CountDownLatch(10);
  2. 减少计数的次数:latch.countDown();
  3. 阻塞线程直到计数完成:atch.await();

CyclicBarrier 循环屏障

​ CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。

CyclicBarrier的应用场景

CyclicBarrier可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个Excel保存了用户所有银行流水,每个Sheet保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水。

CyclicBarrier和CountDownLatch的区别

  • CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
  • CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。比如以下代码执行完之后会返回true。

范例:召唤神龙

CyclicBarrier barrier = new CyclicBarrier(7, () -> {
    System.out.println("召唤神龙...");
});
for (int i = 1; i <= 7; i++) {
    final int t = i;
    new Thread(() -> {
        System.out.println("得到第" + t + "颗龙珠");
        try {
            barrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }).start();
}

结果:

得到第7颗龙珠
得到第5颗龙珠
得到第6颗龙珠
得到第4颗龙珠
得到第2颗龙珠
得到第3颗龙珠
得到第1颗龙珠
召唤神龙...

Semaphore

​ 在获取项目之前,每个线程必须从信号量获取许可证,以保证项目可供使用。当线程完成项目后,它将返回到池中,并且许可证将返回到信号量,允许另一个线程获取该项目。请注意,acquire()调用时不会保持同步锁定,因为这会阻止项目返回到池中。信号量封装了限制访问池所需的同步,与维护池本身一致性所需的任何同步分开。

​ 信号量初始化为1,并且使用的信号量最多只有一个许可证可用作互斥锁。这通常称为*二进制信号量*,因为它只有两种状态:一种是可用的,或者是零可用的。当以这种方式使用时,二进制信号量具有属性(与许多Lock 实现不同),“锁”可以由除所有者之外的线程释放(因为信号量没有所有权的概念)。这在某些特定的上下文中很有用,例如死锁恢复。

​ 此类的构造函数可选择接受 *公平*参数。设置为false时,此类不保证线程获取许可的顺序。特别是,*闯入*是允许的,也就是说,一个线程调用acquire()可以提前已经等待线程分配的许可证-在等待线程队列的头部逻辑新的线程将自己。当公平性设置为true时,信号量保证线程调用任何一个acquire选择方法以按照处理这些方法的调用的顺序获得许可(先进先出; FIFO)。请注意,FIFO排序必然适用于这些方法中的特定内部执行点。因此,一个线程可以acquire在另一个线程之前调用,但是在另一个线程 之后到达排序点,并且类似地从该方法返回时。另请注意,不定时tryAcquire方法不符合公平性设置,但会采用任何可用的许可。

​ 通常,用于控制资源访问的信号量应初始化为公平,以确保没有线程缺乏访问资源。当将信号量用于其他类型的同步控制时,非公平排序的吞吐量优势通常超过公平性考虑。

  • 限制总共有几个资源运行new Semaphore(2)

  • 阻塞到一个可用的资源semaphore.acquire()

  • 释放资源允许semaphore.release();

范例:同时抢车位

Semaphore semaphore = new Semaphore(2);
for (int i = 1; i <= 5; i++) {
    final int t = i;
    new Thread(() -> {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName()+"\t 抢到车位");
            TimeUnit.SECONDS.sleep(5);
            System.out.println(Thread.currentThread().getName()+"\t 离开车位");
        } catch (Exception e) {

        }finally {
            semaphore.release();
        }
    }, String.valueOf(t)).start();

}