(一)等待多线程完成的CountDownLatch
CountDownLatch允许一个或多个线程等待其他线程完成操作。
假如有
这样
一个需求:我
们
需要解析一个
Excel
里多个
sheet
的数据,此
时
可以考
虑
使用多线
程,每个
线
程解析一个
sheet
里的数据,等到所有的
sheet
都解析完之后,程序需要提示解析完
成。在
这
个需求中,要
实现
主
线
程等待所有
线
程完成
sheet
的解析操作,最
简单
的做法是使用
join()
方法。
1.使用
join()
方法控制主线程最后执行
/**
* sheet2 解析完成
* sheet1 解析完成
* sheet3 解析完成
* 所有sheet解析完成
*/
public class JoinCountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
Thread t1 =new Thread(()->{
System.out.println("sheet1 解析完成");
});
Thread t2 =new Thread(()->{
System.out.println("sheet2 解析完成");
});
Thread t3 =new Thread(()->{
System.out.println("sheet3 解析完成");
});
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("所有sheet解析完成");
}
}
原理:join用于让当前执行线程等待join线程执行结束。其实现原理是不停检查join线程是否存 活,如果join线程存活则让当前线程永远等待。
2.使用CountDownLatch并发工具类控制主线程最后执行
/**
* Thread-0: 解析完成
* Thread-2: 解析完成
* Thread-1: 解析完成
* 所有sheet解析完成
*/
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
//1.计数器初始值为3
CountDownLatch countDownLatch=new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+": 解析完成");
try {
Thread.sleep(300);
//2.每次减一
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
//3.await方法 会阻塞当前线程,直到N变成零
countDownLatch.await();
System.out.println("所有sheet解析完成");
}
}
注意:计数器必须大于等于0,只是等于0时候,计数器就是零,调用await方法时不会 阻塞当前线程。CountDownLatch不可能重新初始化或者修改CountDownLatch对象的内部计数 器的值。一个线程调用countDown方法happen-before,另外一个线程调await方法。
(二)同步屏障CyclicBarrier
CyclicBarrier
的字面意思是可循
环
使用(
Cyclic
)的屏障(
Barrier
)。它要做的事情是,
让
一组线
程到达一个屏障(也可以叫同步点)
时
被阻塞,直到最后一个
线
程到达屏障
时
,屏障才会
开
门
,所有被屏障
拦
截的
线
程才会
继续
运行。
CyclicBarrier
默
认
的构造方法是
CyclicBarrier
(
int parties
),其参数表示屏障
拦
截的
线
程数量,每个
线
程
调
用
await
方法告
诉
CyclicBarrier
我已
经
到达了屏障,然后当前
线
程被阻塞。
1.
CyclicBarrier
(
int parties
)构造方法的使用
public class CyclicBarrierTest {
public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
//1.参数2表示屏障拦截的线程数量
CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
new Thread(()->{
try {
//2.子线程调用await方法告诉诉CyclicBarrier我已经到达了屏障
cyclicBarrier.await();
System.out.println("sheet1");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
//3.主线程调用await方法告诉诉CyclicBarrier我已经到达了屏障
cyclicBarrier.await();
System.out.println("sheet2");
}
}
注意:
一般是主线程优先于子线程执行,但在这里主线程和子线程的调度是由CPU决定的,两个线程都有可能先执行,所以会产生两种输出,第一种:sheet1、sheet2 第二种:sheet2、sheet1。
如果把
new CyclicBarrier(2)
修改成
new CyclicBarrier(3)
,
则
主
线
程和子
线
程会永
远
等待,因
为
没有第三个
线
程
执
行
await
方法,即没有第三个
线
程到达屏障,所以之前到达屏障的两个
线
程都不会
继续执
行。
CyclicBarrier
还
提供一个更高
级
的构造函数
CyclicBarrier
(
int parties
,
Runnable barrier-Action
),用于在
线
程到达屏障
时
,
优
先
执
行
barrierAction
,方便
处
理更复
杂
的
业务场
景。
2. CyclicBarrier的应用场景
CyclicBarrier
可以用于多
线
程
计
算数据,最后合并
计
算
结
果的
场
景。例如,用一个
Excel
保存了用
户
所有
银
行流水,每个
Sheet
保存一个
账户
近一年的每笔
银
行流水,
现
在需要
统计
用
户
的日均
银
行流水,先用多
线
程
处
理每个
sheet
里的
银
行流水,都
执
行完之后,得到每个
sheet
的日
均
银
行流水,最后,再用
barrierAction
用
这
些
线
程的
计
算
结
果,
计
算出整个
Excel
的日均
银
行流
水。
public class BankWaterService implements Runnable {
/*** 创建4个屏障,处理完之后执行当前类的run方法 */
private CyclicBarrier c = new CyclicBarrier(4, this);
/*** 假设只有4个sheet,所以只启动4个线程 */
private Executor executor = Executors.newFixedThreadPool(4);
/*** 保存每个sheet计算出的银流结果 */
private ConcurrentHashMap<String, Integer> sheetBankWaterCount = new ConcurrentHashMap<String, Integer>();
private void count(){
for (int i = 0; i < 4; i++) {
executor.execute(()->{
sheetBankWaterCount.put(Thread.currentThread().getName(),1);
try {
c.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
});
}
}
//到达屏障拦截的线程数量,执行以下线程
@Override
public void run() {
int result = 0;
for(Map.Entry<String, Integer> sheet : sheetBankWaterCount.entrySet()){
result = result+sheet.getValue();
}
System.out.println("result = " + result);
}
public static void main(String[] args) {
BankWaterService bankWaterService = new BankWaterService();
bankWaterService.count();
}
}
3.CyclicBarrier和CountDownLatch的区别
- CountDownLatch是线程组之间的等待,即一个(或多个)线程等待N个线程完成某件事情之后再执行;而CyclicBarrier则是线程组内的等待,即每个线程相互等待,即N个线程都被拦截之后,然后依次执行。
- CountDownLatch是减计数方式,而CyclicBarrier是加计数方式。
- CountDownLatch计数为0无法重置,而CyclicBarrier计数达到初始值,则可以重置。
- CountDownLatch不可以复用,而CyclicBarrier可以复用。
(三)控制并发线程数的Semaphore
Semaphore
(信号量)是用来控制同
时访问
特定
资
源的
线
程数量,它通
过协调
各个
线
程,以保
证
合理的使用公共
资
源。
@Slf4j
public class SemaphoreTest {
private final static int threadCount = 30;
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
//表示允 许3个线程获取许可证,也就是最大并发数是3
final Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
exec.execute(() -> {
try {
// 获取一个许可
semaphore.acquire();
//一次三个线程执行test方法
test(threadNum);
// 释放一个许可
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
});
}
exec.shutdown();
}
private static void test(int threadNum) throws Exception {
log.info(Thread.currentThread().getName()+" is running");
Thread.sleep(10000);
}
}
在代
码
中,
虽
然有
30
个
线
程在
执
行,但是只允
许3
个并
发执
行。
Semaphore
的构造方法Semaphore
(
int permits
)接受一个整型的数字,表示可用的
许
可
证
数量。
Semaphore
(
3
)表示允
许3
个
线
程
获
取
许
可
证
,也就是最大并
发
数是
3
。
Semaphore
的用法也很
简单
,首先
线
程使用
Semaphore
的
acquire()
方法
获
取一个
许
可
证
,使用完之后
调
用
release()
方法
归还许
可
证
。