转载请标明出处:
http://blog.csdn.net/hai_qing_xu_kong/article/details/72354208
本文出自:【顾林海的博客】
前言
关于线程的基础知识可以查看《有关线程的相关知识(上)》和《有关线程的相关知识(下)》,线程同步synchronized和Lock可以查看《线程同步synchronized》和《线程同步Lock》,在并发工具类中提供了更加高级的同步机制来实现多线程间的同步,本篇文章就来讲解这些高级的同步机制如何使用。
CountDownLatch
CountDownLatch类是一个同步辅助类。CountDownLatch提供了一个带整型参数的构造器,这个整数就是用于线程要等待完成的操作的数目。当一个线程要等待某些操作先完成时,可以调用await()方法,这个方法让线程进入休眠直到等待的所有操作都完成,当某个操作完成后,调用countDown()方法将CountDownLatch类的内部计数器减1,当计数器为0时,CountDownLatch类将唤醒所有调用await()方法而进入休眠的线程。下面通过一个例子来展示CountDownLatch的用法。
public class Task implements Runnable {
private CountDownLatch startCountDownLatch;
private CountDownLatch endCountDownLatch;
public Task(CountDownLatch start,CountDownLatch end) {
this.startCountDownLatch=start;
this.endCountDownLatch=end;
}
@Override
public void run() {
try {
System.out.println("堂主:老大,有何吩咐...");
TimeUnit.SECONDS.sleep(2);
/*
* 当startCountDownLatch内部计数为0时,
* 将唤醒所有调用startCountDownLatch.wait()方法
* 当线程,并继续执行下去。
*/
startCountDownLatch.await();
System.out.println("堂主:老大,我们分堂已经完成...");
TimeUnit.SECONDS.sleep(2);
endCountDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
创建一个实现了Runnable接口的Task类,内部定义两个CountDowanLatch实例,并通过构造器初始化,重写了run()方法,我们在run()方法中,可以
看到在调用了startCountDownLatch.await()时,如果startCountDownLatch内部计数器不为0,该线程就会进入休眠状态,在此之前会输出”堂主:老大,有何吩咐…”。如果startCountDownLatch内部计数器为0,就会唤醒该线程,继续执行下去并输出”堂主:老大,我们分堂已经完成…”,接着调用endCountDownLatch.countDown()方法,使得endCountDownLatch内部计数器减1,当endCountDownLatch内部计数器为0时,就会唤醒endCountDownLatch调用await()方法所在的线程。
public class Client {
public static void main(String[] args) {
CountDownLatch start=new CountDownLatch(1);
CountDownLatch end=new CountDownLatch(3);
Task task=new Task(start, end);
ExecutorService executor=Executors.newFixedThreadPool(3);
for(int i=0;i<3;i++){
executor.execute(task);
}
System.out.println("老大吩咐三个堂主做任务...");
try {
TimeUnit.SECONDS.sleep(2);
start.countDown();
System.out.println("剩下的就等消息了...");
end.await();
System.out.println("老大:大家完成的不错...");
executor.shutdownNow();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在主类中,定义了两个CountDownLatch实例,并给start指定了计数器起始为1,end计数器起始为3,最后将这两个实例通过 实例化Task时传入,接下来创建线程池来执行3个线程,从上面代码可以看出,当主线程的start.countDown调用时,由于start在初始化时就已经指定了计数器为1,因此这里执行了countDown()方法后,所有调用了await()方法的线程将被唤醒。接着又调用了end的await()方法,这时主线程处于休眠状态,除非end内部计数器为0,才会继续执行下去,在上面的代码中end在初始化时就已经指定了计数器为3,而上面代码中开启了3个线程并在这三个线程中调用了countDown()方法,正好执行了3此,end内部计数器为0。
最后我们看看输出结果:
老大吩咐三个堂主做任务…
堂主:老大,有何吩咐…
堂主:老大,有何吩咐…
堂主:老大,有何吩咐…
剩下的就等消息了…
堂主:老大,我们分堂已经完成…
堂主:老大,我们分堂已经完成…
堂主:老大,我们分堂已经完成…
老大:大家完成的不错…
CyclicBarrier
CyclicBarrier称为同步屏障,也是一个同步辅助类,允许一组线程彼此互相等待,直到到达某个公共的屏障点,此屏障在等待线程被释放之后可重用,所以称它为可循环使用的屏障。
CyclicBarrier类使用一个整型数进行初始化,这个数是需要在某个点上同步的线程数,当一个线程到达指定的点后,调用await()方法等待其他的线程,当线程调用await()方法后,CyclicBarrier类将阻塞这个线程并使之休眠知道所有其他线程到达。
CyclicBarrier可以传入一个Runnable对象作为初始化参数,当所有线程都到达集合点之后,CyclicBarrier类将这个Runnable对象作为线程执行。
public class Worker implements Runnable {
private String taskName;
private CyclicBarrier cyclicBarrier;
public Worker(String name,CyclicBarrier barrier) {
this.taskName=name;
this.cyclicBarrier=barrier;
}
@Override
public void run() {
try {
work();
cyclicBarrier.await();
other();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
public void work(){
System.out.println(taskName+"正在执行...");
}
public void other(){
System.out.println(taskName+"执行完毕,接下来执行其他任务...");
}
}
创建了一个实现Runnable接口的Worker类,定义了一个taskName用以表示工作名称,又创建了一个CyclicBarrier对象,并在构造器中初始化,在run()方法中,先是执行work()方法,接着调用屏障CyclicBarrier的await()方法,等待其他线程的到达,当所有线程到达后,执行other()方法。
public class Client {
public static void main(String[] args) {
// 模拟创建十个任务
ArrayList<String> taskList = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
taskList.add("任务" + i);
}
CyclicBarrier barrier = new CyclicBarrier(taskList.size(), new Runnable() {
@Override
public void run() {
System.out.println("10个任务都执行完毕了...");
}
});
for (int i = 0, length = taskList.size(); i < length; i++) {
new Thread(new Worker(taskList.get(i), barrier)).start();
}
}
}
在主类中,定义了10个任务,并创建了一个CyclicBarrier对象,在实例化CyclicBarrier时,指定了同步线程数为10,以及一个Runnable对象,当所有调用CyclicBarrier的await()方法的线程到达集合点之后,就会将Runnable对象作为线程来执行。最后创建开启10个线程。
输出结果:
任务2正在执行…
任务5正在执行…
任务4正在执行…
任务3正在执行…
任务1正在执行…
10个任务都执行完毕了…
任务1执行完毕,接下来执行其他任务…
任务2执行完毕,接下来执行其他任务…
任务4执行完毕,接下来执行其他任务…
任务5执行完毕,接下来执行其他任务…
任务3执行完毕,接下来执行其他任务…
Exchanger
Exchanger是Java并发API提供的一个同步辅助类,允许在并发任务之间交换数据,Exchanger类允许在两个线程之间定义同步点,当两个线程都到达同步点时,它们交换数据结构,因此第一个线程的数据结构进入到第二个线程中,同时第二个线程的数据结构进入到第一个线程中。
下面介绍一下交换器交换的几种方式:
(1)public V exchange(V x) throws InterruptedException
在这个交互点上等待其他线程到达(除非调用线程被中断),之后将所给对象传入其中,接收其他线程的对象作为返回,如果其他的线程已经在交换点上等待,为了线程调度它会从中恢复并且接受调用线程所传入的对象。当前线程会立即返回,接收其他线程传入交换器中的对象。当调用线程被中断,会抛出InterruptedException异常。
(2)public V exchange(V x, long timeout, TimeUnit unit)
throws InterruptedException, TimeoutException
该方法等同于上面的方法,只是在等待时会指定等待时间,一旦超时,会抛出TimeoutException异常。
下面实现一个生产者和消费者的问题。
生产者:
public class Producer implements Runnable{
private List<String> buffer;
private Exchanger<List<String>> exchanger;
public Producer(List<String> _buffer,Exchanger<List<String>> _exchanger){
this.buffer=_buffer;
this.exchanger=_exchanger;
}
@Override
public void run() {
for(int i=0;i<5;i++){
for(int j=0;j<5;j++){
buffer.add("buffer"+j);
}
try {
buffer=exchanger.exchange(buffer);
System.out.println("生产者:交换完毕后缓冲区数据还剩"+buffer.size()+"个");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
消费者:
public class Consumer implements Runnable {
private List<String> buffer;
private Exchanger<List<String>> exchanger;
public Consumer(List<String> _buffer, Exchanger<List<String>> _exchanger) {
this.buffer = _buffer;
this.exchanger = _exchanger;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
buffer = exchanger.exchange(buffer);
System.out.println("消费者:交换完毕后缓冲区数据还剩" + buffer.size() + "个");
buffer.clear();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
实现类:
public class Client {
public static void main(String[] args) {
List<String> buffer1=new ArrayList<>();
List<String> buffer2=new ArrayList<>();
Exchanger<List<String>> exchanger=new Exchanger<>();
Producer producer=new Producer(buffer1, exchanger);
Consumer consumer=new Consumer(buffer2, exchanger);
Thread producerThread=new Thread(producer);
Thread consumerThread=new Thread(consumer);
producerThread.start();
consumerThread.start();
}
}
在具体的场景类中定义了两个buffer分别用于生产者和消费者,定义了Exchanger对象,用来同步生产者和消费者,在生产者线程中,循环5次,每次循环向buffer中添加5个数据,添加完毕后,通过exchange方法将数据传入消费者线程中去,在消费者线程中,循环5次,每次循环都将空的 buffer穿入给生产者线程中,并且拿到生产者线程传入到buffer,在获取生产者线程传入的buffer后,进行消费掉。下图的序列图很好的说明了使用exchange方法生产者线程和消费者线程的数据的同步。
运行程序:
消费者:交换完毕后缓冲区数据还剩5个
生产者:交换完毕后缓冲区数据还剩0个
生产者:交换完毕后缓冲区数据还剩0个
消费者:交换完毕后缓冲区数据还剩5个
生产者:交换完毕后缓冲区数据还剩0个
消费者:交换完毕后缓冲区数据还剩5个
生产者:交换完毕后缓冲区数据还剩0个
消费者:交换完毕后缓冲区数据还剩5个
生产者:交换完毕后缓冲区数据还剩0个
消费者:交换完毕后缓冲区数据还剩5个
运行时输出的顺序可能不一样,但这不妨碍我们验证生产者与消费者的数据同步问题,查看输出结果,可以发现每次生产者的5个数据与消费者的空数据交换后,生产者打印缓冲数为0,在消费者线程中获取生产者数据后打印,发现缓冲数据为5并再次请空缓冲区,这样一直反复(生产者生产数据,消费者消费数据)。
Semaphore(信号量)
信号量是一种计数器,用来保护一个或者多个共享资源的访问。信号量内部的计数器大于0说明有可以使用的资源,当信号量的计数器等于0,信号量将会把线程置入休眠直到计数器大于0。当线程使用完某个共享资源时,信号量必须被释放,以便其它线程能够访问共享资源,释放时信号量计数器增加1。下面给出信号量的相关方法:
(1)public void acquire() throws InterruptedException
从这个信号量中获取一个许可证,否则阻塞直到有一个许可证可用或者调用线程被中断。当调用线程中断,抛出InterruptedException。
(2)public void acquire(int permits) throws InterruptedException
从这个信号量中获取permits数量的许可证,否则阻塞直到这些许可证可用或者调用线程被中断。当调用线程中断,抛出InterruptedException,当permits小于0时,抛出IllegalArgumentException。
(3)public void acquireUninterruptibly()
获取一个许可证,否则阻塞直到有一个许可证可用。
(4)public void acquireUninterruptibly(int permits)
获取permits个许可证,否则阻塞直到这些许可证可用,当permits小于0,抛出IllegalArgumentException。
(5)public int availablePermits()
返回当前可用许可证的数目。该方法适用于调试和测试。
(6)public int drainPermits()
获取并返回立即可用的许可证的数量。
(7)public final int getQueueLength()
返回等待获取许可证的大致线程数。由于线程数在该方法遍历内部数据结构的时候可能会改变,返回的值只能是估算值。
(8)public boolean isFair()
返回公平性设置(公平返回true,不公平返回false)。
(9)public void release()
释放一个许可证,将其返回给信号量。可用许可证的数目增加1。如果任何线程正在尝试获取一个许可证,被选到的线程就会被给予刚刚释放的许可证。那条线程就会因为线程调度而被重新启用。
(10)public boolean tryAcquire()
仅当调用时有一个许可证可用的情况,才能从这个信号量中获取这个许可证。当获取许可证后返回true,否则,立刻返回false。
(11)public boolean tryAcquire(int permits)
仅当调用时有permits个许可证可用的情况,才能从这个信号量中获取这些许可证。当获取到这些许可证后,返回true,否则立即返回false。当permits小于0时,抛出IllegalArgumentException。
(12)public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException
仅当调用时有permits个许可证可用的情况,才能从这个信号量中获取这些许可证。当permits个许可证尚不可用时,调用线程会等待。当全部许可证可用时等待结束。若timeout超时或者调用线程中断,抛出InterruptedException。
(13)public boolean tryAcquire(long timeout, TimeUnit unit)
throws InterruptedException
和tryAcquire(int permits)方法类似,不过调用线程会一直等待直到有一个许可证可用。当许可证可用时,等待结束。若timeout超时或者调用线程中断,抛出InterruptedException。
下面使用信号量来模拟打印机的打印队列:
public class PrintQueue {
//声明一个信号量对象semaphore
private final Semaphore semaphore;
//用于存放打印机的状态
private boolean freePrinters[];
//声明一个锁对象
private Lock lock;
public PrintQueue(){
semaphore=new Semaphore(3);
freePrinters=new boolean[3];
lock=new ReentrantLock();
for(int i=0;i<3;i++){
freePrinters[i]=true;
}
}
public void printJob(String task){
try {
//获取信号量
semaphore.acquire();
//获取打印编号
int assignedPrinter=getPrinter();
//模拟耗时的打印任务
System.out.println("模拟耗时的打印任务..."+new Date());
TimeUnit.SECONDS.sleep(2);
freePrinters[assignedPrinter]=true;
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
semaphore.release();
}
}
private int getPrinter(){
int number=-1;
try{
lock.lock();
for(int i=0,length=freePrinters.length;i<length;i++){
if(freePrinters[i]){
number=i;
//该打印机执行打印任务
freePrinters[i]=false;
break;
}
}
}catch(Exception e){
}finally{
lock.unlock();
}
return number;
}
}
public class Job implements Runnable{
private PrintQueue printQueue;
public Job(PrintQueue _printQueue){
this.printQueue=_printQueue;
}
@Override
public void run() {
printQueue.printJob("打印文档");
}
}
public class Client {
public static void main(String[] args) {
PrintQueue printQueue=new PrintQueue();
Thread thread[]=new Thread[10];
for(int i=0;i<10;i++){
thread[i]=new Thread(new Job(printQueue));
}
for(int i=0;i<10;i++){
thread[i].start();;
}
}
}
上面代码实现了一个打印队列被三个不同当打印机使用,在打印队列类中定义了一个内部计数器为3的信号量,当调用acquire方法的3个线程将获得对临界区的访问,其它线程将被阻塞。
运行结果:
模拟耗时的打印任务…Wed May 17 23:06:38 CST 2017
模拟耗时的打印任务…Wed May 17 23:06:38 CST 2017
模拟耗时的打印任务…Wed May 17 23:06:38 CST 2017
模拟耗时的打印任务…Wed May 17 23:06:40 CST 2017
模拟耗时的打印任务…Wed May 17 23:06:40 CST 2017
模拟耗时的打印任务…Wed May 17 23:06:40 CST 2017
模拟耗时的打印任务…Wed May 17 23:06:42 CST 2017
模拟耗时的打印任务…Wed May 17 23:06:42 CST 2017
模拟耗时的打印任务…Wed May 17 23:06:42 CST 2017
模拟耗时的打印任务…Wed May 17 23:06:44 CST 2017
Phaser
Phaser是一个更加弹性的同步屏障,一个phaser使得一组线程在屏障上等待,在最后一条线程到达之后,这些线程得以继续执行。具体使用请用户查阅相关文档。
总结
CyclicBarrier和CountDownLatch有很多共性,但它们之间有一定的差异。其中最为明显的是CyclicBarrier对象可以被重置回初始化状态,并把它的内部计数器重置成初始化时的值,而CountDownLatch只准许进入一次,一旦CountDownLatch的内部计数器为0,在调用这个方法将不起作用。
使用Semaphore(信号量)时,定义内部计数器只有0和1两个值时,称这种信号量为二进制信号量,这种信号量可以保护对单一共享资源,或者单一临界区的访问,从而使得保护的资源在同一个时间内只能被一个线程访问。上面信号量例子实现了保护一个资源的多个副本。