概述
在JDK的并发包里提供了几个非常有用的并发工具类。CountDownLatch、CyclicBarrier和Semaphore工具类提供了一种并发流程控制的手段
CountDownLatch
如何能够保证T2在T1执行完后执行,T3在T2执行完后执行?
join方法
可以使用join方法解决这个问题。比如在线程A中,调用线程B的join方法表示的意思就是: A等待B线程执行完毕后(释放CPU执行权),在继续执行。
public class RunnableJob {
public static void main(String[] args) throws InterruptedException {
Worker runnableJob = new Worker();
Thread t1 = new Thread(runnableJob, "T1");
Thread t2 = new Thread(runnableJob, "T2");
Thread t3 = new Thread(runnableJob, "T3");
t1.start();
//这里就是在main主线程中,调用t1线程的join方法。
//也就是main主线程要等待t1执行完成后才能继续往下执行
t1.join();
t2.start();
t2.join();
t3.start();
t3.join();
System.out.println("主线程执行完毕----");
}
}
class Worker implements Runnable{
public void run() {
Thread thread = Thread.currentThread();
try {
Thread.sleep(1000);
System.out.println(thread.getName()+"正在执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出:
T1正在执行
T2正在执行
T3正在执行
主线程执行完毕----
CountDownLatch
原理
CountDownLatch是基于 AQS 实现的
CountDownLatch对AQS的共享方式实现为:CountDownLatch 将任务分为N个子线程去执行,将 state 初始化为 N, N与线程的个数一致,N个子线程是井行执行的,每个子线程都在执行完成后 countDown() 1次, state 执行 CAS 操作并减1。在所有子线程都执行完成(state=0)时会unpark()主线程,然后主线程会从 await()返回,继续执行后续的动作。
具体使用
倒计时计数器
CountDownLatch用于某个线程等待其他线程执行完任务再执行,可以被认为是加强版的join()。
public class CountDownLatchTest {
public static void main(String[] args) {
final CountDownLatch countDownLatch = new CountDownLatch(3);
new Thread("T1"){
public void run() {
try {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName()+"正在执行");
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}.start();
new Thread("T2"){
public void run() {
try {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName()+"正在执行");
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}.start();
new Thread("T3"){
public void run() {
try {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName()+"正在执行");
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}.start();
System.out.println("等待三个线程执行完,主线程才能执行");
try {
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行;
//或者等待timeout时间后count值还没变为0的话也会继续执行
countDownLatch.await();
// countDownLatch.await(20000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程执行完毕");
}
}
输出:
等待三个线程执行完,主线程才能执行
T1正在执行
T3正在执行
T2正在执行
主线程执行完毕
调用了await后,主线程被挂起,它会等待直到count值为0才继续执行;因此只影响主线程的执行顺序一定要在T1 T2 T3之后,但T1 T2 T3之间的顺序互不影响
应用场景: 开启多个线程同时执行某个任务,等到所有任务执行完再执行特定操作,如汇总统计结果。
CountDownLatch和join区别
相同点:都能等待一个或者多个线程执行完成操作,比如等待三个线程执行完毕后,第四个线程才能执行
不同点:join能让线程按我们预想的的顺序执行,比如线程1执行完了,线程2才能执行,线程2执行完,线程3才能执行,但是CountDownLatch就做不到.
当调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法会阻塞当前线程,直到N变为零(也就是线程都执行完了),由于countDown方法可以用在任何地方,所以这里说的N个点,可以是N个线程,也可以是1个线程里的N个执行步骤。用在多线程时,只需把这个CountDownLatch的引用传递到线程中即可
CyclicBarrier
循环屏障
用法:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续执行。也就是说,一组线程互相等待到某个状态,然后这组线程再同时执行。
这个屏障之所以用循环修饰,是因为在所有的线程释放彼此之后,这个屏障是可以重新使用的(reset()方法重置屏障点)。这一点与CountDownLatch不同。
用法
构造函数
//parties表示屏障拦截的线程数量
public CyclicBarrier(int parties) {
}
//意思是在线程到达屏障时,优先执行barrierAction线程,再执行
public CyclicBarrier(int parties, Runnable barrierAction) {
}
举例:
public class CyclicBarrierTest {
// 请求的数量
private static final int threadCount = 10;
// 需要同步的线程数量
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
Thread.sleep(1000);
threadPool.execute(() -> {
try {
test(threadNum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
}
public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
System.out.println("threadnum:" + threadnum + "is ready");
try {
/**等待60秒,保证子线程完全执行结束*/
cyclicBarrier.await(60, TimeUnit.SECONDS);
} catch (Exception e) {
System.out.println("-----CyclicBarrierException------");
}
System.out.println("threadnum:" + threadnum + "is finish");
}
}
输出:
threadnum:0is ready
threadnum:1is ready
threadnum:2is ready
threadnum:3is ready
threadnum:4is ready
threadnum:4is finish
threadnum:3is finish
threadnum:2is finish
threadnum:1is finish
threadnum:0is finish
threadnum:5is ready
threadnum:6is ready
...
显然,循环屏障是可以重用的。
但是,如果以上的threadCount = 4,那么就会永远等待,因为 new CyclicBarrier(5)需要等待5个线程执行完毕,但是没有第5个线程执行await()方法,既没有第5个线程到达屏障,那么之前的线程达到屏障时都不会往下执行了
应用场景:
CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。例如,用一个Excel保存了用户所有银行流水,每个Sheet保存一个账户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction根据这些线程的计算结果,计算出整个Excel的日均银行流水。
CyclicBarrier和CountDownLatch区别
- CyclicBarrier 和 CountDownLatch 都能够实现线程之间的等待
- CountDownLatch简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用countDown()方法发出通知后,当前线程才可以继续执行。
- cyclicBarrier是所有线程都进行等待,直到所有线程都准备好进入await()方法之后,所有线程同时开始执行!
- CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
Semaphore
信号量
Semaphore类似于锁,它用于控制同时访问特定资源的线程数量,控制并发线程数。
public class SemaphoreDemo {
public static void main(String[] args) {
final int N = 7;
Semaphore s = new Semaphore(3);
MyRunnable myRunnable = new MyRunnable(s);
for (int i = 0; i < N; i++) {
new Thread(myRunnable, "Thread" + i).start();
}
}
static class MyRunnable implements Runnable {
private Semaphore s;
public MyRunnable(Semaphore s) {
this.s = s;
}
@Override
public void run() {
try {
s.acquire();//获取许可证
System.out.println("worker" + Thread.currentThread().getName() + " 开始下载");
Thread.sleep(1000);
System.out.println("worker" + Thread.currentThread().getName() + " 下载完毕");
s.release();//释放许可证
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
输出:
workerThread0 开始下载
workerThread2 开始下载
workerThread1 开始下载
workerThread0 下载完毕
workerThread2 下载完毕
workerThread3 开始下载
workerThread1 下载完毕
workerThread5 开始下载
workerThread4 开始下载
workerThread5 下载完毕
workerThread6 开始下载
workerThread3 下载完毕
workerThread4 下载完毕
workerThread6 下载完毕
可以看出并非按照线程访问顺序获取资源的锁
应用场景:
Semaphore可以用于做流量控制,特别是公共资源有限的应用场景,比如数据库连接。
假如有一个需求要读取几万个文件的数据,因为都是IO密集型任务,可以启动几十个线程并发地读取,读到内存中后,还需要存储到数据库中,而数据库的连接数只有10个,那么就可以控制这几十个线程只有10个线程同时获取数据库连接来保存数据。这个时候,就可以使用Semaphore来做流量控制。
Exchanger
交换器
Exchanger是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchanger方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
应用场景:
Exchanger可用于校对工作,比如我们需要将纸质银行流水通过人工的方式录入成电子银行流水,为了避免错误,采用AB岗两人进行录入,录入到Excel之后,系统需要加载这两个Excel,并对两个Excel数据进行校对,看看是否录入一致。
public class ExchangerTest {
private static final Exchanger<String> exchanger=new Exchanger<>();
private static ExecutorService threadPool = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
threadPool.execute(new Runnable() {
@Override
public void run() {
String a = "银行流水A";
try {
a=exchanger.exchange(a);
System.out.println("交换后,a="+a);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadPool.execute(new Runnable() {
@Override
public void run() {
String b = "银行流水B";
try {
b=exchanger.exchange(b);
System.out.println("交换后,b="+b);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadPool.shutdown();
}
}
输出:
交换后,a=银行流水B
交换后,b=银行流水A
如果两个线程有一个没有执行exchange()方法,则会一直等待,如果担心有特殊情况发生,避免一直等待,可以用exchange(V x, long timeout, TimeUnit unit)设置最大等待时长。