本系列将梳理一下多线程同步的一些经常用到方法或类,包括有CountDownLatch,CyclicBarriar,join,synchronized,wait/notify/notifyAll,Semaphore,ReentrantLock,Phaser,Future,Exchanger,concurrent等。通过这些方法或类的对比和使用,不仅可以拓宽我们的知识面,提高我们的线程同步处理能力,还可以让我们在面试时可以更从容和淡定。
本节将首先介绍一下CountDownLatch--倒计数锁存器
CountDownLatch的一个特别典型的应用场景是:有一个任务想要继续往下执行,它必须等到其它任务执行完毕后才可以继续往下执行。
1、CountDownLatch是什么?
CountDownLatch是在Java1.5中被引入的一个线程同步类,跟它一起被引入的并发工具类还有CyclicBarrier、Semaphore、ConcurrentHashMap和BlockingQueue等,它们都位在于java.util.concurrent包中。
CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
因此CountDownLatch这个类用作这样的场景,一个线程在等待其他线程完成各自的工作后再执行。例如,应用程序的主线程希望一些初始化参数或者属性的子线程都完成后再执行。
2、基本元素和常用方法
CountDownLatch类有4个基本元素或方法:
初始值。它决定CountDownLatch类需要等待的事件或者线程的数量。
(1)、await() 方法。 被等待全部事件终结的线程调用。
(2)、countDown() 。方法事件在结束执行后调用。
(3)、await(long time, TimeUnit unit)。此方法会休眠直到被中断;
当创建 CountDownLatch 对象时,对象使用构造函数的参数来初始化内部计数器。每次调用 countDown() 方法, CountDownLatch 对象内部计数器减一。当内部计数器达到0时, CountDownLatch 对象唤醒全部使用 await() 方法睡眠的线程们。
CountDownLatch是不可逆的,即不可能重新初始化或者修改CountDownLatch对象的内部计数器的值。一旦计数器的值初始后,唯一可以修改它的方法就是之前用的 countDown() 方法,也就是只能创建CountDownLatch对象,并使其内部值减少,不能重新修改或增加内部值。当计数器到达0时, 全部调用 await() 方法将会立刻返回,后面再调用此CountDownLatch对象的countDown() 方法都将不会产生任何作用。后续会介绍CyclicBarriar,它除了可以实现CountDownLatch的功能外,还可以重新进行初始化,重复使用。
另一种版本的 await() 方法是await(long time, TimeUnit unit),表示CountDownLatch内部计数器到达0或者特定的时间过去了。TimeUnit 类包含了:DAYS, HOURS, MICROSECONDS, MILLISECONDS, MINUTES, NANOSECONDS和 SECONDS等多种类型,在此方法中都可以使用。
3、演示代码
以下是使用了两个CountDownLatch的demo, CountDownLatch begin保证了所有线程都能在相同时间点开始处理。CountDownLatch end保证所有线程执行完毕后,主线程再退出。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountDownLatchDemo {
//赛跑运动员的个数
private static final int RUNNER_AMOUNT = 5;
public CountDownLatchDemo() {
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
//对于每位运动员,裁判员发令枪响了后才开始比赛,只有一次发令枪
CountDownLatch cdl_startgun = new CountDownLatch(1);
//对于整个比赛,所有赛跑人员跑完后才算结束,有RUNNER_AMOUNT个运动员
CountDownLatch cdl_runner = new CountDownLatch(RUNNER_AMOUNT);
Runner[] plays = new Runner[RUNNER_AMOUNT];
for(int i=0;i
plays[i] = new Runner(i+1,cdl_startgun,cdl_runner);
//定义特定的线程池,大小为5,每个线程对应一个运动员
ExecutorService exe = Executors.newFixedThreadPool(RUNNER_AMOUNT);
for(Runner p:plays)
exe.execute(p); //分配线程
System.out.println("Race begins!");
//启动所有运动员的线程后,发令枪发令,cdl_startgun归0
cdl_startgun.countDown();
try{
//开始等待所有运动员结束各自的跑步,只有都跑完后才能继续运行,否则一直处于阻塞状态
cdl_runner.wait(); //等待cdl_runner变为0,即为比赛结束
}catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}finally{
System.out.println("Race ends!");
}
exe.shutdown();
}
}
接下来是Runner类:
import java.util.concurrent.CountDownLatch;
public class Runner implements Runnable {
private int id;
private CountDownLatch cdl_startgun;
private CountDownLatch cdl_runner;
public Runner(int i, CountDownLatch cdl_startgun, CountDownLatch cdl_runner) {
// TODO Auto-generated constructor stub
super();
this.id = i;
this.cdl_startgun = cdl_startgun;
this.cdl_runner = cdl_runner;
}
@Override
public void run() {
// TODO Auto-generated method stub
try{
//等待发令枪响起,一旦发令枪响过,cdl_startgun归0,程序就可以往下执行
cdl_startgun.await(); //等待begin的状态为0
Thread.sleep((long)(Math.random()*100)); //随机分配时间,即运动员完成时间
System.out.println("Play"+id+" arrived.");
}catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}finally{
//本运动员完成跑步,通知cdl_runner减1
cdl_runner.countDown(); //使end状态减1,最终减至0
}
}
}
执行结果可能是这样:
Race begins!
Play2 arrived.
Play4 arrived.
Play3 arrived.
Play5 arrived.
Play1 arrived.
Race ends!