闭锁
闭锁是一种同步工具类,可以延迟线程的进度直到其到达最终状态。闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关着的,并且没有任何线程能通过,
当到达结束状态时,这扇门会打开允许所有的线程通过。当闭锁到达结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。闭锁可以用来确保某些活动指导其他活动都完成后才继续执行,例如:
1、确保某个计算在其需要的所有资源都被初始化之后才继续执行。二元闭锁(包括两个状态)可以用来表示“资源R已经被初始化”,而所有需要R的操作都必须现在这个闭锁上等待。
2、确保某个服务在其依赖的所有其他服务都已经启动之后才启动,每个服务都有一个相关的二元闭锁。当启动服务S时,将首先在S依赖的其他服务的闭锁上等待,在所有依赖的服务都启动后悔释放闭锁S,这样其他依赖S的服务才能继续执行。
3、等待直到某个操作的所有参与者(例如:在多玩家游戏中的所有玩家)都就绪再继续执行。在这种情况直接哦欧诺个,当所有玩家都准备就绪时,闭锁将到达结束状态。
CountDownLatch是一种灵活的闭锁实现,可以在上述各种情况中使用。
闭锁状态包括一个计数器,该计数器被初始化为一个正数,表示需要等待的时间数量。countDown方法递减计数器,表示有一个时间已经发生了,而await方法等待计数器到达零,这表示所有需要等待的时间都已经发生。如果计数器的值非零,那么await会一直阻塞知道计数器为零,或者等待中的线程中断,或者等待超时。
下面的TestHarness中给出了闭锁的两种常见的用法。TestHarness创建一定数量的线程,利用它们并发地执行指定的任务。它使用两个闭锁,分别表示 起始门 和 结束门。起始门计数器的初始值为1,二结束们计数器的初始值为工作线程的数量。每个工作线程首先要做的值就是在启动门上等待,从而确保所有线程都就绪后才开始执行。而每个线程要做的最后一件事情就是将调用结束们的countDown方法减1,这能使主线程高效的等待直到所有工作线程都执行完成,因此可以统计所消耗的时间。
public class TestHarness {
public long timeTasks(int nThreads, final Runnable task) throws InterruptedException{
final CountDownLatch startGate =new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(nThreads);
for(int i = 0 ; i < nThreads ; i ++){
Thread t = new Thread(){
public void run(){
try {
startGate.await();
try {
task.run();
}finally{
endGate.countDown();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t.start();
}
long start = System.nanoTime();
startGate.countDown();
endGate.await();
long end = System.nanoTime();
return end-start;
}
}
FutureTask
FutureTask也可以用作闭锁。在Executor框架中表示异步任务,此外还可以用来表示一些时间较长的计算,这些计算可以在使用计算结果之前启动。
Semaphore
计数信号量(Count Semaphore) 用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。计数信号量还可以用来实现某个资源池,或者对容器施加边界。
Semaphore中管理着一组虚拟的许可,许可的初始数量可通过构造函数来指定,在执行操作时可以首先获得许可,并在使用以后释放许可。如果没有许可那么acquire将阻塞直到有许可。release方法将返回一个许可给信号量,计算信号量的一种简化形式是二值信号量,即初始值为1的Semaphore。二值信号量可以用做互斥体,并具备不可重入的加锁语义:谁拥有这个唯一的许可,谁就拥有了互斥锁。
同样,你也可以使用Semaphore将任何一种容器变成有界阻塞容器,如下程序,信号量的计数值会初始化为容器容量的最大值。add操作在向底层容器中添加一个元素之前,首先要获取一个许可。如果add操作没有添加任何元素,那么会立刻释放许可。同样,remove操作释放一个许可,使更多的元素能够添加到容器中。底层的Set实现并不知道关于边界的任何信息,这是有BoundedHashSet来处理的。
public class BoundedHashSet<T> {
private final Set<T> set;
private final Semaphore sem;
public BoundedHashSet(int bound){
this.set = Collections.synchronizedSet(new HashSet<T>());
sem = new Semaphore(bound);
}
public boolean add(T o) throws InterruptedException{
sem.acquire();
boolean wasAdded = false;
try{
wasAdded = set.add(o);
return wasAdded;
}finally{
if (!wasAdded) {
sem.release();
}
}
}
public boolean remove(Object o){
boolean wasRemoved = set.remove(o);
if (wasRemoved) {
sem.release();
}
return wasRemoved;
}
}
CyclicBarrie
栅栏(Barrier)类似于闭锁,它能阻塞一组线程直到某个事件发生。栅栏与闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。栅栏用于实现一些协议,例如几个家庭决定在某个地方集合:“所有人6:00在麦当劳碰头,到了以后要等待其他人,之后再讨论下一步要做的事情。”
CyclicBarrier可以使一定数量的参与方反复地在栅栏位置汇集,它在并行迭代算法中非常有用:这种算法通常将一个问题拆分成一系列相互独立的子问题。当线程到达栅栏位置时将调用await方法,这个方法将阻塞知道所有线程都到达栅栏位置。如果所有线程都到达了栅栏位置,那么栅栏将打开,此时所有线程都被释放,而栅栏将被重置以便下次使用。
如果成功地通过栅栏,那么await将为每个线程返回一个唯一的到达索引号,我们可以利用这些索引来选举产生一个领导线程,并在下一次迭代中由该领导线程执行一些特殊的工作。CyclicBarrier还可以使你将一个栅栏操作传递给构造函数,这是一个Runnable,当成功通过栅栏时会执行它,但在阻塞线程被释放之前是不能执行的。
public class CellularAutomata {
private final CyclicBarrier barrier;
public final Worker[] workers;
public CellularAutomata(){
int count = Runtime.getRuntime().availableProcessors();
barrier = new CyclicBarrier(count, new Runnable() {
public void run() {
System.out.println("开始汇总");
}
});
workers = new Worker[count];
for(int i = 0 ; i < count ; i ++){
workers[i] = new Worker("任务"+i,(i+1)*10000);
}
}
private class Worker implements Runnable{
private String task;
private long time;
public Worker(String task,long time){
this.task = task;
this.time = time;
}
public void run() {
try {
Thread.sleep(time);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.out.println("已经完成"+task+"了");
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
CellularAutomata cl = new CellularAutomata();
for(int i = 0 ; i < cl.workers.length ; i ++){
new Thread(cl.workers[i]).start();
}
}
}