Synchronizer同步器
在java并发编程中,多线程编程中无可避免的是线程流的控制问题以及共享数据的同步问题。java.util.concurrent包下的许多工具类都是为更好、更简单的方式为程序提供线程流控制和共享数据同步管理。Synchronizer是一个对象,它根据自身的状态调节线程的控制流。在java.util.concurrent包中可以充当Synchronizer的类包括:阻塞队列BlockingQueue、闭锁CountDownLatch、栅栏CyclicBarrier和信号量Semaphore,此外,还可以自定义Synchronizer。
阻塞队列 BlockingQueue
BlockingQueue是一个线程容器,不同于Collection中的List、Set和Collections中的SynchronizedList、
SynchronizedSet,也不同于Map、Collections中的SynchronizedMap和CocurrentMap,它即不会抛出
ConcurrentModificationException、IndexOutOfBoundsException, 也不用在客户端添加synchronized
锁定对象保持读-写同步。BlockingQueue支持生产者-消费者模式,提供了可阻塞的put和take方法来协同队列
容器的存取操作。开发人员只需集中于业务逻辑的处理,而不必费力的维护线程流。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ArrayBlockingQueue;
public class MyBlockingQueueTest {
public static void main(String[] args) {
/**
* BlockingQueue是接口,它的实现类包括
* ArrayBlockingQueue, DelayQueue, LinkedBlockingQueue,
* PriorityBlockingQueue, SynchronousQueue
*/
final BlockingQueue<Goods> blockingQueue = new ArrayBlockingQueue<Goods>(10);
//先来2个厂商进行商品的生产
for(int i = 1, len = 2; i <= len; i++) {
final String producerName = "厂商" + i;
new Thread(new Runnable() {
public void run() {
Producer producer = new Producer(blockingQueue, producerName);
try {
while(true) producer.produce();
} catch(Exception ex) {
ex.printStackTrace();
}
}
}, producerName).start();
}
//在来20个消费者用来消费商品
for(int i = 0, len = 20; i < len; i++)
new Thread(new Runnable() {
public void run() {
Customer customer = new Customer(blockingQueue);
try {
while(true) {
Goods goods = customer.consume();
//模拟产品消费时间
Thread.currentThread().sleep(5 * 100);
System.out.println(goods.getProducerName() + " : " + goods.getName() + " has been consumed!");
}
} catch(Exception ex) {
ex.printStackTrace();
}
}
}).start();
/**注意协调下厂商数和消费者数,从而观察它们的变化*/
}
/**生产者*/
private static class Producer {
/**内部保持阻塞队列*/
private BlockingQueue<Goods> blockingQueue;
/**厂商名字*/
private String name;
/**商品编号*/
private int goodNum;
public Producer(final BlockingQueue<Goods> blockingQueue, final String name) {
this.blockingQueue = blockingQueue;
this.name = name;
}
/**向容器中添加商品*/
public void produce() throws NullPointerException, InterruptedException{
Goods goods = new Goods("商品" + ++this.goodNum, this.name);
//put是将指定元素添加到此队列中,如果没有可用空间,将一直等待(如果有必要)。
this.blockingQueue.put(goods);
System.out.println(goods.getProducerName() + " : " + goods.getName());
}
}
/**消费者*/
private static class Customer {
/**内部保持阻塞队列*/
private BlockingQueue<Goods> blockingQueue;
public Customer(final BlockingQueue<Goods> blockingQueue) {
this.blockingQueue = blockingQueue;
}
/**取容器中的商品*/
public Goods consume() throws InterruptedException{
//take是检索并移除此队列的头部,如果此队列不存在任何元素,则一直等待。
return this.blockingQueue.take();
}
}
/**商品*/
private static class Goods {
//商品编号
private String name;
//生成厂商
private String producerName;
public Goods(final String name, final String producerName) {
this.name = name;
this.producerName = producerName;
}
public String getName() {
return this.name;
}
public String getProducerName() {
return this.producerName;
}
}
}
闭锁CountDownLatch
CountDownLatch是一个同步辅助类,持有者通过调用await方法进行拦截(阻塞)调用线程,而内部的countDown倒计数形式进行线程流控制。CountDownLatch的使用场景是当有一个或多个线程需要等待一组特定的线程完成时,该线程才能执行后续动作。更一般的比喻为:“一个闭锁工作起来就像一道大门,直到闭锁到达终点状态前,门一直是关闭的,没有线程能够通过,在终点状态到来的时候,门开了,允许所有线程都通过。一旦闭锁到达了终点状态,它就不能够在改变状态,所以它永远保持敞开状态。”这句话里面其实包括了两种线程:等待闭锁终点的线程、改变闭锁状态使得闭锁到达终点的线程。
import java.util.concurrent.CountDownLatch;
public class MyCountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
InnerCountDownLatch latch = new InnerCountDownLatch(10, new Runnable() {
/**当闭锁到达终点时,执行操作*/
public void run() {
System.out.println("We say hello, together!");
}
});
latch.execute();
}
/**
* 内部开始、倒数闭锁.
*/
public static class InnerCountDownLatch {
/**开始闭锁,用于阻塞一组等待线程*/
private CountDownLatch startGate = null;
/**结束闭锁,用于记录一组线程完成后,执行特定操作*/
private CountDownLatch endGate = null;
/**线程数*/
private int nThreads = 0;
/**nThreads线程完成后,执行的操作*/
private Runnable task = null;
public InnerCountDownLatch(int nThreads, final Runnable task) {
this.startGate = new CountDownLatch(1);
this.endGate = new CountDownLatch(nThreads);
this.nThreads = nThreads;
this.task = task;
}
/**
* 执行闭锁.
*/
public void execute() throws InterruptedException {
int i = 0;
while(i++ < this.nThreads)
//启动nThreads个线程进行等待开始闭锁的终点
new Thread(new Runnable() {
public void run() {
try {
//等待开始闭锁到达终点
InnerCountDownLatch.this.startGate.await();
InnerCountDownLatch.this.task.run();
} catch(InterruptedException ex) {
System.out.println("what the hell");
} finally {
//倒数结束闭锁
InnerCountDownLatch.this.endGate.countDown();
}
}
}).start();
//记录一组特定操作的运行时间,这里的开始时间
long startTime = System.nanoTime();
//将开始闭锁到达终点,放开nThreads线程执行权限
this.startGate.countDown();
//等待nThreads线程执行完成
this.endGate.await();
//记录nThreads都完成时的时间点
long endTime = System.nanoTime();
System.out.println(endTime - startTime);
}
}
}
上面的程序其实就类似与运动赛跑比赛,一共设置了两个闭锁startGate、endGate,它们分别扮演比赛开始和比赛结束的场景。nTheads线程可以看作nThreads个运动员,开始所有的运动员都在起跑线,规定每个运动员按特定的跑道跑(也就是task),然后裁判发出比赛开始口令(this.startGate.countDown),所有的运动员开始冲向终点,每一个到达终点的运动员都必须进行登记(InnerCountDownLatch.this.endGate.countDown),当所有的运动员都跑完时,裁判宣布结束比赛(打印比赛时间),在这期间裁判都不能宣布比赛结束,所有的人都必须等待(this.endGate.await)。
- 栅栏CyclicBarrier
Latch闭锁虽然可以控制线程流,但是它也有一个比较明显的缺陷“一旦进入到最终状态,就不能将闭锁重新关闭。”此外,CountDownLatch闭锁和CyclicBarrier栅栏都能够阻塞一组线程,直到某些事件发生。但是它们关键点在于,闭锁是等待事件(countDown),而栅栏等待的是线程,换句话说,闭锁等待所有的事件都发生了才放开阻塞线程,而栅栏是等待所有线程都到达时才放开阻塞线程。
import java.util.concurrent.CyclicBarrier;
public class MyCyclicBarrierTest {
public static void main(String[] args) {
InnerCyclicBarrier barrierTest = new InnerCyclicBarrier(3);
barrierTest.execute();
}
public static class InnerCyclicBarrier {
/**栅栏*/
private CyclicBarrier barrier;
/**栅栏阻塞线程数*/
private int nThreads;
public InnerCyclicBarrier(int nThreads) {
this.nThreads = nThreads;
this.barrier = new CyclicBarrier(nThreads, new Runnable() {
//当所有的运动员都到达栅栏处时,在放开栅栏,在此之前都阻塞
public void run() {
System.out.println("比赛开始");
/**可以将栅栏重新关上,然后等其他人继续全部都调用了await方法后在放行*/
//InnerCyclicBarrier.this.barrier.reset();
}
});
}
/**执行*/
public void execute() {
int i = 0;
while(i++ < this.nThreads) {
final int j = i;
//开启nThreads个线程,调用await方法进行栅栏登记
new Thread(new Runnable() {
public void run() {
try {
System.out.println("第" + j + "选手准备好了");
InnerCyclicBarrier.this.barrier.await();
Thread.currentThread().sleep(5 * 100);
System.out.println("第" + j + "到达终点");
} catch (Exception ex) {
ex.printStackTrace();
}
}
}).start();
}
}
}
}
栅栏CyclicBarrier也可以像CountDownLatch类似的赛跑比赛的比喻,但是它们的区别在于栅栏Barrier可以在选手跑过后重置状态,等待下一批运动员进行比赛。这就像循环赛(CyclicBarrier)和冠军赛(CountDownLatch),因为相同的场景发生多次和场景只用一次。
闭锁Latch和栅栏Barrier区别
两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:
1、CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程
都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行
2、CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务
3、CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了信号量Semaphore
信号量Semaphore是用来控制能够同时访问某特定资源活动的数量或者同时执行某一给定操作的的数量,它最常见的应用是用来实现资源池或者给一个容器限定边界。信号量Semaphore管理一个许可集permits,这个许可是在构造信号量Semaphore对象的时候传入,任何活动能够获得acquire权限则可以执行后续操作,并且在使用之后释放许可,如果没有可用的许可,那么acquire则会阻塞请求权限的线程,直到有可用的许可为止。
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.Semaphore;
public class MySemaphoreTest {
public static void main(String[] args) {
final MySemaphoreTest.InnerSemaphoreTest semaphoreTest
= new MySemaphoreTest.InnerSemaphoreTest(1000, true);
int i = 0;
/**开1000个线程进行存苹果*/
while(i++ < 1000) {
final int temp = i;
new Thread(new Runnable() {
/**往集合中添加苹果*/
public void run() {
try {
String appName = "appleName" + temp;
boolean isTrue = semaphoreTest.add(appName);
if(isTrue) System.out.println(appName + " has been put in the bracket!");
} catch(Exception ex) {
ex.printStackTrace();
}
}
}).start();
}
/**开1000个线程进行取苹果*/
while(i-- > 0) {
final int temp = i;
new Thread(new Runnable() {
/**取走集合中苹果*/
public void run() {
String appName = "appleName" + temp;
boolean isTrue = semaphoreTest.remove(appName);
if(isTrue) System.out.println(appName + " has been eated by someone!");
}
}).start();
}
}
/**信号量测试*/
public static class InnerSemaphoreTest {
/**信号量*/
private Semaphore semaphore;
/**许可集大小*/
private int permits;
/**是否按公平分配策略,即先到先得*/
private boolean isFair;
/**假设容器,用于模拟容器存取操作的权限控制,类似于生产者和消费者模式*/
private List<String> apples;
public InnerSemaphoreTest(int permits, boolean isFair) {
this.permits = permits;
this.isFair = isFair;
this.semaphore = new Semaphore(permits, isFair);
this.apples = new ArrayList<String>(permits);
}
public boolean add(String appleName) throws InterruptedException{
/**
* 信号量在获取时,可能抛出InterruptedException异常。
* 当没有多余的信息量可以获取时,则会阻塞当前线程,直到有可用的信号量为止
*/
this.semaphore.acquire();
return this.apples.add(appleName);
}
/**如果成功删除,则返回true,否则返回false*/
public boolean remove(String appleName) {
if(this.apples.remove(appleName)) {
this.semaphore.release();
return true;
}
return false;
}
}
}
结论
synchronizer中的闭锁、栅栏和信号量,以及Exchanger、FutureTask都是线程同步辅助类。如果某一线程或一组线程需要等待另外一组线程的执行完成,那么synchronizer应该可以满足程序对线程流的要求。