昨天讲了 ReentrantLock, 今天就来讲讲 CountDownLatch, CyclicBarrier, Semaphore, Exchanger 这几个玩意, 如果你清楚了 ReentrantLock 原理, 那么搞懂这些就像喝水一样简单
CountDowLatch (倒计时器)
允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。
例如, 当一个线程完成 run 方法时, 调用一次 countDown() , 计数器减一, 当计数器值为 0 时, 才能执行 await() 方法后面的代码, 具体用法见API
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(() -> {task(); countDownLatch.countDown();}).start();
}
// 调用 await() 会阻塞当前线程
countDownLatch.await();
System.out.println("同学都走光了, 班长可以锁门了.");
}
public static void task(){
System.out.println(Thread.currentThread().getName() + "同学走了");
}
}
点看 CountDownLatch 查看源码, 就会发现, 这东西里面也是用了 AQS , 通过构造器设置一个 volatile 状态值, 通过观测这个状态值来实现 “倒计时”
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count);
}
// countDown(), 先是调用这个方法, 通过 CAS 修改值, 当状态值为 0 时, 就会唤醒阻塞的线程
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
CyclicBarrier (同步屏障)
让一组线程到达一个屏障时被阻塞, 直到最后一个线程到达屏障时, 屏障才会开门, 所有被屏障拦截的线程才会继续运行
通俗点来讲, 就是与上面那货相反, 这玩意每调用一次 await , 就将状态值 -1, 当状态值达到 0 时, 就执行指定任务 (如果存在), 然后唤醒所有线程
// 当有七个线程到达屏障, 优先执行构造器里面的任务,然后再放行被屏障拦截的线程
static CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> System.out.println("七颗龙珠已经集齐, 可以召唤神龙了!"));
public static void main(String[] args) {
for (int i = 0; i < 7; i++) {
new Thread(() -> task()).start();
}
}
public static void task() {
System.out.println(Thread.currentThread().getName() + "集齐了一颗龙珠!");
try {
// 到达屏障, 被阻塞, 直到屏障开门
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
查看 await() 源代码, 这玩意和 AQS 同样有一腿
// await() -> dowait(false, 0L)
// 传入 false, 和 0L 进来
final ReentrantLock lock = this.lock;
lock.lock();// 获取同步状态去操作
try {
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
// 如果当前线程被打断过, 唤醒所有被屏障阻塞的线程, 然后抛出异常
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
// 可以看到, 每执行一次 await(), 计数器 -1
int index = --count;
if (index == 0) { // tripped
boolean ranAction = false;
try {
// 当计数器等于 0 时, 就先执行指定任务
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
// 唤醒所有被屏障阻塞的线程
breakBarrier();
}
}
// 循环, 直到阻塞, 线程被打断, 或者时间超时
for (;;) {
try {
if (!timed)// 传进来的参数, 为 false, 取反, 继续执行
// 之后调用 AQS 的 ConditionObject#await 方法阻塞当前线程
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
CountDownLatch 和 CyclicBarrier 的区别
CountDownLatch的计数器只能使用一次, 而 CyclicBarrier 可以使用 reset() 方法重置. 所以 CyclicBarrier 能处理更复杂的业务场景, 比如说, 如果计算发生错误, 可以重置计数器, 并让线程重新执行一次
CyclicBarrier 还提供了其他方法, getNumberWaiting() 返回 CyclicBarrier 被阻塞的线程数量, isBroken() 返回阻塞的线程是否被中断过
Semaphore(信号量)
一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire() ,然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。
Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数
通俗点来讲, 每次去海底捞都要排队, 而信号量相当于海底捞一次性可以接纳的最多顾客, 当有一桌人走了 (可以看做是线程), 我们 (看做是线程) 就顶上去
Semaphore semaphore = new Semaphore(3);//设置最大通行证数量, 最多同时只有三个线程持有通行证
for (int i = 0; i < 9; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取通行证
System.out.println(Thread.currentThread().getName() + "获取到通行证了!");
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}).start();
}
//结果: 每三组打印一次
查看 acquire() 源码, 发现这玩意和 AQS 同样脱不了关系, 里面同样使用了 公平锁
, 非公平锁
// 在创建 Semaphore 对象时, 默认使用非公平锁, 通过构造器并设置了状态值
// acquireSharedInterruptibly(1) -> tryAcquireShared(int) -> nonfairTryAcquireShared(int)
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires; // 让状态值 -1
if (remaining < 0 ||
compareAndSetState(available, remaining))// 利用 CAS 修改状态值
return remaining;
}
}
-----------------
// 当发现状态值为 < 0 时, 就会来到这里: doAcquireSharedInterruptibly(int)
final Node node = addWaiter(Node.SHARED);// 以共享模式保存节点并加入队列
boolean failed = true;
try {
// 通过自旋去获取 '通行证' (类似 ReentrantLock 的自旋)
for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 只有当前线程节点的前驱结点是头结点, 回到上面那个方法,
// 如果状态值修改后 >= 0 , 才能成功获取到通行证, 线程才能去执行任务
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
如果有对 AQS 不了解的, 可以看我昨天写的博客
Exchanger
该类用于线程间的数据交换, 他提供一个同步点, 在这个同步点, 两个线程可以交换彼此数据
如果两个线程没有一起执行 exchange() , 则会一直等待下去, 该方法提供了重载, 可以设置参数实现超时退出
static Exchanger<String> exchanger = new Exchanger<>();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> a(), "a() ").start();
Thread.sleep(1000);
new Thread(() -> b(), "b() ").start();
}
static void a(){
String a = Thread.currentThread().getName() + "的数据!";
try {
a = exchanger.exchange(a);
System.out.println("a = " + a);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static void b(){
String b = Thread.currentThread().getName() + "的数据!";
try {
b = exchanger.exchange(b);
System.out.println("b = " + b);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 运行结果 :
// b = a() 的数据!
// a = b() 的数据!