JUC中几个常见的并发工具类详解

在JDK的并发包里提供了几个非常有用的并发工具类。

其中CountDownLatch,CyclicBarrier和Semaphore工具类提供了一种并发控制的手段。

而Exchanger工具类则提供了在线程间进行交换数据的一种手段。

1.等待多线程完成的CountDownLatch

CountDownLatch允许一个或多个线程等待其他线程完成操作。

定义抽象难理解,直接上测试代码!

import java.util.*;
import java.util.concurrent.CountDownLatch;

public class Main1 {
	
	//主函数,作为程序测试代码的入口
	public static void main(String args[]) throws Exception
	{
		CountDownLatch countDownLatch = new CountDownLatch(2);	//使用CountDownLatch,注意构造方法的传入参数
		
		//创建两个测试线程,并启动线程
		Thread thread1 = new Thread(new Test(1,countDownLatch));
		Thread thread2 = new Thread(new Test(2,countDownLatch));
		thread1.start();
		thread2.start();
			
		countDownLatch.await();		//CountDownLatch的实现核心,产生关联于计数器的阻塞方式!
		System.out.println("主线程main中打印信息永远在所有子线程完成后!");
	}

}

//编写线程类Test,实现Runnable接口,并实现其线程方法run
class Test implements Runnable 
{
	private int number = 0;
	private CountDownLatch countDownLatch = null;
	
	//基于构造方法,进行线程类成员变量的初始化设置
	public Test(int i,CountDownLatch countDownLatch)
	{
		this.number = i;
		this.countDownLatch = countDownLatch;
	}
	
	//实现Runnable下的接口方法,达到线程的目的
	public void run()
	{
		System.out.println("子线程的打印! " + this.number);
		this.countDownLatch.countDown();
	}
}

程序测试结果,可能有两种情况,分别为:

或者

此时,可以看出,无论两个子线程的执行顺序如何,我们永远保证了main中打印语句在两个线程执行后执行!

具体分析:

CountDownLatch的构造函数接收一个int类型的参数作为计数器,而int具体数值则代表要等待多少个线程点的完成!

例如,程序中初始设置为2,则表示等待的数量为2.

然后就是,countDownLatch的countDown()的方法,每次使用该方法,所维护的计数器数值就减一。

例如,每个子线程的测试逻辑中,都有countdown方法的调用,每次调用,计数器减一。

最后就是,countDownLatch的await()方法,await阻塞于计数器大于一时,并在计数器为0时,结束阻塞!

例如,程序中主线程main中的await方法,只有计数器为0时,才会往下执行!

细节思考:

1.所谓的计数器必须大于或者等于0,只不过当等于0的时候,await方法就会返回,不在阻塞当前线程。

2.countDown()方法,可以用在任何地方,所以,计数器的实际意义,其实是管理countDown()方法的调用次数。通俗来说就是,countDown()方法可以用在多线程中,也可以代表一个线程中的多个执行步骤。

2.同步屏障CyclicBarrier

让一组线程到达一个屏障(也就是所谓的同步点)时被阻塞,知道最后一个线程到达屏障时,屏障才会开门,所有被该屏障所阻塞的线程才能够继续执行。

测试代码如下:

import java.util.*;
import java.util.concurrent.CyclicBarrier;

public class Main2 
{
	//程序测试代码的入口
	public static void main(String args[])
	{
		CyclicBarrier cyclicBarrier = new CyclicBarrier(3);	//注意构造函数传入的整形值
		
		Thread thread1 = new Thread(new Test2(1, cyclicBarrier));
		Thread thread2 = new Thread(new Test2(2, cyclicBarrier));
		thread1.start();
		thread2.start();
		
		System.out.println("mian到达同步点之前");
		try {
			cyclicBarrier.await();
		} catch (Exception e) {
			// TODO: handle exception
		}
		System.out.println("都到达同步点后继续执行main");
	}

}

//线程类,实现runnable接口
class Test2 implements Runnable
{
	private int number = 0;
	CyclicBarrier cyclicBarrier = null;
	
	//构造函数,初始化该类的成员变量
	public Test2(int i,CyclicBarrier cyclicBarrier)
	{
		this.number = i;
		this.cyclicBarrier = cyclicBarrier;
	}
	
	public void run()
	{
		System.out.println(this.number + "到达同步点之前");
		try {
			cyclicBarrier.await();
		} catch (Exception e) {
			// TODO: handle exception
		}
		System.out.println("都到达同步点后继续执行" + this.number);
	}
}

程序运行结果如下;

需要知道的是,多线程的具体执行顺序是不可控制的,但是程序结果中我们可以看到,明显的同步点隔离的效果!三个线程都达到同步点,也就是所谓的cyclicbarrier的await方法后,才会继续执行各自同步点时候的代码!

