同步工具类可以是任意一个对象,只要它可以根据自身的状态来协调线程的控制流。阻塞队列可以作为同步工具类,其他类型的同步工具类还包括信号量(Semaphore)、栅栏(Barrier)以及闭锁。在平台类库中还包含一些其他同步工具类,如果还是不能满足需要,我们可以创建自己的同步工具类。
一、闭锁
闭锁可以延迟线程的进度直到其达到终止状态。闭锁可以用来确保某些活动直到其他活动都完成后才继续执行。例如:
- 某个计算在其需要的资源都初始化完成之后执行;
- 某个服务在其所有依赖的服务都启动之后才启动;
- 游戏中所有的玩家都就绪才继续执行。
CountDownLatch是一种灵活的闭锁实现,它可以使一个或多个线程等待一组时间发生。闭锁状态包括一个计数器,该计数器初始化为一个正数,表示需要等待的事件数量,countDown方法表示递减计数器,表示一个事件发生了,而await方法等待直到计数器为0,表示所有事件都已经发生。如果计数器的值非零,那么就会一直等待下去,或者等待中被打断,或者超时。
例子(计算所有线程运行时间):
package thread.semaphore;
import java.util.concurrent.CountDownLatch;
public class TestHarness {
public static long timeTask(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 thread = new Thread() {
@Override
public void run() {
try {
startGate.await();
task.run();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
endGate.countDown();
}
}
};
thread.start();
}
long start = System.currentTimeMillis();
startGate.countDown();
endGate.await();
long end = System.currentTimeMillis();
return end-start;
}
public static void main(String[] args) throws InterruptedException {
long time = timeTask(5, new Runnable() {
@Override
public void run() {
try {
Thread.sleep((int)(500 * Math.random()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println("all task use " + time + "ms");
}
}
上例中使用了两个闭锁,一个起始门(startGate),一个结束门(endGate)。起始门的计数器值初始化为1,结束门是线程数,每个线程首先要做的就是在起始门上等待多有的线程都就绪后才开始执行。而每个线程最后要做的事就是调用结束门countDown方法减1,这能高效的等待所有的线程都工作完成,这样可以统计消耗的时间。
其他方法
如果有某个线程处理的比较慢,我们不可能让主线程一直等待,所以我们可以使用另外一个带指定时间的await方法,await(long time, TimeUnit unit): 这个方法等待特定时间后,就会不再阻塞当前线程。join也有类似的方法。
注意:计数器必须大于等于0,只是等于0时候,计数器就是零,调用await方法时不会阻塞当前线程。CountDownLatch不可能重新初始化或者修改CountDownLatch对象的内部计数器的值。
二、信号量
计数信号量(Counting Semaphore)用来控制同时访问某个特定资源的操作数量。计数信号量还可以用来实现某种资源池(如:数据库连接池),或者对容器施加边界。
Semaphore中管理着一组虚拟的许可(permit),许可的数量可以通过构造器来指定。在执行操作时可以先获取许可(只要还有剩余的许可),并在使用之后释放许可。如果没有许可,那么aquire将阻塞指定获取许可(或者直到被中断或者操作超时)。release将返回一个许可给信号量。计算信号量的一种简化形式就是二值信号量,即初始值为1的Semaphore。二值信号量可以用作互斥体(mutex),并具备不可重入的语义:谁拥有这个唯一的许可,谁就拥有了互斥锁。
例子(流量控制):
要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发的读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有十个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。
public class SemaphoreTest {
private static final int THREAD_COUNT = 30;
private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT);
private static Semaphore s = new Semaphore(10);
public static void main(String[] args) {
for (int i = 0; i < THREAD_COUNT; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
s.acquire();
System.out.println("save data");
s.release();
} catch (InterruptedException e) {
}
}
});
}
threadPool.shutdown();
}
}
其他方法:
- int availablePermits() :返回此信号量中当前可用的许可证数。
- int getQueueLength():返回正在等待获取许可证的线程数。
- boolean hasQueuedThreads() :是否有线程正在等待获取许可证。
- void reducePermits(int reduction) :减少reduction个许可证。是个protected方法。
- Collection getQueuedThreads() :返回所有等待获取许可证的线程集合。是个protected方法。
三、栅栏(同步屏障)
1. CyclicBarrier
栅栏(Barrier)类似于闭锁,它能阻塞一组线程直到某个时间发生。栅栏和闭锁的区别在于,所有线程必须都到达栅栏位置之后才能继续执行。闭锁用于等待事件,二栅栏用于等待其他线程。
闭锁是一次性操作,一旦进入终止状态就不能重置。CyclicBarrier可以使一定数量的线程反复在栅栏位置汇集,它在并行迭代算法中非常有用:这种算法通常将一个问题拆分成多个互不相关的子问题。当线程执行到栅栏位置时将调用await方法等待其他线程,这个方法阻塞直到所有线程都到达栅栏位置。当所有线程都到达栅栏位置,那么栅栏打开,所有线程都被释放。而栅栏将被重置以便下次使用。如果await被中断或者超时,那么栅栏被认为是打破了,所有线程的await都将被终止并抛出BrokenBarrierException。如果成功的通过栅栏,那么await将为每个线程返回一个唯一的到达索引号,我们可以利用这些索引来“选举”产生一个领导线程,并在下一次迭代中由该领导线程执行一些特殊的工作。
CyclicBarrier还可以利用构造函数传递一个Runnable,当成功通过栅栏时会执行它,但在阻塞线程被释放前不会执行。
例子(10个人去春游,规定达到一个地点后才能继续前行)
package thread.semaphore;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierWorker implements Runnable {
private int id;
private CyclicBarrier cyclicBarrier;
public CyclicBarrierWorker(int id, CyclicBarrier cyclicBarrier) {
this.id = id;
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
System.out.println(id + " th people wait, waitings " + cyclicBarrier.getNumberWaiting());
int returnIndex = cyclicBarrier.await();// 大家等待最后一个线程到达
System.out.println(id + " th people go, returnIndex:" + returnIndex);
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
int num = 10;
CyclicBarrier cyclicBarrier = new CyclicBarrier(num, new Runnable() {
@Override
public void run() {
System.out.println("go on together!");
}
});
for (int i=1; i<=num; i++) {
new Thread(new CyclicBarrierWorker(i, cyclicBarrier)).start();
}
}
}
运行结果:
2 th people wait, waitings 1
4 th people wait, waitings 1
3 th people wait, waitings 3
5 th people wait, waitings 4
6 th people wait, waitings 5
7 th people wait, waitings 6
8 th people wait, waitings 6
9 th people wait, waitings 7
10 th people wait, waitings 9
go on together!
10 th people go, returnIndex:0
2 th people go, returnIndex:8
4 th people go, returnIndex:7
3 th people go, returnIndex:6
1 th people go, returnIndex:9
7 th people go, returnIndex:3
9 th people go, returnIndex:1
6 th people go, returnIndex:4
5 th people go, returnIndex:5
8 th people go, returnIndex:2
2.Exchanger(两个线程进行数据交换)
另一种栅栏是Exchanger,它是一种两方(two-party)栅栏,各方在栅栏位置互换数据。当两方执行不对称操作时Exchanger会非常有用,例如一个线程向缓存中写数据,另一线程读数据,这两个线程可以使用Exchanger汇合,并将满的缓冲区和空的缓冲区互换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
例子(校对工作):
public class ExchangerTest {
private static final Exchanger<String> exgr = new Exchanger<String>();
private static ExecutorService threadPool = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
String A = "银行流水A";// A录入银行流水数据
exgr.exchange(A);
} catch (InterruptedException e) {
}
}
});
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
String B = "银行流水B";// B录入银行流水数据
String A = exgr.exchange("B");
System.out.println("A和B数据是否一致:" + A.equals(B) + ",A录入的是:"
+ A + ",B录入是:" + B);
} catch (InterruptedException e) {
}
}
});
threadPool.shutdown();
}
}
运行结果:
A和B数据是否一致:false,A录入的是:银行流水A,B录入是:银行流水B
其他方法
如果两个线程有一个没有到达exchange方法,则会一直等待,如果担心有特殊情况发生,避免一直等待,可以使用exchange(V x, long timeout, TimeUnit unit)设置最大等待时长。