JDK的并发包里提供了几个非常有用的并发工具类:
- 控制并发线程数的Semaphore
- 等待多线程完成的CountDownLatch
- 同步屏障CyclicBarrier
Semaphore
1. 是什么
Semaphore 字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目,底层依赖AQS的状态State,是在生产当中比较常用的一个工具类。
2.怎么用
2.1构造方法
public Semaphore(int permits)
public Semaphore(int permits, boolean fair)
- permits 表示许可线程的数量
- fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程
2.2重要方法
public void acquire() throws InterruptedException
public void acquire(int permits)
//重载方法,可以设置获取许可证的数量
- 阻塞并等待获取许可,支持中断
public void acquireUninterruptibly()
- 阻塞并等待获取许可,不支持中断
public void release()
public void release(int permits)
//重载方法,可以设置释放许可证的数量
- 释放许可
public boolean tryAcquire()
//尝试获取许可,不会阻塞,获得许可返回true,反之false
public boolean tryAcquire(long timeout, TimeUnit unit)
//重载方法,传入了超时时间。
//如果超时时间为3分钟,则最多等待3分钟
public int availablePermits()
- 这个方法用来查询可用许可证的数量,返回一个整形结果。
public final int getQueueLength()
- 返回正在等待获取许可证的线程数
public final boolean hasQueuedThreads()
- 是否有线程正在等待获取许可
2.3使用场景
资源访问控制,服务限流Hystrix里限流就有基于信号量方式。
比如有一个很慢的方法,我们要控制同一时刻只能最多3个线程去访问。
public class SemaphoreDemo {
static Semaphore semaphore = new Semaphore(3);
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(50);
for (int i = 0; i < 1000; i++) {
service.submit(new Task());
}
service.shutdown();
}
static class Task implements Runnable {
@Override
public void run() {
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿到了许可证,花费2秒执行慢服务");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("慢服务执行完毕," + Thread.currentThread().getName() + "释放了许可证");
semaphore.release();
}
}
}
3.注意点
- 获取和释放的许可证数量尽量保持一致,否则比如每次都获取 2 个但只释放 1 个甚至不释放,那么信号量中的许可证就慢慢被消耗完了,最后导致里面没有许可证了,那其他的线程就再也没办法访问了;
- 在初始化的时候可以设置公平性,如果设置为 true 则会让它更公平,但如果设置为 false 则会让总的吞吐量更高。
- 信号量是支持跨线程、跨线程池的,而且并不是哪个线程获得的许可证,就必须由这个线程去释放。事实上,对于获取和释放许可证的线程是没有要求的,比如线程 A 获取了然后由线程 B 释放,这完全是可以的,只要逻辑合理即可。
4.总结
semaphore主要是通过控制许可证的发放和回收的方式去控制并发量的。
semaphore的功能跟FixedThreadPool非常类似,但semaphore有其不可被替代的原因,因为semaphore是跨线程、跨线程池的,而FixedThreadPool做不到跨线程池限制。比如,在大型应用程序中,调用方可能是tomcat服务器或者网管,那么就只能用semaphore来限制同时访问的数量。
CountDownLatch
1.是什么
CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。
例如,去游乐场的玩过山车,假如座位还没坐满,管理员会等到座位坐满才开始,这样的话可以一定程度上节约游乐园的成本。
CountDownLatch的核心思想就是等到一个设定的数值到达之后,才能运行下去。
2.怎么用
2.1构造方法
public CountDownLatch(int count){};
//CountDownLatch countDownLatch=new CountDownLatch(10);
- count是需要倒数的数值. 底层为AQS的state,setState(10)
2.2重要方法
await();
//countDownLatch.await();
- 调用await()方法的线程开始等待,直到count的值为0才会继续执行
await(long timeout,TimeUnit unit);
//countDownLatch.await(5, TimeUnit.SECONDS);
- 此方法为await的重载方法,里面会传入超时参数,作用跟await()方法类似,但是这里可以设置超时时间,如果超时时间到了就不在等待。
countDown();
//countDownLatch.countDown();
- 把数值count-1,直到减为0时,之前等待的线程被唤醒。
2.3使用场景:
-
一个线程等待其他多个线程都执行完毕,再继续自己的工作
比如:跑步比赛中,裁判要等所有运动员都跑完,才会宣布比赛结束。
public class RunDemo1 { public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(5); ExecutorService service = Executors.newFixedThreadPool(5); for (int i = 0; i < 5; i++) { final int no = i + 1; Runnable runnable = new Runnable() { @Override public void run() { try { Thread.sleep((long) (Math.random() * 10000)); System.out.println(no + "号运动员完成了比赛。"); } catch (InterruptedException e) { e.printStackTrace(); } finally { latch.countDown(); } } }; service.submit(runnable); } System.out.println("等待5个运动员都跑完....."); latch.await(); System.out.println("所有人都跑完了,比赛结束。"); } }
-
多个线程等待某一个线程的信号,同时开始执行
比如:跑步比赛中,所有的运动员要先准备完毕,听到裁判的发令枪才开始跑步。
public class RunDemo2 { public static void main(String[] args) throws InterruptedException { System.out.println("运动员有5秒的准备时间"); CountDownLatch countDownLatch = new CountDownLatch(1); ExecutorService service = Executors.newFixedThreadPool(5); for (int i = 0; i < 5; i++) { final int no = i + 1; Runnable runnable = new Runnable() { @Override public void run() { System.out.println(no + "号运动员准备完毕,等待裁判员的发令枪"); try { countDownLatch.await(); System.out.println(no + "号运动员开始跑步了"); } catch (InterruptedException e) { e.printStackTrace(); } } }; service.submit(runnable); } Thread.sleep(5000); System.out.println("5秒准备时间已过,发令枪响,比赛开始!"); countDownLatch.countDown(); } }
3.注意点
CountDownLatch是不能够重用的,比如已经完成了倒数,那这个CountDownLatch的作用就已经失效了,不能用于下次重新倒数。你可以使用CyclicBarrier或者创建一个新的CountDownLatch实例。
4.总结
CountDownLatch 类在创建实例的时候,需要在构造函数中传入倒数次数,然后由需要等待的线程去调用 await 方法开始等待,而每一次其他线程调用了 countDown 方法之后,计数便会减 1,直到减为 0 时,之前等待的线程便会继续运行。
CyclicBarrier
1.是什么
CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
2.怎么用
2.1构造方法
CyclicBarrier(int parties)
- parties表示屏障拦截的线程数
public CyclicBarrier(int parties, Runnable barrierAction)
- 当parties线程到达屏障时,继续往下执行之前会优先执行barrierAction
- 这个barrierAction每个周期只会执行一次
2.2重要方法
cyclicBarrier.await();
- 告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
cyclicBarrier.getNumberWaiting()
- 获取cyclicBarrier阻塞的线程数量
cyclicBarrier.isBroken()
- 返回阻塞的线程是否被中断
2.3使用场景
阻塞某一个线程,直到某个预设的条件达成,再统一出发
比如:公园里的三座自行车需要三个人才能骑,那么就需要凑满三个人才能一起骑一辆。
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
for (int i = 0; i < 6; i++) {
new Thread(new Task(i + 1, cyclicBarrier)).start();
}
}
static class Task implements Runnable {
private int id;
private CyclicBarrier cyclicBarrier;
public Task(int id, CyclicBarrier cyclicBarrier) {
this.id = id;
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println("游客" + id + "现在从大门出发,前往自行车驿站");
try {
//随机到达的时间
Thread.sleep((long) (Math.random() * 10000));
System.out.println("游客" + id + "到了自行车驿站,开始等待其他人到达");
cyclicBarrier.await();
System.out.println("游客" + id + "开始骑车");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
3.注意点
CyclicBarrier与CountDownLatch的区别
1.CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景。
2.CyclicBarrier可以设置barrierAction,而CountDownLatch没有类似的执行动作。