写在前面
同步辅助工具类的目的是在于多线程间的协调与通信。本文参考官方文档。
CountDownLatch
允许一个或多个线程等待,直到在其它线程中执行的一组操作完成。
CountDownLatch
是用给定的count
初始化的。由于调用了countDown()
方法,await
方法阻塞,直到当前计数为零之后释放所有等待线程;任何后续的await
调用将立即返回。这是一种一次性现象——计数无法重置。如果需要重置计数的版本,可以考虑使用 CyclicBarrier
。
用例:实现多个工作线程将在同一时刻开始工作,而主线程需要等待所有工作线程完成才能继续执行。我们将使用两个 CountDownLatch
:
- 第一个是启动信号,它阻止任何工作线程执行,直到主线程准备好让它们开始工作;
- 第二个是完成信号,它使主线程等待,直到所有工作线程完成;
Worker.java:工作线程类
public class Worker implements Runnable {
private final CountDownLatch startSignal;
private final CountDownLatch doneSignal;
Worker(CountDownLatch startSignal, CountDownLatch doneSignal){
this.startSignal = startSignal;
this.doneSignal = doneSignal;
}
@Override
public void run() {
try {
startSignal.await();
doWork();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
doneSignal.countDown();
}
}
private void doWork(){
System.out.println("我完成工作了!");
}
}
Driver.java:启动类(主线程类)
public class Driver {
public static void main(String[] args) throws InterruptedException{
int workerNumber = 10;
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(workerNumber);
for (int i = 0; i < 10 ; i++) {
new Thread(new Worker(startSignal, doneSignal)).start();
}
System.out.println("工作都准备完毕!");
System.out.println("开始执行!");
startSignal.countDown();
doneSignal.await();
System.out.println("一共:" + workerNumber + "人都执行完了");
}
}
运行将得到以下结果:
工作都准备完毕!
开始执行!
我完成工作了!
我完成工作了!
我完成工作了!
我完成工作了!
我完成工作了!
我完成工作了!
我完成工作了!
我完成工作了!
我完成工作了!
我完成工作了!
一共:10人都执行完了
await 方法在调用时,计数已经变为0,该方法将立即返回。
发现大多数博客都未对 await()
方法的 InterruptedException
异常进行解释,该异常是指当前线程的 interrupt()
方法被调用,会导致该异常的抛出;我们可以在调用 await()
方法时,捕获该异常,届时线程的中断状态将会被清除。
CountDownLatch
的 await()
可以设置时间参数,该方法拥有返回值,如果在指定的时间内得以继续执行,该方法将返回 true
, 如果是超时,该方法将放回 false,程序接着往下执行,该方法依然能够响应中断异常。
有一种大家都在遵守的模式,如果在调用 await() 超时的方法时,需要有条件的判断,那么应基于循环的条件判断,而不是 if,例如,我有着这样一个条件,当商品总数大于 10000 时,信号
CountDownLatch
将等待,我们应该这样调用:while(total > 10000){ signalCountDownLatch.await(1, TimeUnit.SECONDS); } // 不是这样,尽管我们很容易写成这样 if(total > 10000){ signalCountDownLatch.await(1, TimeUnit.SECONDS); }
Exchanger
线程间的元素交换。每个线程在进入 exchange
方法时,传递某个对象,等待与合作伙伴线程匹配,并在返回时接收其合作伙伴的对象。交换器可以看作是同步队列的双向形式。
用例:使用交换器在线程之间交换缓冲区,以便填充缓冲区的线程在需要时得到一个新清空的缓冲区,并将填充的缓冲区交给正在清空缓冲区的线程。
FillAndEmpty.java
package com.duofei.synchron;
import java.nio.ByteBuffer;
import java.util.concurrent.Exchanger;
/**
* Exchanger 同步工具学习
* @author duofei
* @date 2019/11/20
*/
public class FillAndEmpty {
Exchanger<ByteBuffer> exchanger = new Exchanger<>();
ByteBuffer initialEmptyBuffer = ByteBuffer.allocateDirect(10);
ByteBuffer initialFullBuffer = ByteBuffer.allocateDirect(10);
public static void main(String[] args) {
FillAndEmpty fillAndEmpty = new FillAndEmpty();
new Thread(fillAndEmpty.new FillingLoop()).start();
new Thread(fillAndEmpty.new Emptying()).start();
}
class FillingLoop implements Runnable{
@Override
public void run() {
ByteBuffer currentBuffer = initialEmptyBuffer;
try {
currentBuffer.clear();
while (currentBuffer != null){
if(currentBuffer.hasRemaining()){
currentBuffer.put(new String("he").getBytes());
}
if(!currentBuffer.hasRemaining()){
currentBuffer = exchanger.exchange(currentBuffer);
currentBuffer.clear();
System.out.println("重新获得空的缓冲区");
}
}
}catch (InterruptedException ex){
ex.printStackTrace();
}
}
}
class Emptying implements Runnable{
@Override
public void run() {
ByteBuffer currentBuffer = initialFullBuffer;
try{
currentBuffer.flip();
byte[] bytes = new byte[10];
while (currentBuffer != null){
if (currentBuffer.hasRemaining()){
currentBuffer.get(bytes);
System.out.println("缓存区内容为:" + new String(bytes));
}
if(!currentBuffer.hasRemaining()){
currentBuffer = exchanger.exchange(currentBuffer);
currentBuffer.flip();
System.out.println("重新获得拥有数据的缓冲区");
}
}
}catch (InterruptedException ex){
ex.printStackTrace();
}
}
}
}
控制台打印内容:
重新获得空的缓冲区
重新获得拥有数据的缓冲区
缓存区内容为:hehehehehe
上述语句在控制台会成块(上面的三条打印语句)打印,但块里的语句打印顺序不定。
CyclicBarrier
允许一组线程彼此等待到达一个共同的障碍点。cyclicbarrier
在包含固定大小的线程的程序中非常有用,这些线程有时必须彼此等待。这个屏障被称为循环屏障,因为它可以在等待的线程被释放后重新使用。仅从这里来看,它的特性和 CountDownLatch
(计数为1的)类似,唯一的差别在于 CountDownLatch
中的计数无法被重置,而 cyclicbarrier
中的状态是可以重用的。
其实它们是不同的,或许描述为 " CountDownLatch 计数为1 的情况下能完成部分栅栏的工作" 更为合理;并且,CyclicBarrier
在构造时支持一个可选的Runnable命令,该命令在每一个屏障点上运行一次,在最后一个线程到达之后,但是在释放任何线程之前。这个屏障动作对于在任何一方继续之前更新共享状态非常有用。
用例:使用屏障实现并行分解。每个工作线程处理矩阵的一行,然后在屏障处等待,直到处理完所有行。在处理所有行时,执行所提供的Runnable barrier操作并合并行。
Solver.java
package com.duofei.synchron;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* CyclicBarrier
* @author duofei
* @date 2019/11/20
*/
public class Solver {
final int N;
final float[][] data;
final CyclicBarrier barrier;
public static void main(String[] args) {
Solver solver = new Solver(new float[10][2]);
for (int i = 0; i < solver.N; i++) {
new Thread(solver.new Worker(i)).start();
}
}
class Worker implements Runnable{
int myRow;
Worker(int row){
myRow = row;
}
@Override
public void run() {
System.out.println("我处理完成第" + myRow + "行");
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
public Solver(float[][] matrix){
data = matrix;
N = matrix.length;
barrier = new CyclicBarrier(N, ()->{
System.out.println("行数据合并");
});
}
}
输出结果:
我处理完成第0行
我处理完成第1行
我处理完成第3行
我处理完成第2行
我处理完成第4行
行数据合并
我完成了4行,我是第0个到达屏障处的
我完成了0行,我是第4个到达屏障处的
我完成了1行,我是第3个到达屏障处的
我完成了3行,我是第2个到达屏障处的
我完成了2行,我是第1个到达屏障处的
还有一点,在调用 await
方法时,还抛出 BrokenBarrierException
方法,这是什么样的情形呢?官方文档描述到:
如果一个线程由于中断,失败,或超时,过早离开障碍点;所有在障碍点等待的线程也将离开,通过捕获的
BrokenBarrierException
异常 ( 或InterruptedException
如果他们也被打断了大约在同一时间)。
由于异常的原因离开屏障,并不会执行构造屏障时的 Runnable
,并且当一个线程在已经破坏了的屏障前等待时,会立即得到一个 BrokenBarrierException
异常。我们可以修改上述代码来验证我们的猜想:
package com.duofei.synchron;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* CyclicBarrier
* @author duofei
* @date 2019/11/20
*/
public class Solver {
final int N;
final float[][] data;
final CyclicBarrier barrier;
public static void main(String[] args) {
Solver solver = new Solver(new float[5][2]);
for (int i = 0; i < solver.N; i++) {
final Thread thread = new Thread(solver.new Worker(i));
thread.start();
System.out.println(System.currentTimeMillis() + ": 打断" + thread.getName());
// 测试线程打断
thread.interrupt();
}
}
class Worker implements Runnable{
int myRow;
Worker(int row){
myRow = row;
}
@Override
public void run() {
System.out.println("我处理完成第" + myRow + "行");
try {
final int await = barrier.await();
System.out.println("我完成了" + myRow+ "行,我是第" + await + "个到达屏障处的");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
System.out.println(System.currentTimeMillis() + "屏障破坏异常:" + Thread.currentThread().getName());
e.printStackTrace();
}
}
}
public Solver(float[][] matrix){
data = matrix;
N = matrix.length;
barrier = new CyclicBarrier(N, ()->{
System.out.println("行数据合并");
});
}
}
尽管我新增了很多打印语句,但由于程序执行的非常快,这仍然很难判断,但通过基于线程的断点调试,你将会获得更清楚的结果。执行上述语句,我们将捕获到四个 BrokenBarrierException
异常,一个中断异常,构造屏障时的 Runnable
也并未执行。
Semaphore
计数信号量。从概念上讲,信号量维护一组许可证。如果需要,每个acquire()
都会阻塞,直到获得许可证,然后获取许可证。每个release()
添加一个许可证,潜在地释放一个阻塞的获取者。但是,没有实际使用许可证对象;信号量只是保持可用数量的一个计数,并相应地进行操作。
用例:信号量通常用于限制能够访问某些(物理或逻辑)资源的线程数量。例如,这里有一个类使用信号量来控制对池中数据的处理。
Pool.java
package com.duofei.synchron;
import java.util.concurrent.Semaphore;
import java.util.function.Consumer;
/**
* semaphore 学习
* @author duofei
* @date 2019/11/20
*/
public class Pool {
private static final int MAX_AVAILABLE = 5;
private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);
private Integer data = new Integer(10);
/**
* 处理池中数据
*/
int handleData(Consumer<Integer> handle){
try {
available.acquire();
handle.accept(data);
return data.intValue();
} catch (InterruptedException e) {
e.printStackTrace();
}
return -1;
}
/**
* 释放池中数据
*/
void releaseData(){
available.release();
}
public static void main(String[] args) {
Pool pool = new Pool();
for (int i = 0; i < 10; i++) {
new Thread(()-> pool.handleData(System.out::println)).start();
// 处理完数据释放
// pool.releaseData();
}
}
}
在不释放数据时,我们仅仅只能处理五次数据,剩下的线程并没有处理数据的机会。
一个初始化为一个的信号量,它的使用方式是最多只有一个可用的许可证,可以用作互斥锁。这通常被称为二进制信号量,因为它只有两种状态:一个可用许可证,或者0个可用许可证。当以这种方式使用时,二进制信号量具有这样的属性(与许多锁实现不同),即“锁”可以由所有者以外的线程释放(因为信号量没有所有权的概念)。这在某些特定上下文中很有用,比如死锁恢复。
该类的构造函数可选地接受公平性参数。当设置为
false
时,该类不能保证线程获得许可的顺序。特别是,允许倒挂,也就是说,可以一直在等待的线程之前为调用acquire()的线程分配许可证——从逻辑上讲,新线程将自己放在等待线程队列的最前面。当公平性设置为真时,信号量保证会选择调用任何acquire
方法的线程,从而按照它们对这些方法的调用的处理顺序(先进先出)。请注意,FIFO顺序必然适用于这些方法中的特定内部执行点。因此,一个线程可以在另一个线程之前调用acquire
,但是在另一个线程之后到达排序点,同样地,在方法返回时到达排序点。还请注意,不定时tryAcquire
方法不尊重公平设置,但将采取任何许可是可用的。
通常,用于控制资源访问的信号量应该被初始化为公平的,以确保没有线程因为访问资源而饿死。当将信号量用于其他类型的同步控制时,非公平排序的吞吐量优势常常超过了公平性考虑。
这个类还提供了一次获取和释放多个许可的便利方法。当使用这些方法而不将公平性设置为真时,要注意无限期延迟的风险。
官方文档的解释非常棒,如果你认真品的话。