概述
在java 1.5中,提供了一些非常有用的辅助类来帮助我们进行并发编程,比如CountDownLatch,CyclicBarrier和Semaphore,今天我们就来学习一下这三个辅助类的用法。
CountDownLatch
上图TA刚开始被阻塞,三个线程T1,T2,T3每次调用countDown()方法cnt就减1,等到cnt=0时,TA才开始执行。
正如Java文档所描述的那样,CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。CountDownLatch是在Java1.5被引入的,跟它一起被引入的并发工具类还有CyclicBarrier、Semaphore、ConcurrentHashMap和BlockingQueue,它们都存在于java.util.concurrent包下。
CountDownLatch工作原理相对简单,可以简单看成一个倒计数器,在构造方法中指定初始值,每次调用countDown()方法时将计数器减1,而await()会等待计数器变为0。
主要接口分析
构造器
- CountDownLatch(int count)构造器中的计数值(count)实际上就是闭锁需要等待的线程数量。这个值只能被设置一次, countDown方法会对count减一,直到count减到0的时候,当前调用await方法的线程继续执行。
主要方法
- countDown() 如果当前计数器的值大于1,则将其减1;若当前值为1,则将其置为0并唤醒所有通过await等待的线程;若当前值为0,则什么也不做直接返回。
- await() 等待计数器的值为0,若计数器的值为0则该方法返回;若等待期间该线程被中断,则抛出InterruptedException并清除该线程的中断状态。
- await(long timeout, TimeUnit unit) 在指定的时间内等待计数器的值为0,若在指定时间内计数器的值变为0,则该方法返回true;若指定时间内计数器的值仍未变为0,则返回false;若指定时间内计数器的值变为0之前当前线程被中断,则抛出InterruptedException并清除该线程的中断状态。
- getCount() 读取当前计数器的值,一般用于调试或者测试。
代码
召唤神龙需要集齐7颗龙珠才行。。
private static final int THREAD_COUNT_NUM = 7;
private static CountDownLatch latch = new CountDownLatch(THREAD_COUNT_NUM);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < THREAD_COUNT_NUM; i++) {
int index = i;
new Thread(() -> {
System.out.println("第" + index + "颗龙珠已收集!");
// 每收集到一颗龙组合,需要等待的颗数就减1
latch.countDown();
}).start();
}
// 上述7个线程执行完毕之后,才执行await后面的代码
latch.await();
System.out.println("集齐七颗龙珠,召唤神龙!");
}
运行结果
CyclicBarrier
在上图中,T1、T2、T3每调用一次await,说明当前线程到达barrier,计数减1,并且开始阻塞当前子线程,开始等待其他线程到达barrier,如果全部到达(计数为0),TA线程开始执行;
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier),和CountDownLatch非常类似,他也可以实现线程间的计数等待,但他的功能要比CountDownLatch更加强大一些。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。当某个线程调用了await方法之后,就会进入等待状态,并将计数器-1,直到所有线程调用await方法使计数器为0,才可以继续执行,由于计数器可以重复使用,所以我们又叫他循环屏障。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
主要接口分析
构造器
- CyclicBarrier(int parties) parties表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。CyclicBarrier强调的是parties个线程,大家相互等待,只要有一个没完成,所有人都得等着。
- CyclicBarrier(int parties, Runnable barrierAction)参数barrierAction是当这些线程都达到barrier状态时会执行的内容
主要方法
- await() 第一个版本比较常用,用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;等待其它参与方的到来(调用await())。如果当前调用是最后一个调用,则唤醒所有其它的线程的等待并且如果在构造CyclicBarrier时指定了action,当前线程会去执行该action,然后该方法返回该线程调用await的次序(getParties()-1说明该线程是第一个调用await的,0说明该线程是最后一个执行await的),接着该线程继续执行await后的代码;如果该调用不是最后一个调用,则阻塞等待;如果等待过程中,当前线程被中断,则抛出InterruptedException;如果等待过程中,其它等待的线程被中断,或者其它线程等待超时,或者该barrier被reset,或者当前线程在执行barrier构造时注册的action时因为抛出异常而失败,则抛出BrokenBarrierException。
- await(long timeout, TimeUnit unit) 与await()唯一的不同点在于设置了等待超时时间,等待超时时会抛出TimeoutException。
- reset() 该方法会将该barrier重置为它的初始状态,并使得所有对该barrier的await调用抛出BrokenBarrierException。
代码
还是用上收集七颗龙珠的例子,不过为了后面演示方便,输出的时候加了当前线程
简单版
private static final int THREAD_COUNT_NUM = 7;
public static void main(String[] args) throws InterruptedException {
// 屏障点
CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT_NUM, new Runnable() {
@Override
public void run() {
System.out.println("集齐七颗龙珠,召唤神龙!");
}
});
// 线程
for (int i = 0; i < THREAD_COUNT_NUM; i++) {
int index = i + 1;
Thread.sleep(5000);
new Thread(() -> {
try {
System.out.println("当前线程:" + Thread.currentThread().getName());
System.out.println("第" + index + "颗龙珠已收集!");
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
运行结果
带参数
private static final int THREAD_COUNT_NUM = 7;
public static void main(String[] args) throws InterruptedException {
// 屏障点
CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT_NUM, new Runnable() {
@Override
public void run() {
System.out.println("集齐七颗龙珠,召唤神龙!");
}
});
// 线程
for (int i = 0; i < THREAD_COUNT_NUM; i++) {
int index = i + 1;
Thread.sleep(5000);
new Thread(() -> {
try {
System.out.println("当前线程:" + Thread.currentThread().getName());
System.out.println("第" + index + "颗龙珠已收集!");
// 这里带了参数
barrier.await(2000, TimeUnit.MILLISECONDS);
} catch (InterruptedException | BrokenBarrierException | TimeoutException e) {
e.printStackTrace();
}
}).start();
}
}
运行结果
到达barrier之后,如果已经超过等待时间,但是下一个还没有到barrier。就抛出异常,继续下一步操作
可重入代码
private static final int THREAD_COUNT_NUM = 7;
public static void main(String[] args) throws InterruptedException {
// 屏障点
CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT_NUM, new Runnable() {
@Override
public void run() {
System.out.println("集齐七颗龙珠,召唤神龙!");
}
});
// 线程
for (int i = 0; i < THREAD_COUNT_NUM; i++) {
int index = i + 1;
new Thread(() -> {
try {
System.out.println("当前线程:" + Thread.currentThread().getName());
System.out.println("第" + index + "颗龙珠已收集!");
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
Thread.sleep(2000);
// 重入
for (int i = 0; i < THREAD_COUNT_NUM; i++) {
int index = i + 1;
new Thread(() -> {
try {
System.out.println("当前线程:" + Thread.currentThread().getName());
System.out.println("第" + index + "颗龙珠已收集!");
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
运行结果
图片太长,只截取部分。。
CountDownLatch与CyclicBarrier的比较
CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以理解成维护的就是一个计数器,但是这两者还是各有不同侧重点的:
- CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成,再携手共进。
- 调用CountDownLatch的countDown方法后,会阻塞主线程,当前子线程并不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法,不会阻塞主线程,会阻塞当前子线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行;
- CountDownLatch方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入barrierAction,指定当所有线程都到达时执行的业务功能;
- CountDownLatch是不能复用的,而CyclicLatch是可以复用的。
Semaphore
信号量主要用于控制访问资源的线程个数,常常用于实现资源池,如数据库连接池,线程池...
在Semaphore中,acquire方法用于获取资源,有的话,继续执行(使用结束后,记得释放资源),没有资源的话将阻塞直到有其它线程调用release方法释放资源;
Semaphore翻译成字面意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
主要接口分析
构造器
- Semaphore(int permits)参数permits表示许可数目,即同时可以允许多少线程进行访问
- Semaphore(int permits, boolean fair)这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可
主要方法
- acquire()用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。
- acquire(int permits)获取permits个许可
- release()用来释放许可。注意,在释放许可之前,必须先获获得许可。
- release(int permits)释放permits个许可
这4个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:
- tryAcquire()尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
- tryAcquire(long timeout, TimeUnit unit)尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
- tryAcquire(int permits)尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
- tryAcquire(int permits, long timeout, TimeUnit unit) 尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
另外还可以通过availablePermits()方法得到可用的许可数目。
代码
拿停车位举例,有10个司机想要停车,但是总共只有5个停车位。
private static final int DRIVER_COUNT = 10;
private static final int PARKING_SPOT_COUNT = 5;
private static ExecutorService threadPool = Executors.newFixedThreadPool(DRIVER_COUNT);
private static Semaphore semaphore = new Semaphore(PARKING_SPOT_COUNT);
public static void main(String[] args) {
for (int i = 0; i < DRIVER_COUNT; i++) {
threadPool.execute(new Driver(String.valueOf(i), semaphore));
}
threadPool.shutdown();
}
static class Driver implements Runnable {
private String id;
private Semaphore semaphore;
public Driver(String id, Semaphore semaphore) {
this.id = id;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(this.id + "号司机正在使用车位");
Thread.sleep(1000);
semaphore.release();
System.out.println(this.id + "号司机离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果
下面对上面说的三个辅助类进行一个总结:
CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:
CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。
Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。
参考
https://blog.csdn.net/itmyhome1990/article/details/74971388
https://cloud.tencent.com/developer/article/1181493
http://www.jasongj.com/java/thread_communication/