第四章 闭锁
闭锁##
闭锁(latch)是一种Synchronizer(Synchronizer是一个对象,它根据本身的状态调节线程的控制流)。它可以延迟线程的执行进度直到到达终点状态。一般用来确保特定活动直到其他的活动完成后才发生。
工作的原理就像一道大门,直到闭锁达到终点状态之前,门一直是关闭的,所有线程都不能通过;当终点状态到来时,门打开,允许所有线程通过。一旦闭锁到达终点状态,就不能够再改变状态。
CountDownLatch###
CountDownLatch是闭锁的一个灵活的实现。它通过一个计数器,并初始化一个正数,代表完成多少个准备工作。每完成一次,调用countDown方法对计数器做减一操作,再通过await方法等待计数器达到零。await方法会一直阻塞线程直到计数器为零或者等待线程中断以及超时。
下面是测试并发10个线程执行for循环100000次所花费的时间,既然是并发执行,所以就需要所有线程创建完成后同时执行,我们就需要使用CountDownLatch类了。
public class CountDownLatchDemo extends Thread{
private static final CountDownLatch latch = new CountDownLatch(10);
@Override
public void run() {
//countDown方法表示一个线程已经创建完成
latch.countDown();
//如果计数器没有到达零,调用await方法等待其他线程创建完成
if(latch.getCount()!=0){
try {
latch.await();
} catch (InterruptedException e) {
//TODO
}
}
long startTime = System.nanoTime();
for(int i=0;i<100000;i++){
//TODO
}
long endTime = System.nanoTime();
System.out.println("Thread "+Thread.currentThread().getName()+" waste time: "+
(endTime-startTime)+"ns");
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
CountDownLatchDemo countDownLatchDemo =
new CountDownLatchDemo();
countDownLatchDemo.start();
}
}
}
FutureTask###
FutureTask同样可以作为闭锁。它的实现描述了一个抽象的可携带结果的操作。FutureTask的操作通过传递一个Callable类型的参数实现的,它等价于一个可携带结果的Runnable。
我们通过get方法获得执行的结果,如果任务没有执行结束,则会被阻塞到直到任务执行完成,然后返回结果;或者任务发生异常,get方法会获得该异常并抛出。
在需要执行一个耗时的计算时,我们可以通过FutureTask提前就开始执行,然后在需要结果的时候通过get方法去获取。这样可以避免同步操作一直等待的尴尬。简要实现如下:
public class FutureTaskDemo {
private final FutureTask<String> ft = new FutureTask<String>(new Callable<String>() {
public String call() throws Exception {
Thread.sleep(5000);
//do something
return "success";
};
});
private final Thread thread = new Thread(ft);
public void start(){
thread.start();
}
public String get()
throws InterruptedException, ExecutionException{
return ft.get();
}
public static void main(String[] args) {
FutureTaskDemo demo = new FutureTaskDemo();
demo.start();
try {
System.out.println(demo.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
Semaphore(信号量)###
信号量可以用来限定同时使用某种资源或执行某个操作的数量。我们可以通过它来实现资源池或者带边界的容器。
Semaphore管理一个有效的许可集。许可集的初始量通过构造函数传递进来,调用acquire方法获取许可,如果没有许可,则一直阻塞,直到有许可为止,调用release方法返回一个许可。
我们可以通过Semaphore把任何容器转化为有界的阻塞容器。
public class BoundedHashSet<T> {
private final Set<T> set;
private final Semaphore semaphore;
public BoundedHashSet(int bound) {
this.set = Collections.synchronizedSet(new HashSet<T>());
this.semaphore = new Semaphore(bound);
}
public boolean add(T t) throws InterruptedException{
semaphore.acquire();
boolean wasAdded = false;
try {
wasAdded = set.add(t);
return wasAdded;
} finally{
if(!wasAdded)
semaphore.release();
}
}
public boolean remove(T t){
boolean wasRemoved = set.remove(t);
if(wasRemoved)
semaphore.release();
return wasRemoved;
}
}
关卡###
关卡(barrier)类似于闭锁,他们都能阻塞一组线程,直到某些事件发生。
不同在于关卡要求所有线程必须同时到达关卡点。可以这样说:闭锁等待的是事件,而关卡等待的是其他线程。
CyclicBarrier####
CyclicBarrier是关卡的一种实现。它允许一个给定数量的成员多次集中在一个关卡点。当线程到达关卡点时,调用await方法阻塞直到所有线程到达关卡点。然后关卡就被成功的突破,所有线程被释放。关卡会重置以备下一次使用。
其实CyclicBarrier和我们小时候打的西游记街机的关卡一样。在游戏的每一关里,打完所有的怪物后,所有英雄必须到达关卡点,然后才能进入下一关。如果要设计西游记这样的游戏,就可以创建一个关卡点的对象,记录当前的关卡数,每当通过一个关卡,当前关卡数加1,直到通关。
我们来实现这个简单的游戏:
首先创建一个英雄类:
public class Hero {
private String name;
public Hero(String name) {
this.name = name;
}
//打怪
public void fight(){
System.out.println(name+" 开始打怪");
}
//到达关卡点
public void reachBarrier(){
System.out.println(name+" 到达关卡点");
}
}
然后是单例的关卡类:
public class Barrier {
//总共一百关
private final int allCount = 100;
//当前关数
private AtomicInteger count = new AtomicInteger(1);
private Barrier(){}
//是否通关
public boolean isThroughBarrier(){
return count.get() == allCount;
}
//通过一个关卡,当前关数加1
public void throughBarrier(){
System.out.println("通过了"+count.get()+"关");
count.getAndIncrement();
}
//返回一个单例的Barrier
public static Barrier getInstance(){
return BarrierCreator.barrier;
}
static class BarrierCreator{
private static Barrier barrier = new Barrier();
}
}
最后是游戏的实现Game类:
public class Game {
//标示game是否结束
private volatile boolean isGameOver = false;
private CyclicBarrier cyclicBarrier;
private HeroThread[] heros;
public Game(int heroNum,String[] names) {
this.cyclicBarrier = new CyclicBarrier(heroNum, new Runnable() {
@Override
public void run() {
Barrier barrier = Barrier.getInstance();
if(barrier.isThroughBarrier()){
isGameOver = true;
System.out.println("恭喜通关了!");
}else{
barrier.throughBarrier();
}
}
});
heros = new HeroThread[heroNum];
for(int i=0;i<heroNum;i++){
heros[i] = new HeroThread(new Hero(names[i]));
}
}
public static void main(String[] args) {
String[] heroNames = {"孙悟空","猪八戒","白龙马"};
Game game = new Game(heroNames.length, heroNames);
game.start();
}
public void start(){
for(int i=0;i<heros.length;i++){
heros[i].start();
}
}
private class HeroThread extends Thread{
private Hero hero;
public HeroThread(Hero hero) {
this.hero = hero;
}
@Override
public void run() {
while(!isGameOver){
hero.fight();
hero.reachBarrier();
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
break;
} catch (BrokenBarrierException e) {
e.printStackTrace();
break;
}
}
}
}
}
运行的结果如下:
猪八戒 开始打怪
猪八戒 到达关卡点
孙悟空 开始打怪
孙悟空 到达关卡点
白龙马 开始打怪
白龙马 到达关卡点
通过了1关
白龙马 开始打怪
白龙马 到达关卡点
孙悟空 开始打怪
孙悟空 到达关卡点
猪八戒 开始打怪
猪八戒 到达关卡点
通过了2关
猪八戒 开始打怪
猪八戒 到达关卡点
白龙马 开始打怪
白龙马 到达关卡点
孙悟空 开始打怪
孙悟空 到达关卡点
通过了3关
孙悟空 开始打怪
孙悟空 到达关卡点
猪八戒 开始打怪
猪八戒 到达关卡点
白龙马 开始打怪
白龙马 到达关卡点
通过了4关
...
孙悟空 开始打怪
孙悟空 到达关卡点
猪八戒 开始打怪
白龙马 开始打怪
猪八戒 到达关卡点
白龙马 到达关卡点
通过了97关
白龙马 开始打怪
猪八戒 开始打怪
孙悟空 开始打怪
孙悟空 到达关卡点
猪八戒 到达关卡点
白龙马 到达关卡点
通过了98关
白龙马 开始打怪
猪八戒 开始打怪
孙悟空 开始打怪
猪八戒 到达关卡点
白龙马 到达关卡点
孙悟空 到达关卡点
通过了99关
孙悟空 开始打怪
白龙马 开始打怪
猪八戒 开始打怪
白龙马 到达关卡点
孙悟空 到达关卡点
猪八戒 到达关卡点
恭喜通关了!
如果对await的调用超时,或者阻塞线程被中断,关卡就被认为是失败的。
结合上面的游戏,我们可以设置每一关通过的时间限制,超过时间后就认为关卡失败了,游戏就结束了。
如果成功通过关卡,await为每一个线程返回一个唯一的到达索引号,可以根据索引号来进行选举或者做其他操作。
Exchanger####
另一种关卡是Exchanger。它是一种两部关卡,在关卡点会交换数据。当两方进行的活动不对称,且需要相互间交换数据,Exchanger就会非常适合。当两个线程通过Exchanger交换对象时,其内部为双方的对象建立了一个安全的发布。
交换的时机取决于应用程序的响应需求。最简单的方案是当生产者的缓冲存满时就发生交换,且当消费者的缓冲清空后也发生交换。这样交换时的平均等待时间最少。另一种方案是缓冲满了就发生交换或缓冲只有部分充满但已存在特定长时间也发生交换。
我们来看一个简单的使用Exchanger的生产者消费者的例子:
public class ExchangerDemo {
private final Exchanger<Set> exchanger = new Exchanger<Set>();
class Productor implements Runnable{
private Set fullSet = new HashSet<>();
private final int fullNum = 10;
private AtomicInteger product = new AtomicInteger(0);
public void addToSet(){
int num = product.incrementAndGet();
System.out.println("生产了"+num);
fullSet.add(num);
}
public boolean isFull(){
return fullSet.size() == fullNum;
}
@Override
public void run() {
while(!isFull()){
addToSet();
if(isFull()){
try {
fullSet = exchanger.exchange(fullSet);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class Customer implements Runnable{
private Set emptySet = new HashSet<>();
public void removeFromSet(){
Object obj = emptySet.iterator().next();
emptySet.remove(obj);
System.out.println("消费了"+obj.toString());
}
public boolean isEmpty(){
return emptySet.size() == 0;
}
@Override
public void run() {
while(emptySet != null){
if(isEmpty()){
try {
emptySet = exchanger.exchange(emptySet);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
removeFromSet();
}
}
}
void start(){
new Thread(new Productor()).start();
new Thread(new Customer()).start();
}
public static void main(String[] args) {
ExchangerDemo demo = new ExchangerDemo();
demo.start();
}
}