Synchronizer 类
Synchronizer
java.util.concurrent
中其他类别的有用的类也是同步工具。这组类相互协作,控制一个或多个线程的执行流。
Semaphore
、CyclicBarrier
、CountdownLatch
和 Exchanger
类都是同步工具的例子。每个类都有线程可以调用的方法,方法是否被阻塞取决于正在使用的特定同步工具的状态和规则。
Semaphore
Semaphore
类实现标准 Dijkstra 计数信号。计数信号可以认为具有一定数量的许可权,该许可权可以获得或释放。如果有剩余的许可权,acquire()
方法将成功,否则该方法将被阻塞,直到有可用的许可权(通过其他线程释放许可权)。线程一次可以获得多个许可权。
计数信号可以用于限制有权对资源进行并发访问的线程数。该方法对于实现资源池或限制 Web 爬虫(Web crawler)中的输出 socket 连接非常有用。
注意信号不跟踪哪个线程拥有多少许可权;这由应用程序来决定,以确保何时线程释放许可权,该信号表示其他线程拥有许可权或者正在释放许可权,以及其他线程知道它的许可权已释放。
计数信号的一种特殊情况是互斥,或者互斥信号。互斥就是具有单一许可权的计数信号,意味着在给定时间仅一个线程可以具有许可权(也称为二进制信号)。互斥可以用于管理对共享资源的独占访问。
虽然互斥许多地方与锁定一样,但互斥还有一个锁定通常没有的其他功能,就是互斥可以由具有许可权的线程之外的其他线程来释放。这在死锁恢复时会非常有用。
CyclicBarrier
CyclicBarrier
类可以帮助同步,它允许一组线程等待整个线程组到达公共屏障点。CyclicBarrier
是使用整型变量构造的,其确定组中的线程数。当一个线程到达屏障时(通过调用 CyclicBarrier.await()
),它会被阻塞,直到所有线程都到达屏障,然后在该点允许所有线程继续执行。该操作与许多家庭逛商业街相似 —— 每个家庭成员都自己走,并商定 1:00 在电影院集合。当您到电影院但不是所有人都到了时,您会坐下来等其他人到达。然后所有人一起离开。
认为屏障是循环的是因为它可以重新使用;一旦所有线程都已经在屏障处集合并释放,则可以将该屏障重新初始化到它的初始状态。
还可以指定在屏障处等待时的超时;如果在该时间内其余线程还没有到达屏障,则认为屏障被打破,所有正在等待的线程会收到BrokenBarrierException
。
下列代码将创建 CyclicBarrier
并启动一组线程,每个线程将计算问题的一部分,等待所有其他线程结束之后,再检查解决方案是否达成一致。如果不一致,那么每个工作线程将开始另一个迭代。该例将使用 CyclicBarrier
变量,它允许注册 Runnable
,在所有线程到达屏障但还没有释放任何线程时执行 Runnable
。
class Solver { // Code sketch void solve(final Problem p, int nThreads) { final CyclicBarrier barrier = new CyclicBarrier(nThreads, new Runnable() { public void run() { p.checkConvergence(); }} ); for (int i = 0; i < nThreads; ++i) { final int id = i; Runnable worker = new Runnable() { final Segment segment = p.createSegment(id); public void run() { try { while (!p.converged()) { segment.update(); barrier.await(); } } catch(Exception e) { return; } } }; new Thread(worker).start(); } }
CountdownLatch
类与 CyclicBarrier
相似,因为它的角色是对已经在它们中间分摊了问题的一组线程进行协调。它也是使用整型变量构造的,指明计数的初始值,但是与 CyclicBarrier
不同的是,CountdownLatch
不能重新使用。
其中,CyclicBarrier
是到达屏障的所有线程的大门,只有当所有线程都已经到达屏障或屏障被打破时,才允许这些线程通过,CountdownLatch
将到达和等待功能分离。任何线程都可以通过调用 countDown()
减少当前计数,这种不会阻塞线程,而只是减少计数。await() 方法的行为与 CyclicBarrier.await()
稍微有所不同,调用 await()
任何线程都会被阻塞,直到闩锁计数减少为零,在该点等待的所有线程才被释放,对 await()
的后续调用将立即返回。
当问题已经分解为许多部分,每个线程都被分配一部分计算时,CountdownLatch
非常有用。在工作线程结束时,它们将减少计数,协调线程可以在闩锁处等待当前这一批计算结束,然后继续移至下一批计算。
相反地,具有计数 1 的 CountdownLatch
类可以用作“启动大门”,来立即启动一组线程;工作线程可以在闩锁处等待,协调线程减少计数,从而立即释放所有工作线程。下例使用两个 CountdownLatche
。一个作为启动大门,一个在所有工作线程结束时释放线程:
class Driver { void main() throws InterruptedException { CountDownLatch startSignal = new CountDownLatch(1); CountDownLatch doneSignal = new CountDownLatch(N); for (int i = 0; i < N; ++i) // create and start threads new Thread(new Worker(startSignal, doneSignal)).start(); doSomethingElse(); // don't let them run yet startSignal.countDown(); // let all threads proceed doSomethingElse(); doneSignal.await(); // wait for all to finish } } class Worker implements Runnable { private final CountDownLatch startSignal; private final CountDownLatch doneSignal; Worker(CountDownLatch startSignal, CountDownLatch doneSignal) { this.startSignal = startSignal; this.doneSignal = doneSignal; } public void run() { try { startSignal.await(); doWork(); doneSignal.countDown(); } catch (InterruptedException ex) {} // return; } }
Exchanger
Exchanger
类方便了两个共同操作线程之间的双向交换;这样,就像具有计数为 2 的 CyclicBarrier
,并且两个线程在都到达屏障时可以“交换”一些状态。(Exchanger
模式有时也称为聚集。)
Exchanger
通常用于一个线程填充缓冲(通过读取 socket),而另一个线程清空缓冲(通过处理从 socket 收到的命令)的情况。当两个线程在屏障处集合时,它们交换缓冲。下列代码说明了这项技术:
class FillAndEmpty { Exchanger<DataBuffer> exchanger = new Exchanger<DataBuffer>(); DataBuffer initialEmptyBuffer = new DataBuffer(); DataBuffer initialFullBuffer = new DataBuffer(); class FillingLoop implements Runnable { public void run() { DataBuffer currentBuffer = initialEmptyBuffer; try { while (currentBuffer != null) { addToBuffer(currentBuffer); if (currentBuffer.full()) 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.empty()) currentBuffer = exchanger.exchange(currentBuffer); } } catch (InterruptedException ex) { ... handle ...} } } void start() { new Thread(new FillingLoop()).start(); new Thread(new EmptyingLoop()).start(); } }