同步工具类可以使任何一个对象,只要它根据自身的状态来协调线程的控制流。阻塞队列可以作为同步工具类,其他类型的同步工具类还包括信号量,栅栏以及闭锁。
所有的同步工具类都包含一些特定的结构化属性:封装了一些状态,这些状态将决定执行同步工具类的线程是继续执行还是等待,还提供一些方法对状态进行操作,自己用于高效的等待同步工具类进入预期状态的方法。
闭锁
可以延迟线程的进度直到其到达终止状态。
闭锁作用相当于一扇门:到达结束状态前,门一直关闭,任何线程不得通过执行,只有,闭锁这个门打开,才会允许其他线程执行。状态只能从关闭到打开,并一直保持。闭锁可以确保某些活动直到其他活动都完成才继续执行:
eg:
- 确保计算的资源都被初始化才执行计算。
- 确保每个服务所依赖的服务启动后才启动。
- 等待直到某个操作的所有参与者(例如,多玩家游戏中的所有玩家)都就绪再继续执行。
CountDown时灵活的闭锁实现,可以使一个或多个线程等待一组事件的发生。包含一个计数器,通过构造函数初始化,调用countDown(),计数器每次减1,表示有一个事件发生了,await()方法等待计数器到达零,表示所有等待事件均已发生,如果非零,await()会一直阻塞,直到零,或者等待线程中断,或超时。
示例如下:线程创建后并不会立即执行,而是在startGate.countDown()之后执行,保证了所有创建的线程一起执行,而不是一些还没创建,一些已经在执行。启动门使得同一时间释放所有线程,结束们保证所有线程结束才执行下面操作。
public long timeTasks(int nThreads, final Runnable task){
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(nThreads);
for( int i = 0; i < nThreads; i++){
Thread t = new Thread(i + ""){
@Override
public void run(){
try {
System.out.println("阻塞等待事件" + Thread.currentThread().getName() );
startGate.await();
try {
task.run();
}finally {
endGate.countDown();
}
}catch (Exception e){
e.printStackTrace();
}
}
};
t.start();
}
long start = System.nanoTime();
System.out.println("主线程执行");
try {
Thread.sleep(10);
//让主线程等一下看事件组会执行吗
} catch (InterruptedException e) {
e.printStackTrace();
}
startGate.countDown();
System.out.println("等待事件组可以执行");
try {
System.out.println("等待事件组未执行完毕");
endGate.await();
System.out.println("等待事件组执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.nanoTime();
return end - start;
}
FutureTask
FutureTask也可以做闭锁(实现了Future的语义,表示一种抽象的可计算的结果)。通过Callable实现,相当于一个可生成结果的Runnable.
三种状态:
- 等待运行
- 正在运行
- 运行完成
“运行完成”表示计算的所有可能结束的状态,包含正常结束,由于取消而结束和由于异常而结束。当进入完成状态,他会停止在这个状态上。
Furture.get()获取执行结果的值,取决于执行的状态,如果任务完成,会立即返回结果,否则一直阻塞直到任务进入完成状态,然后返回结果或者抛出异常。FutureTask负责将计算结果从执行任务的线程传递到调用这个线程的线程,而且确保了 传递过程中结果的安全发布
public class PreLoader {
private final FutureTask<Product> future = new FutureTask<Product>(new Callable<Product>(){
public Product call() throws Exception{
//执行一些耗时任务
return loadProduct();
}
});
private final Thread thread = new Thread(future);
public void start(){
thread.start();
}
private Product loadProduct() {
Product product = new Product();
//可从数据库或者其他方式获取,耗时任务
return product;
}
public Product get(){
try {
return future.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return null;
}
}
信号量
用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量,还可以实现对某种资源池或者同期划分边界
Semaphore管理一组许可,通过构造函数设置。执行操作时需要先获得许可(有剩余)通过acquire()方法尝试获取许可,如果没有许可,会一直阻塞直到有许可(中断或超时)。release()方法将释放一个许可给信号量。二值信号量可以做互斥体,并具备不可重入锁语义,可以做互斥锁。
Semaphore可以用于资源池,比如数据库连接池,可构造固定长度数据库连接池,当池为空则请求失败(实际为阻塞)还可以将容易变为有界阻塞容器 如下例:
public class TestSemaphore {
private final Set<T> set;
private final Semaphore sem;
public TestSemaphore(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 {
//如果add操作不成功,立刻释放许可
if(!wasAdded){
sem.release();
}
}
}
public boolean remove(Object o){
boolean wasRemoved = set.remove(o);
//删除成功,释放许可
if(wasRemoved){
sem.release();
}
return wasRemoved;
}
}
栅栏(Barrier)
CyclicBarrier
栅栏(Barrier)类似于闭锁,它能阻塞一组线程直到某个事件的发生。栅栏与闭锁的区别在于,所有的线程必须同时到达栅栏未知,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。
CyclicBarrier可以使一定数量的参与方反复的在栅栏处等待汇集。在迭代算法中非常有用:这种算法通常将一个问题拆分成一系列相互独立的子问题。当线程到达栅栏将调用await()方法,这个方法将阻塞,直到所有线程到达栅栏处,如果所有线程达到了栅栏处,栅栏将打开,此时所有线程释放,而栅栏将被重置,以便下次使用。如果对await()方法调用超时,或者await阻塞的线程被中断,那么栅栏被认为是打破,所有await()调用将终止并抛出BrokenBarrierException,如果成功通过栅栏,那么await()将为每个线程返回一个唯一索引号。
CyclicBarrier存在两个构造函数
public CyclicBarrier(int parties);
public CyclicBarrier(int parties, Runnable barrierAction);
//第一个参数表示等待的线程数,第二个表示所有线程结束执行的操作。
第二个参数是一个Runnable,当成功通过栅栏时(在子任务线程)执行它,但在阻塞线程被释放之前是不能执行的。
另一个栅栏是EXchanger
Exchanger
**它是在两个任务之间交换对象的栅栏。当这些任务进入栅栏时,各持有一个对象,当他们离开时,他们都拥有之前由对象持有的对象。典型应用场景:
一个任务在创建对象,这些对象的生产代价很高昂,而另一个任务消费这些对象。通过这种方式,可以有更多的对象在被创建的同事被消费。