同步工具类可与是任何一个对象,只要它根据自身的状态来协调线程的控制流。阻塞队列可与作为同步工具类,其他类型的同步工具类还包括 信号量(Semaphore)、栅栏(Barrier)以及闭锁(Latch)。
所有的同步工具类都包含一些特定的结构化属性:它们封装了一些状态,这些状态将决定执行同步工具类的线程是继续执行还是等待,此外还提供了一些方法对状态进行操作,以及另一些方法用于高效地等待同步工具类进入到预期状态。
闭锁:闭锁是一种同步工具类,可以延迟线程的进度直到其(闭锁)到达种终止状态。闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直关闭,没有任何线程可以通过,当到达结束状态时,这扇门会打开并允许所有的线程通过。当闭锁到达结束状态后,将不会改变状态,即这扇门会永远保持打开状态。
闭锁可以用来确保某些活动直到其他活动都完成后才继续执行。例如:
· 确保某个计算在其需要的所有资源都被初始化之后才继续执行。二元闭锁(包括两个状态)可以用来表示“资源R已经被初始化”,而所有需要R的操作都必须现在这个闭锁上等待。
· 确保某个服务在其依赖的所有其他服务都已经启动之后才启动。每个服务都有一个相关的二元闭锁。当启动服务S时,将首先在S依赖的其他服务的闭锁上等待,在所以依赖的服务都启动后会释放闭锁S,这样其他依赖S的服务才能继续执行。
· 等待直到某个操作的所有参与者都就绪再继续执行。在这种情况中,当所有参与者都准备就绪时,闭锁将到达结束状态。
CountDownLatch是一种灵活的闭锁实现,可以在上述的各种情况中使用,它可以使一个或多个线程等待一组事件发生。闭锁状态包括一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示有一个事件已经发生了,而await方法等待计数器达到零,或者等待中的线程中断,或者等待超时。
闭锁的两种常见用法:
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
ignored
) {}
}
};
t
.start();
}
long
start
= System.
nanoTime
();
startGate
.countDown();
//开始门减一,闭锁结束,所有线程开始执行,每个线程执行完任务后结束门闭锁等待的线程会减一
endGate
.await();
//等待结束门开启
long
end
= System.
nanoTime
();
return
end
-
start
;
}
}
FutureTask
FutureTask表示的计算是通过Callable来实现的,相当于一种可生成结果的Runnable,并且可以处于以下3种状态:等待运行、正在运行和运行完成。“执行完成”表示计算的所有可能结束方式,包括正常结束、由于取消而结束和由于异常而结束等。当FutureTask进入完成状态后,它会永远停止在这个状态上。
Future.get的行为取决于任务的状态。如果任务已经完成,那么get会立即返回结果,否则get将阻塞直到任务进入完成状态,然后返回结果或抛出异常。
FutureTask在Executor 框架中表示异步任务,此外还可以用来表示一些时间较长的计算,这些计算可以在使用计算结果之前启动。通过提前启动计算,可以减少在等待结果时需要的时间。
信号量(Semaphore)
计数信号量用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。计数信号量还可以用来实现某种资源池,或者对容器施加边界。
栅栏(Barrier)
栅栏(Barrier)类似于闭锁,它能阻塞一组线程直到某个事件发生。栅栏与闭锁的关键区别在于,所有线程必须同时到达栅栏位置才能继续执行。
闭锁用于等待事件,而栅栏用于等待其他线程。
CyclicBarrier 可以使一定数量的参与方反复地在栅栏位置汇集,它在并行迭代算法中非常有用:这种算法通常将一个问题拆分成一系列相互独立的子问题。当线程到达栅栏位置时将调用
await 方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达了栅栏位置,那么栅栏将打开,此时所有线程被释放,而栅栏将被重置以便下次使用。如果对
await 的调用超时,或者
await 阻塞的线程被中断,那么栅栏就被认为是打破了,所有阻塞的
await 调用都将终止并抛出
BrokenBarrierException 。如果成功通过栅栏,那么 await 将为每个线程都返回一个唯一的到达索引号。CyclicBarrier还可以使你将一个栅栏操作传递给构造函数,这是一个Runnable,当成功通过栅栏时会(在一个子任务线程中)执行它,但在阻塞线程被释放之前是不能执行的。
两方栅栏(Two-Party)
另一种形式的栅栏是Exchanger,它是一种两方(Two-Party)栅栏。当两方执行不对称的操作时,Exchanger会非常有用。最简单的方案是:当缓冲区被填满时,由填充任务进行交换,当缓冲区会空时,由清空任务进行交换。
如果新数据的到达率不可预测,那么一些数据的处理过程就将延迟。另一个方法是,不仅当缓冲被填满时进行交换,并且当缓冲被填充到一定程度并保持一定时间后,也进行交换。