并发包java.util.concurrent不仅提供了线程池相关的接口和类,同时也提供了几个很有用的工具类!这里打算介绍4个,包括Semaphore,CyclicBarrier,CountDownLatch,Exchanger。在实际编码中,进行多线程的并发互斥和同步控制,使用这些工具类可以节省不少代码!

【Semaphore】

信号灯工具类。我们想想以前控制线程互斥的方式:synchronized & Lock,都达到了什么效果? synchronized关键字,保证了一段多线程敏感代码一次只会有一个线程运行,Lock中“写锁”也会是这个效果,多个持有“读锁”的线程就可以多线程并发访问资源。但我们现在有这个需求,控制某个资源最多有几个线程访问。如果自己实现这个需求,还是需要一定的代码量的,利用Semaphore就可以轻松实现这个需求,我们看这个例子:

package cn.test;

import java.util.concurrent.Semaphore;

public class SemaphoreTest {

	public static void main(String[] args) {
		
		// 创建一个资源,最大允许2个线程同时并发
		final Resources resource = new Resources(2);
		// 起5个线程,开始处理资源!
		for(int i=0; i<5; i++){
			
			new Thread(new Runnable(){

				@Override
				public void run() {
					
					resource.processResource();
					
				}}, "Thread " + (i+1)).start();
		}
	}
	
	/**
	 * 资源类,该资源允许多线程并发访问!但为了保证可用性,外界可以设定最多几个线程进行并发!
	 */
	static class Resources {
		
		// 信号灯,用于控制该资源的并发访问数量
		Semaphore semaphore = null;
		public Resources(int concurrentNum){
			
			if(concurrentNum < 1){
				
				throw new IllegalArgumentException("该资源的并发数量应该>0!");
			}
			semaphore = new Semaphore(concurrentNum);
		}

