为什么需要并发控制
多个线程并发执行时候,在默认情况下CPU是随机切换线程的,不受我们程序员控制的。有时候我们希望CPU能按照我们想要的规律执行线程,此时就需要线程之间的协调控制。
如何实现并发控制
- CountDownLatch
- CyclicBarrier
- Condition
- Semaphore
- Pharse
CountDownLatch倒计数器
CountDownLatch是一个非常实用的多线程控制工具类,通常用来控制线程等待,让某一个线程等待倒计数结束再开始执行。
主要方法
- 构造方法 CountDownLatch(int count) 构造一个用给定计数初始化的 CountDownLatch
- await() 调用该方法的线程会被挂起,等到count值减到0时才继续执行,除非线程被中断。
- await(long timeout, TimeUnit unit)使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。
- countDown():每调用一次countDown(),会将count值减1,直到减为0时,调用await的线程会被唤醒
用法场景
- 用法一:一个线程等待多个线程都执行完后再继续执行。 比如:我们用他来模拟拼夕夕上的拼单过程。假如某个商品需要5个用户参与拼单购买才能生成订单,我们用CountDownLatch来实现。
public class CountDownLatchDemo {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(5);
ExecutorService service = Executors.newFixedThreadPool(5);
for(int i = 0; i < 5; i++){
final int userId = i+1;
service.submit(() -> {
try {
Thread.sleep((long) (Math.random() * 1000));
System.out.println(String.format("[用户%s] 加入了拼单...", userId));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
}
System.out.println("等待拼单.....");
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("完成拼单.....");
}
}
等待拼单.....
[用户3] 加入了拼单...
[用户2] 加入了拼单...
[用户1] 加入了拼单...
[用户4] 加入了拼单...
[用户5] 加入了拼单...
完成拼单.....
- 用法二:多个线程等待某一个线程的信号,同时开始执行
public class CountDownLatchDemo2 {
public static void main(String[] args) {
CountDownLatch begin = new CountDownLatch(1);
ExecutorService service = Executors.newFixedThreadPool(5);
for(int i = 0; i < 5; i++){
final int userId = i+1;
service.submit(() -> {
try {
System.out.println(String.format("[用户%s] 加入了秒杀抢购...", userId));
begin.await();
System.out.println(String.format("[用户%s] 开始秒杀...", userId));
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
service.shutdown();
try {
TimeUnit.SECONDS.sleep(5);
System.out.println("秒杀开始!!!");
begin.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
[用户5] 加入了秒杀抢购...
[用户4] 加入了秒杀抢购...
[用户3] 加入了秒杀抢购...
[用户1] 加入了秒杀抢购...
[用户2] 加入了秒杀抢购...
秒杀开始!!!
[用户3] 开始秒杀...
[用户5] 开始秒杀...
[用户4] 开始秒杀...
[用户1] 开始秒杀...
[用户2] 开始秒杀...
这种多等一的场景,可用于做压力测试。
使用总结:创建CountDownLatch的时候,需要传入倒数次数,哪个线程需要等待,就让那个线程中调用CountDownLatch的await()方法挂起,哪个线程倒数,就在那个线程中调用CountDownLatch的countDown()来减1。三个方法结合起来,当countDown到0时,之前调用await的线程就会被触发执行。从而控制并发。
- CountDownLatch不能回滚重置
CyclicBarrier循环栅栏
CyclicBarrier翻译为循环栅栏,它允许一组线程互相等待,直到到达某个公共屏障点。这样看来CyclicBarrier和CountDownLatch很类似,都能阻塞一组线程。
主要方法
-
CyclicBarrier(int parties, Runnable barrierAction) 创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。
-
await() 在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。
代码示例
用CyclicBarrier模拟集五福
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, ()->{
System.out.println(Thread.currentThread().getName());
System.out.println("五福集齐,开始合成!");
});
String[] blessings = {"爱国福","友善福","敬业福","和谐福","富强福"};
for (int i = 0; i < 5; i++) {
final int index = i;
new Thread(()->{
System.out.println(
String.format("%s送你%s",
Thread.currentThread().getName(),
blessings[index]));
try {
Thread.sleep(index * 100);
//每个线程到达此处等待其他线程到达。五个都到达后,先由最后到达的线程执行构造函数中的线程,再继续往下执行
cyclicBarrier.await();
System.out.println("五福到!合成!!!"+blessings[index]+"消失");
// 验证计数器是可以重置
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
==注意看控制台输出,先是五个线程分别到cyclicBarrier.await() 屏障点前,他们互相等待...收集到五福,即调用了五次await(),然后由最后一个线程Thread-4来执行构造函数中指定的屏障操作,不是main线程. 然后继续执行线程各自的操作。==
==而第二次我又调用了await方法。发现我们设置的cyclicBarrier(5, runnable)还能继续使用。==
Thread-2送你敬业福
Thread-4送你富强福
Thread-1送你友善福
Thread-0送你爱国福
Thread-3送你和谐福
Thread-4
五福集齐,开始合成!
五福到!合成!!!富强福消失
五福到!合成!!!敬业福消失
五福到!合成!!!爱国福消失
五福到!合成!!!友善福消失
五福到!合成!!!和谐福消失
Thread-3
五福集齐,开始合成!
与CountDownLatch的不同:CountDownLatch是做减法,在倒数到0,才唤醒await的线程,而且不能重复使用;而CyclicBarrier是加法,而且可以循环使用。
Semaphore信号量
Semaphore 翻译为信号量,是用来控制同时访问某个特定资源的操作数量,或者同时执行某个操作的数量。
主要方法
- 初始化Semaphore并指定许可证数量
- 得到许可证。在需要被限制访问次数的方法前调用 acquire()/acquireUninterruptibly()
- 释放许可证release()
- tryAcquire(),如果有空余的许可证,就获取。没有的话,也不会阻塞,可以去做别的事,过一会儿再来看是否有空余的。rtyAcquire(timeout)和tryAcquire()一样的,但是多了超时时间,指的是在timeout时间内或不到的话,就去做别的事。
简单使用
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
service.submit(() ->{
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"获取到许可证!");
//假设这是我们需要限流执行的方法 我们用线程睡眠代替
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
semaphore.release();
System.out.println(Thread.currentThread().getName()+"释放了许可证!");
}
});
}
service.shutdown();
}
}
注意点
- Semaphore的acquire(int permits)和release(int permits)可以看到,一次可以获取和释放==多个==许可证,但要注意获取和释放的数量要保持一致。
Condition 接口
Condition是一个接口,它用来替代传统的在synchronized的方法中调用Object的wait()、notify()实现线程间的协作。 Condition 实例实质上需要绑定到一个锁上,我们需要调用Lock.newCondition()来获取,必须在lock.lock()和lock.unlock之间使用condition的signal()与signalAll()。
主要方法
- await() 当调用condition.await()方法后会使得当前获取lock的线程进入到等待队列。 直至被signal/signalAll唤醒,从等待队列移动到同步队列。
- signal() 因为等待队列是一个FIFO的,只会唤起那个等待时间最长的线程。
- signalAll()会唤起所有正在等待的线程。
用Condition来实现生产者消费者模式
public class ConditionDemo {
private static int QUEUE_MAX_SIZE = 10;
private Queue<Integer> queue = new ArrayDeque<Integer>(QUEUE_MAX_SIZE);
private Lock lock = new ReentrantLock();
private Condition full = lock.newCondition();
private Condition empty = lock.newCondition();
public static void main(String[] args) {
ConditionDemo demo = new ConditionDemo();
new Thread(demo.new Consumer()).start();
new Thread(demo.new Producer()).start();
}
class Consumer implements Runnable {
@Override
public void run() {
while (true) {
try {
lock.lock();
while(queue.size()==0){
System.out.println("队列空,等待生产者生产数据");
empty.await();
}
queue.poll();
full.signal();
System.out.println(String.format("消费者消费了一个元素,剩余%s个元素",
queue.size()));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
private class Producer implements Runnable {
@Override
public void run() {
while (true) {
try{
lock.lock();
while(queue.size() == QUEUE_MAX_SIZE){
System.out.println("队列已满,等待消费者来消费");
full.await();
}
queue.offer(1);
empty.signal();
System.out.println(String.format("生产者添加了一个元素,剩余%s个元素",
queue.size()));
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();
}
}
}
}
}
Phaser 相位器
Phaser (我在工作中很少见到多使用的) 它是JDK1.7版本中新加的成员,它的功能与 CountDownLatch、CyclicBarrier类似,但是使用起来更加灵活强大。它可以实现====控制多线程分阶段共同完成任务====的情景问题。
主要方法
- Phaser()/Phaser(int parties) 构建一个Phaser /创建一个指定屏障数量的Phaser,与CountDownLatch一样,传入同步的线程数。
- register()/bulkRegister(int parties) 新注册一个party/批量注册多个party,这是比CountDownLatch强大之处,可以动态注册。
- arrive() 到达此phaser的屏障点,完成该阶段,但不等待其他线程。
- arriveAndAwaitAdvance() 到达此phaser的屏障点,完成该阶段,并且阻塞等待其他线程到达此屏障点。
- arriveAndDeregister() 到达phaser的屏障点,使phaser的到达的线程数加一,取消一个屏障点的注册,不会阻塞等待其他线程。
- 其他方法参见jdk文档
举个栗子
还是模拟拼单购物的场景,我们把拼购的过程假设为一个线程。这个线程分两个阶段,step1:加入拼单,step2:支付。假设五个用户参与拼购,即五个线程相互等待彼此完成step1,才能去完成step2。
public class PhaserDemo {
public static void main(String[] args) {
PhaserDemo demo = new PhaserDemo();
Phaser phaser = new Phaser();
phaser.bulkRegister(5);
ExecutorService service = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
final int userId = i + 1;
service.submit(demo.new BuyTogeter(userId, phaser));
}
service.shutdown();
}
class BuyTogeter implements Runnable {
private Phaser phaser;
private int userId;
public BuyTogeter(int userId, Phaser phaser) {
this.userId = userId;
this.phaser = phaser;
}
@Override
public void run() {
try {
// 第0阶段— 屏障 -等待5 个用户加上拼单,只有五个用户拼单完成,才能支付
Thread.sleep((long) (Math.random() * 1000));
System.out.println(String.format("第%s阶段:[用户%s] 加入了拼单...",phaser.getPhase(), userId));
phaser.arriveAndAwaitAdvance();
// 第1阶段 -等待5 个用户支付
Thread.sleep((long) (Math.random() * 1000));
System.out.println(String.format("第%s阶段:[用户%s] 完成支付...",phaser.getPhase(), userId));
phaser.arriveAndDeregister();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
我们看到控制台输出
第0阶段:[用户2] 加入了拼单...
第0阶段:[用户1] 加入了拼单...
第0阶段:[用户3] 加入了拼单...
第0阶段:[用户5] 加入了拼单...
第0阶段:[用户4] 加入了拼单...
第1阶段:[用户2] 完成支付...
第1阶段:[用户5] 完成支付...
第1阶段:[用户3] 完成支付...
第1阶段:[用户1] 完成支付...
第1阶段:[用户4] 完成支付...
总结
CountDownLatch/CyclicBarrier/Semaphore/Phaser 他们在用法上大同小异,我们在使用中注意他们的区别,当我们需要控制线程等待,相互等待,限流,分阶段等待执行时候,灵活运用这些工具类。
以上对线程并发控制工具类做了一些简单的入门介绍,并没有从源码层分析他们的实现原理,以后会去做源码的分析,欢迎关注。
欢迎扫描二维码关注公众号