同步工具类是指,能够根据自身的状态来协调线程的控制流的类,同步工具类的特征是,它们封装的一些状态能够决定执行同步工具类的线程是执行还是等待,此外还提供一些方法对状态进行操作,以及一些方法用于高效地等待同步工具类进入到预期状态。
1. 阻塞队列
BlockingQueue,阻塞队列不仅能作为保存对象的容器,也是同步工具类,它能协调生产者消费者等线程之间的控制流,take、put、offer和poll能够阻塞,知道队列达到预期状态。
2. 闭锁
如果把阻塞队列想象成工厂里的工作流,闭锁就好像在公司开会的集合过程。产品和程序员收到消息到办公司开会,各自开始放下手头的工作进入办公室,当boss确定人员到齐,会议开始。
闭锁是一种同步工具类,可以延迟线程的进度,知道其达到终止状态。在开会集合里,参与会议的人员就是线程,终止状态是所有人员到期的条件。闭锁可以确保某些活动直到一个动作都发生才继续执行,例如:确保所有资源都被初始化才继续执行;确保某个服务器以来的其它服务器都已经启动才启动;直达lol所有玩家都准备好才进入游戏。
public class CountDown {
public static 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(){
@Override
public void run() {
try {
startGate.await();
try{
task.run();
}finally{
endGate.countDown();
}
} catch (InterruptedException ignore) {
}
}
};
t.start();
}
Long startTime = System.currentTimeMillis();
startGate.countDown();
endGate.await();
Long endTime = System.currentTimeMillis();
return endTime - startTime;
}
public static void main(String[] args) {
Runnable task = new Runnable(){
@Override
public void run() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+" 进入办公室");
}
};
try {
System.out.println("项目经理要求开会");
Long time = CountDown.timeTasks(10, task);
System.out.println("人员到齐,会议开始");
} catch (InterruptedException ignore) {
}
}
}
上面的代码描述了开会的集合过程。
这里提出一个疑问,CountDown.timeTasks的startGate和endGated用一个原子变量AtomicInteger也可以达到同样的效果,为什么还要闭锁?
讲道理,使用原子变量或其他同步手段实现计数,如果只是做到“达到终止条件就继续执行“是没问题的,CountDownLatch底层是一个链表,链表的每个元素是一个node对象,所有调用await的操作会把一个新的node对象插入链表tail端,node还将调用的线程对象因此存在thread变量。当终止条件到达,是从head开始逐个唤醒线程的。也就是说,闭锁不仅能实现“达到终止条件继续执行“,还能做到“先进先出“。而且就线程访问控制来说,CountDownLatch的封装性也更好呀,杜绝了对外公布计数器的风险。
class Node {
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
...
}
+------+ prev +-----+ +-----+
head | | <---- | | <---- | | tail
+------+ +-----+ +-----+
3. FutureTask
FutureTask是一种特殊的闭锁,它相当于CountDownLatch的计数为1的情况。FutureTask增加了对运行中的线程的监控,还能够从线程中显式的传递数据到上层代码。关于FutureTask的get方法在线程还未运行、正在运行时调用,调用者会阻塞,线程已经运行完成,则会返回结果或抛出异常。FutureTask相关的知识比较多,后面单独讲,这里先挖个坑。
4. 信号量
计数信号量用来控制同时访问某个资源的操作数量,或者同时执行某个操作的数量。用法上和阻塞队列没很大区别,信号量更专注于对访问数量的控制,只管理着一组虚拟的许可,并不存放资源,这一点和阻塞队列还是有别的。信号量的功能更加纯粹,它能和任何一种容器结合使之变成有界阻塞容器。SemaphoreSet是一个用信号量封装过的Set类,支持对线程访问的控制。
Semaphore的acquire用来获得许可,release用来释放许可。
public class SemaphoreSet<T> {
private Set<T> set;
private Semaphore sem;
public SemaphoreSet(Set<T> set, Integer permits){
this.set = set;
this.sem = new Semaphore(permits);
}
public boolean add(T e) throws InterruptedException{
sem.acquire();
boolean wasAdd = false;
try{
wasAdd = set.add(e);
return wasAdd;
}finally{
if(!wasAdd)
sem.release();
}
}
public boolean remove(Object o){
boolean wasRemoved = set.remove(o);
if(wasRemoved)
sem.release();
return wasRemoved;
}
}
5. 栅栏
前面说的闭锁能够确保某些活动直到一个动作都发生才继续执行,这里必须要说明一下,当一个线程调用CountDownLatch的countDown,就会进入阻塞状态,不管这个线程终止、中断或是发生其他异常情况,对于CountDownLatch除了记录又一个线程成果到达并不会关心这些,参与活动的线程的终止、中断也不会影响到其他活动线程。
在一些活动线程之间业务上紧关联的情况下,一个活动线程的失败意味着整个活动已经没意义,需要终止,闭锁很可能占有cpu运行没有价值的代码。栅栏因此孕育而生。
栅栏比闭锁增加了四个特性:
1. 对await的调用超时,或者await阻塞的线程被中断,那么栅栏就被认为是被打破了,所有阻塞的调用都将被终止并抛出BrokenBarrierException;
2. 如果成果地通过栅栏,await将为每个线程返回一个唯一的到达索引号,利用这些索引号可以选择一个“领导线程“,并在后续由它执行一些特殊的工作;
3. CyclicBarrier构造函数运行将一个Runnable对象存储起来,当成功通过栅栏时会(在一个字任务线程中)执行它,但在阻塞线程被全部释放前是不能执行的;
4. CyclicBarrier的官方注释提到,“Cyclic“意味着栅栏时可以重复使用的,特别环保。
public class Barrier {
final int N;
final float[][] data;
final CyclicBarrier barrier;
class Worker implements Runnable {
int myRow;
Worker(int row) {
myRow = row;
}
public void run() {
while (!done()) {
processRow(myRow);
try {
barrier.await();
} catch (InterruptedException ex) {
return;
} catch (BrokenBarrierException ex) {
return;
}
}
}
}
public Barrier(float[][] matrix) {
data = matrix;
N = matrix.length;
Runnable barrierAction = new Runnable() {
public void run() {
mergeRows();
}
};
barrier = new CyclicBarrier(N, barrierAction);
List<Thread> threads = new ArrayList<Thread>(N);
for (int i = 0; i < N; i++) {
Thread thread = new Thread(new Worker(i));
threads.add(thread);
thread.start();
}
// wait until done
for (Thread thread : threads){
try {
thread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
private boolean done(){
return false;
}
private void processRow(int row){}
private void mergeRows(){}
}
上面的Barrier类用于计算二维数组的和。这里为每行开了一个线程Worker
计算和,当所有计算都完成,调用barrierAction
计算最终的结果。
栅栏特别适合并行迭代算法中,将一个问题拆分成一些列互相独立的子问题的情况。