1. 什么是控制并发流程
在我们不控制并发的时候,线程尽可能跑,受线程调度器控制,而不受程序员控制;如果我现在想让一些线程先执行,一些线程在最后执行,就要使用并发流程的工具类,让线程之间相互合作,来满足业务需求;比如让线程A等待线程B执行完再执行等策略
类 | 作用 | 说明 |
---|---|---|
Semaphore | 信号量,可以通过控制许可证的数量来保证线程之间的配合 | 线程只有在拿到许可证后才能继续运行 |
CyclicBarrier | 线程会等待,直到足够多线程达到了事先规定的数目,一旦触发了条件就可以进行下一步动作 | 适合线程之间互相等待处理结果就绪的场景 |
Phaser | 和CyclicBarrier 类似,但是计数可变 | |
CountDownLatch | 和CyclicBarrier 类似,数量递减到0的时候触发动作 | 不可重复使用 |
Exchanger | 让两个线程在合适时交换对象 | 当两个线程工作在同一个类的不同实例上时,用于交换数据 |
Condition | 可以控制线程的等待和唤醒 |
2. CountDownLatch
倒计时门闩
2.1 基本方法
CountDownLatch是一个计数器闭锁,通过它可以完成类似于阻塞当前线程的功能,即:一个线程或多个线程一直等待,直到其他线程执行的操作完成
//仅有一个构造函数,参数为需要倒数的数值
CountDownLatch(int count)
//调用await的线程会被挂起,等待直到count的值为0才继续执行,谁等待谁await
await()
//将count的值减1,直到为0,等待的线程将会被唤醒,谁倒数谁CountDown
CountDown()
2.2 用法① 一等多
一个线程去等待多个线程完成后才执行,假如有这样的场景,在质检线上有五个质检员,有一个产品只有等到这五个质检员都检查完成才会去进行下面的检查,这就可以使用CountDownLatch
完成
public class CountDownDemo {
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(5);
//CountDownLatch
CountDownLatch countDownLatch = new CountDownLatch(5);
for(int i = 0; i<5; i++){
final int no = i+1;
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
//检查
System.out.println("质检员"+(no+1));
Thread.sleep((long)(Math.random()*1000));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}
};
service.submit(runnable);
}
System.out.println("等待质检员检查完");
countDownLatch.await();
System.out.println("检查完了,进入下一个环节");
}
}
2.3 用法② 多等一
多个线程等待统一的信号,然后让他们同时进行,比如模拟双十一的请求,让多个线程等待一个信号然后同时开始访问服务器
下面模拟一个例子:模拟100米跑步,5名选手都准备好了,只等裁判员发令,然后同时起跑
public class CountDownDemo2 {
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(5);
//CountDownLatch
CountDownLatch countDownLatch = new CountDownLatch(1);
for(int i = 0; i<5; i++){
final int no = i+1;
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("准备完毕等待发令");
try {
countDownLatch.await();
System.out.println("运动员"+no+"开始跑步");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
service.submit(runnable);
}
System.out.println("等待裁判员....");
Thread.sleep((long)(Math.random()*1000));
System.out.println("开始");
countDownLatch.countDown();
}
}
2.4 好像用join
也能实现?
让一个线程等待另一些线程执行完毕再执行好像用join
也能达到目的,但是实际并非如此;
如果我们使用线程池来运行任务,任务虽然会结束,但是线程永远不会停止,所以这种情况就不适用与join
2.5 注意点
CounDownLatch
是不可以重用的,如果需要重新计数可以考虑使用CyclicBarrier
或者创建新的CountDownLatch
3. Semaphore
信号量
3.1 基本方法
Semaphore
用来限制或管理有限的资源的使用情况,信号量的作用是为维护一个许可证的计数,线程可以获取许可证,那信号量的许可证的总数就-1,线程也可以释放许可证,那信号量的许可证的总数就+1,当信号量所拥有的许可证的数量为0,接下来想获得许可证的线程就要等待别人释放
我们可以利用它来"限流"
//初始化信号量指定许可证的数量,也可以设置公平与否
new Semaphore(int permits, boolean fair)
//获取,可以中断
acquire()
//可以分配多个许可证,也就相当于现在的线程的权重大,需要的许可证更多
acquire(num)
//获取,不可中断
acquireUninterruptibly()
//尝试获取
tryAcquire()
tryAcquire(timeout)
//归还许可证
release()
//归还多个,对应前面一次获得了多个的情况
release(num)
3.2 基本用法
public class SemaphoreDemo {
static Semaphore semaphore = new Semaphore(3);
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(50);
for(int i = 0; i<50; i++){
executorService.submit(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"拿到许可证");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName()+"释放了许可证");
semaphore.release();
}
}
});
}
}
}
3.3 注意点
① 获取和是释放线程的许可证数量必须一致,否则比如每次获取两个但是只释放一个,随着时间的推移最后许可证数量不够用会导致程序卡死(这不是语法规定,只是编程规范)
② 在初始化的时候最好是公平的,避免饥饿
③ 释放和获取并是跨线程的,并不是必须由获得许可证的线程释放许可证,只要合理,也可以由其他线程释放
④ 信号量除了控制临界区最多可以同时N个线程访问外,另一个作用是实现条件等待,例如线程1需要在线程2完成准备工作再开始工作,那么就可以线程1acquire()
,线程2完成任务后再release()
,这样相当于一个轻量级的CountDownLatch
4. Condition
条件对象
4.1 基本介绍
Condition
是个接口又称为条件对象,假设线程1需要等待某个条件的时候,就去执行condition.await()
方法,进入阻塞状态
然后线程2再执行对应的条件,满足条件后执行condition.signal()
方法唤醒之前阻塞的线程
4.2 基本使用
public class ConditionDemo {
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
void method1(){
lock.lock();
try {
System.out.println("条件不满足,等待");
condition.await();
System.out.println("条件满足继续工作");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
void method2(){
lock.lock();
try {
System.out.println("条件满足,唤醒其他线程");
condition.signalAll();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
ConditionDemo conditionDemo = new ConditionDemo();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
conditionDemo.method2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
conditionDemo.method1();
}
}
4.3 实现生产者消费者模式
public class ConsumerAndProduce {
private int queueSize = 20;//队列的满负荷容量
private PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(queueSize);
private Lock lock = new ReentrantLock();
//队列空了,消费者没法消费
private Condition empty = lock.newCondition();
//队列满了,生产者不能生产
private Condition full = lock.newCondition();
class Consumer extends Thread {
@Override
public void run() {
consume();
}
public void consume() {
while (true) {
lock.lock();
try {
while (priorityQueue.size() == 0) {
try {
empty.await();
} catch (Exception e) {
}
}
priorityQueue.poll();
full.signalAll();
} finally {
lock.unlock();
}
}
}
}
class Produce extends Thread{
@Override
public void run() {
produce();
}
public void produce(){
while (true){
lock.lock();
try {
while (priorityQueue.size() == queueSize){
try {
full.await();
}catch (Exception e){
}
}
priorityQueue.offer(1);
empty.signalAll();
} finally {
lock.unlock();
}
}
}
}
}
4.4 注意点
- 实际上,如果说
Lock
是用来代替synchronized
,那么Condition
就是用来代替对应的Object.wait()/notify()
的 await
方法会自动释放持有的Lock锁,和Object.wait()
一样不需要自己先手动释放锁- 调用
await
的适合,必须持有锁 - 一个lock锁可以持有多个
condition
5. CyclicBarrier
循环栅栏
5.1 基本介绍
CyclicBarrier
和CountDownLatch
很相似,都能阻塞一组线程
当有大量线程相互配合,分别计算不同任务,并且需要最后统一汇总的时候,我们可以使用CyclicBarrier
,CyclicBarrier
可以构造一个集结点,当某一个线程执行完毕,他就会到集结点等待,直到所有的线程都到了集结点,那么该栅栏就会被撤销,所有线程再统一出发继续执行接下来的任务
比如5个小伙伴约好到校门口集合,大家都到了再做接下来的事情
public class CyclicBarrierDmo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("到齐了,做事");
}
});
ExecutorService executorService = Executors.newFixedThreadPool(5);
for(int i = 0; i<5; i++){
final int no = i+1;
executorService.submit(new Runnable() {
@Override
public void run() {
try {
Thread.sleep((long)(Math.random()*1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("小伙伴"+no+"到了");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
});
}
}
}
5.2 CyclicBarrier
和CountDownLatch
① 作用不同:
CyclicBarrier
是等固定数量的线程到达栅栏位置才继续执行,而CountDownLatch
只需要等待数字到0,也就是说CyclicBarrier
是基于线程的,CountDownLatch
是基于事件的
② 可重用性不同:
CountDownLatch
不能重用,CyclicBarrier
可以重用,用完计数回到初始值