		public void processResource() {
			
			System.out.println(Thread.currentThread().getName() + " 正试图获取准许进入资源.....");
			try {
				// 获取一个信号灯,如果没有了,则线程阻塞在这里
				semaphore.acquire();
				System.out.println(Thread.currentThread().getName() + " 已获取操作资源许可,准备操作!");
				// 进行资源操作,这里让线程sleep 代表耗时任务
				Thread.sleep(2000);
				System.out.println(Thread.currentThread().getName() + " 执行完毕,准备释放信号灯!");
				semaphore.release();
				
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

输出为:

Thread 1 正试图获取准许进入资源.....
Thread 3 正试图获取准许进入资源.....
Thread 4 正试图获取准许进入资源.....
Thread 2 正试图获取准许进入资源.....
Thread 3 已获取操作资源许可,准备操作!
Thread 1 已获取操作资源许可,准备操作!
Thread 5 正试图获取准许进入资源.....
Thread 3 执行完毕,准备释放信号灯!
Thread 1 执行完毕,准备释放信号灯!
Thread 2 已获取操作资源许可,准备操作!
Thread 4 已获取操作资源许可,准备操作!
Thread 4 执行完毕,准备释放信号灯!
Thread 2 执行完毕,准备释放信号灯!
Thread 5 已获取操作资源许可,准备操作!
Thread 5 执行完毕,准备释放信号灯!

我们这个资源最多允许两个线程进入,我们看,开始是4个线程同时申请进入,最后只有两个线程申请成功!只有执行完毕,并且释放信号灯,其他线程才可以成功申请进入!创建Semaphore时,需要设定信号灯的数量,这个数量最终用于控制线程的数量!调用Semaphore对象的acquire()方法来申请信号灯,如果还有,则申请成功,否则线程就会阻塞在这里!线程执行完毕后,要调用Semaphore对象的release()方法来释放信号灯!通过Semaphore我们可以很轻松地对某个资源的操作线程数进行控制!

Semaphore semaphore = new Semaphore对象的作用和synchronized的功效一样!但他有个优点:即某个线程可以调用semaphore的release方法来释放其他线程把持的信号灯!对于synchronized修饰的代码段,如果一个线程进入后,因某种情况如死锁导致无法正常从方法块中出来时,其他线程只能阻塞等待,必要时可能需要人工重启解决。如果使用Semaphore,可以设定一个线程进行监控,如果某个其他线程长时间把持信号灯不放,就可以认为其出现异常情况,可以主动干预释放其把持的信号灯!这种特性的使用,需要根据具体情况仔细才可使用!

【CyclicBarrier】

“可循环的拦阻器”。我们直接用一个例子来解释其用法。假设,一个任务由两个子任务组成,每个子任务包含两部分,子任务2必须要等子任务1完成后才能开始!我们有两个线程,安排线程1执行子任务1第1部分,子任务2第1部分,线程2执行子任务1第2部分,子任务2第2部分,两个线程共同将子任务1完成后才能开始执行子任务2,我们可以通过CyclicBarrier来实现:

package cn.test;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierTest {

	public static void main(String[] args) {
		
		/**
		 * 创建任务,该任务由两个子任务组成,任务1和任务2.每个子任务包含两部分,任务2要依赖于任务1。
		 * 即任务2开始执行时,任务1必须完成。我们创建两个线程来做这个。任务内部通过CyclicBarrier来
		 * 控制子任务的关系!
		 */
		final MyTask task = new MyTask();
		new Thread(new Runnable(){

			@Override
			public void run() {
				
				task.task1Part1();
				task.task2Part1();
				
			}}, "线程一").start();
		new Thread(new Runnable(){

			@Override
			public void run() {
				
				task.task1Part2();
				task.task2Part2();
				
			}}, "线程二").start();
		
	}
	
	static class MyTask {
		
		CyclicBarrier barrier = new CyclicBarrier(2);
		
		public void task1Part1() {
			
			System.out.println(Thread.currentThread().getName() + " 开始执行任务1的部分1!");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + " 将任务1的第1部分执行完毕!!");
		}
		
		public void task1Part2() {
			
			System.out.println(Thread.currentThread().getName() + " 开始执行任务1的部分2!");
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + " 将任务1的第2部分执行完毕!!");
		}
		
		public void task2Part1() {
			
			try {
				barrier.await();
				System.out.println(Thread.currentThread().getName() + " 开始执行任务2的部分1!");
				Thread.sleep(1000);
				System.out.println(Thread.currentThread().getName() + " 将任务2的第1部分执行完毕!!");
			} catch (InterruptedException e) {
				e.printStackTrace();
			} catch (BrokenBarrierException e) {
				e.printStackTrace();
			}
		}
		
		public void task2Part2() {
			
			try {
				barrier.await();
				System.out.println(Thread.currentThread().getName() + " 开始执行任务2的部分2!");
				Thread.sleep(2000);
				System.out.println(Thread.currentThread().getName() + " 将任务2的第2部分执行完毕!!");
			} catch (InterruptedException e) {
				e.printStackTrace();
			} catch (BrokenBarrierException e) {
				e.printStackTrace();
			}
		}
		
		
	}

}

输出结果为:

线程一 开始执行任务1的部分1!
线程二 开始执行任务1的部分2!
线程一 将任务1的第1部分执行完毕!!
线程二 将任务1的第2部分执行完毕!!
线程二 开始执行任务2的部分2!
线程一 开始执行任务2的部分1!
线程一 将任务2的第1部分执行完毕!!
线程二 将任务2的第2部分执行完毕!!

创建CyclicBarrier对象,需要一个整形参数,表示这个对象一次阻拦多少个线程。当阻拦的线程数一到,才准许放行!调用CyclicBarrier对象的await()方法,如果此时线程数没到,则线程阻塞在这里,否则,所有阻塞的线程均放行!CyclicBarrier可以反复使用,放行一批后,另一批过来,又是重复这个过程!!

CountDownLatch 和 Exchanger,我们下篇再讲!