具体说明:

首先,构造函数,同样传入一个int参数,作为计数器,该计数器代表,必须等到N个同步点都完成await的方法调用后,继续执行各个线程同步点之后的代码逻辑!

例子中涉及到的await()方法就是所谓的同步点,必须等到完成计数器所要求的的数量同步点之后,才会解除阻塞!

这里需要注意的是,该工具类还设有一个高级的构造函数,可以指定达到指定数量的同步点后,也就是解除屏障后,优先执行那个线程!public Cyclicbarrier(int parties,Runnable barrierAction).

思考:CountDownLatch和CyclicBarrier的区别:

1.最主要体现在对于计数器的处理上,CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法进行重置。所以CyclicBarrier可以处理相对更加复杂的场景。

3.控制并发线程数的Semaphore(信号量)

semaphore是用来控制同时访问特定资源的线程数量。

借用方腾飞前辈的《java并发编程的艺术》一书中作者的叙述:多年以来,我都觉得从字面上很难理解Semaphore所表达的含义,只能把它比作是控制流量的红绿灯。比如xx马路要限制车流量,只允许同时有一百辆车在这条马路上行驶,其他的必须要在路口等待,所以前面一百辆车会看到绿灯,可以开进这条马路,后面的车会看到红灯,不能进入xx马路,但是假如前面一百辆车中有五辆车已经离开了xx马路,那么后面就允许有五辆车再进入马路,这个例子中所说的车就是线程,驶入马路就表示线程正在执行,离开马路就表示线程执行完成,看见红灯就表示线程被阻塞,不能执行。

具体的理解,还可以参考有关计算机操作系统基础中关于信号量的描述。

import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class SemaphoreTest {
	
	private static final int THREAD_COUNT = 30;
	private static Semaphore semaphore = new Semaphore(10); 
	
	private static ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
	
	public static void main(String args[])
	{
		for(int i = 0; i < THREAD_COUNT; i++)
		{
			executor.execute(new Test3(i, semaphore));
		}
		
		executor.shutdown();
	}
}

//封装线程类,实现runnable接口
class Test3 implements Runnable
{
	private int number = 0;
	private Semaphore semaphore = null;
	
	public Test3(int number,Semaphore semaphore)
	{
		this.number = number;
		this.semaphore = semaphore;
	}
	
	public void run()
	{
		try {
			semaphore.acquire();
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("当前并发运行编号为: "+this.number);
		semaphore.release();
	}
}

在代码中,虽然创建30个线程在执行,线程池的容量大小也是30,但是其并发度只有10.

Semaphore的构造方法接收一个整形int的数值,表示可用的许可证数量。new Semaphore(10)表示允许10个线程获取许可证,也就是程序的最大并发数是10,semaphore的用法很简单,首先线程使用acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证!

4.线程间交换数据的Exchanger

Exchanger是一个用于线程间协作的工具类,Exchanger用于进行线程间的数据交换。他提供一个同步点,在这个同步点,两个线程可以交换彼此的数据,这两个线程通过exchange()方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange()方法,当两个线程都到达同步点后,这两个线程就可以交换数据。

import java.util.*;
import java.util.concurrent.Exchanger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExchangerTest {
	
	private static final Exchanger<String> exchanger = new Exchanger<String>();
	private static final ExecutorService executor = Executors.newFixedThreadPool(2);
	
	public static void main(String args[])
	{
		Thread thread1 = new Thread(new Test4("A", exchanger));
		Thread thread2 = new Thread(new Test4("B", exchanger));
		
		executor.execute(thread1);
		executor.execute(thread2);
		
		executor.shutdown();
	}

}

//封装线程测试类,实现接口方法
class Test4 implements Runnable
{
	private String string = null;
	private String strcopy = null;
	private Exchanger<String> exchanger = null;
	
	public Test4(String string,Exchanger<String> exchanger)
	{
		this.string = string;
		this.exchanger = exchanger;
	}
	
	//实现接口方法
	public void run()
	{
		try {
			this.strcopy = exchanger.exchange(string);
			System.out.println("当前线程发送的字符串:" + string + ",接收的字符串:" + this.strcopy);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

程序的执行结果显示如下:

如果两个线程中有一个线程没有执行exchange()方法,那么另一个线程会一直等待下去,如果要防止特殊情况发生,避免某个线程持续等待,可以设置等待的最大时长,exchange(String x,long timeout,TimeUtil util);

exchange()方法的简单理解就是,等待两个线程都到达同步点,然后使用该方法,把数据传给另一个线程,并返回从另一个线程中所传送过来的数据。

猜你喜欢

转载自blog.csdn.net/romantic_jie/article/details/100146676