内置锁和显式锁是两种比较基本的同步方式,除此之外,Java还提供了一些高级的同步工具:
1. Semaphores
2. CountDownLatch
3. CyclicBarrier
4. Phaser
5. Exchanger
一、控制资源访问:Semaphores (信号量).
当一个线程试图访问共享的资源时,它首先要获取许可。如果内部的信号量计数器是大于0的话,该线程即可获得许可,然后信号量的计数器-1。但是如果获取信号量时内部计数器是0的话,该线程就会阻塞(或者返回false)。有共享的资源被释放时,许可就会被释放,计数器+1。这是由java.util.concurrent.Semaphore类实现的
例子:
class Pool {
private static final int MAX_AVAILABLE = 100;
private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);
public Object getItem() throws InterruptedException {
available.acquire();
return getNextAvailableItem();
}
public void putItem(Object x) {
if (markAsUnused(x))
available.release();
}
// Not a particularly efficient data structure; just for demo
protected Object[] items = ... whatever kinds of items being managed
protected boolean[] used = new boolean[MAX_AVAILABLE];
protected synchronized Object getNextAvailableItem() {
for (int i = 0; i < MAX_AVAILABLE; ++i) {
if (!used[i]) {
used[i] = true;
return items[i];
}
}
return null; // not reached
}
protected synchronized boolean markAsUnused(Object item) {
for (int i = 0; i < MAX_AVAILABLE; ++i) {
if (item == items[i]) {
if (used[i]) {
used[i] = false;
return true;
} else
return false;
}
}
return false;
}
}
通过acquire (或者tryAcquire,非阻塞版本,如果获取许可失败返回false) 来获取许可,如果获取成功计数器-1,否则阻塞,方法响应中断(通过acquireUninterruptibly获取许可将不响应中断)。
通过release可以释放许可。
需要注意的是
1. acquire(and tryAcquire)和release可以一次请求/释放多个许可。当请求数量大于available的许可时会阻塞(tryAcquire返回false)。
2. Semaphore可以为公平的或非公平的,从并发性的角度考虑,默认是非公平的。
二、等待多个并发事件:CountDownLatch (倒数闸门)
CountDownLatch允许一个或者多个线程等待其他一些线程中的操作的完成。CountDownLatch维护一个计数器,并为其设定初始值,即要等待的任务。每当一个被等待的任务完成,就会使计数器-1,当计数器为0时,等待的线程将会被唤醒。
例子:
class Driver { // ...
public static void main() throws InterruptedException {
CountDownLatch doneSignal = new CountDownLatch(N);
Executor e = ...
for (int i = 0; i < N; ++i) // create and start threads
e.execute(new WorkerRunnable(doneSignal, i));
doneSignal.await(); // wait for all to finish
}
}
class WorkerRunnable implements Runnable {
private final CountDownLatch doneSignal;
private final int i;
WorkerRunnable(CountDownLatch doneSignal, int i) {
this.doneSignal = doneSignal;
this.i = i;
}
public void run() {
try {
doWork(i);
doneSignal.countDown();
} catch (InterruptedException ex) {} // return;
}
void doWork() { ... }
}
完成任务后,通过CountDown使计数器-1;通过await使主线程等待所有任务完成。
三、在某一点同步多个任务:CyclicBarrier (循环栅栏)
在某一点同步多个任务,这其实和CountDownLatch十分相似,但CyclicBarrier是可重用的。
例子:
class Solver {
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 Solver(float[][] matrix) {
data = matrix;
N = matrix.length;
barrier = new CyclicBarrier(N,
new Runnable() {
public void run() {
mergeRows(...);
}
});
for (int i = 0; i < N; ++i)
new Thread(new Worker(i)).start();
waitUntilDone();
}
}
线程中的await方法会一直阻塞,除非所有参与的线程都运行到了await方法。await响应中断,也可以抛出BrokenBarrierException。
四、运行阶段性并发任务:Phaser
也是一种可重用的栅栏,与CyclicBarrier以及CountDownLatch相似,但是更加灵活和强大。
Phaser把任务分成几个阶段,当某个阶段所有的参与者全部到达协议位置时,这个阶段就会结束,新的阶段就会开启。下面是更加具体的介绍:
1. 注册
与一般的栅栏不同的是,Phaser中注册的参与者的数量随着时间的推移是变化的。通过构造函数或者register(或者bulkRegister)方法,任务可以随时注册进来。而通过arriveAndDeregister方法又可以在任务到达时把任务从Phaser中解除。
2. 同步
a. 到达:当任务完成时调用arrive或arriveAndDeregister可以获得阶段号,并不阻塞。onArrive方法可以override,以用来定制化arrive后的操作
b. 等待:调用awaitAdvance(int phase)和awaitAdvanceInterruptibly(int phase),就可以等待目标阶段的其他参与线程,后者响应阻塞。而arriveAndAwaitAdvance等价于awaitAdvance(arrive())。如果想要删除到达的任务就可以调用awaitAdvance(arriveAndDeregister())
3. 终结
阶段或被终结,如果被终结的话,所有该阶段的参与者都不会再等待。相关方法:isTerminated,forceTermination
4. 分层
可以注册树状结构的阶段,即存在child和parent。当child的注册任务不为0时,child就会注册到parent上,否则就会被Deregister掉
5. 监控
通过getter可以获取到Phaser的信息。包括getRegisteredParties(), getArrivedParties(),getPhase(),getUnarrivedParties(),toString()
五、并发的线程间交换数据:Exchanger (交换器)
通过Exchanger可以实现线程间对象的相互传递。两个线程共享一个同步的点:Exchanger。和SynchronizedQueue左右相似,不同的是Exchanger是双向的,而SynchronizedQueue是从“生产者”到“消费者”的,而且SynchronizedQueue是没有内部空间的,仅仅相当于一个管道。
例子:
class FillAndEmpty {
Exchanger<DataBuffer> exchanger = new Exchanger<DataBuffer>();
DataBuffer initialEmptyBuffer = ... a made-up type
DataBuffer initialFullBuffer = ...
class FillingLoop implements Runnable {
public void run() {
DataBuffer currentBuffer = initialEmptyBuffer;
try {
while (currentBuffer != null) {
addToBuffer(currentBuffer);
if (currentBuffer.isFull())
currentBuffer = exchanger.exchange(currentBuffer);
}
} catch (InterruptedException ex) { ... handle ... }
}
}
class EmptyingLoop implements Runnable {
public void run() {
DataBuffer currentBuffer = initialFullBuffer;
try {
while (currentBuffer != null) {
takeFromBuffer(currentBuffer);
if (currentBuffer.isEmpty())
currentBuffer = exchanger.exchange(currentBuffer);
}
} catch (InterruptedException ex) { ... handle ...}
}
}
void start() {
new Thread(new FillingLoop()).start();
new Thread(new EmptyingLoop()).start();
}
}
Reference:
Java Documents Provided by Oracle
《Java 7 Concurrency Cookbook》