接上一篇《Java并发系列(8)——并发容器》
7 并发工具类
除了之前讲到的并发容器,JDK 还提供了一些方便易用的并发工具类。
7.1 Semaphore
Semaphore 主要用于控制并发量。
7.1.1 核心方法
Semaphore 的核心方法就两个:
- acquire():取走一个 permit,成功拿到了 permit 才能继续干活,否则就阻塞直到拿到为止;
- release():放入一个 permit,通常干完活之后把之前 acquire 的 permit 再放回去。
7.1.2 demo
package per.lvjc.concurrent.util;
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
//小区门口一共 2 辆共享单车
private static Semaphore bicycles = new Semaphore(2);
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
String name = Thread.currentThread().getName();
System.out.println(name + ": 社畜的一天开始了");
timeFlows();
boolean findBicycle = bicycles.tryAcquire();
if (findBicycle) {
System.out.println(name + ": 竟然还有一辆共享单车,骑车上班");
timeFlows();
System.out.println(name + ": 下班还车");
bicycles.release();
} else {
System.out.println(name + ": 没车了,走路上班吧");
}
System.out.println(name + ": 社畜的一天结束了");
};
for (int i = 1; i <= 6; i++) {
System.out.println("------ 星期 " + i + " ------");
new Thread(runnable, "Jack").start();
new Thread(runnable, "Rose").start();
new Thread(runnable, "Nobody").start();
Thread.sleep(500);
}
}
private static void timeFlows() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果:
------ 星期 1 ------
Jack: 社畜的一天开始了
Rose: 社畜的一天开始了
Nobody: 社畜的一天开始了
Jack: 竟然还有一辆共享单车,骑车上班
Rose: 竟然还有一辆共享单车,骑车上班
Nobody: 没车了,走路上班吧
Nobody: 社畜的一天结束了
Jack: 下班还车
Rose: 下班还车
Jack: 社畜的一天结束了
Rose: 社畜的一天结束了
------ 星期 2 ------
Jack: 社畜的一天开始了
Rose: 社畜的一天开始了
Nobody: 社畜的一天开始了
Jack: 竟然还有一辆共享单车,骑车上班
Rose: 竟然还有一辆共享单车,骑车上班
Nobody: 没车了,走路上班吧
Nobody: 社畜的一天结束了
Jack: 下班还车
Jack: 社畜的一天结束了
Rose: 下班还车
Rose: 社畜的一天结束了
------ 星期 3 ------
(省略......)
这里,通过 tryAcquire() 方法尝试骑走一辆共享单车,不阻塞,有就返回 true,否则返回 false;
因为 Semaphore 设置了只有 2,所以只有 2 个人可以骑车,剩下一个人得走路;
因为骑走车的人又还回来了,所以第二天又有 2 辆车可以骑。
7.1.3 注意点
- 构造方法设的参数仅仅是初始值;
- Semaphore 跟 Lock 不一样,Lock 在释放锁之前必须获得锁,Semaphore 可以不 acquire 直接 release;
- fair 与 unfair 这点与 Lock 是一样的;
- Semaphore 有个 acquireUninterruptibly 方法,acquire 时如果获取不到则阻塞并且不可被打断,但别真的没事就去 interrupt 一下,线程会从阻塞中被唤醒,然后发现还是 acquire 不到,又重新阻塞,损伤效率的。
7.1.4 手写
相比前面已经讲过的 AQS 和 ConcurrentHashMap 来说,Semaphore 算是非常简单的了,所以我们来自己实现一个 Semaphore。
先明确核心功能:
- acquire:
- 获取一个(或多个)permit——需要一个变量来存储当前 permit 数量,并且要保证并发可见性;
- 取走 permit 之后,修改剩余 permit 数量——并发修改要保证原子性;
- 如果 permit 数量不足,当前线程要阻塞——等待和通知;
- release:
- 放入一个(或多个)permit;
- 修改 permit 数量;
- 如果有线程在等待,将其唤醒。
实现方案:
- 保证可见性:
- synchronized;
- volatile;
- 保证原子性:
- synchronized;
- cas;
- 线程的等待和通知:
- wait / notify;
- await / signal;
- park / unpark(不同于上面两个,不会释放锁)。
所以实现方案有多种,这里选用 volatile + cas + park / unpark:
package per.lvjc.concurrent.util;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.LockSupport;
/**
* @author lvjc
* @date 2020/9/7
*/
public class LvjcSemaphore {
/**
* cas 操作需要 Unsafe,当然也可以用 JDK 封装好的 AtomicInteger
*/
private static final long permitOffset;
private static final Unsafe unsafe;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
permitOffset = unsafe.objectFieldOffset(LvjcSemaphore.class.getDeclaredField("permits"));
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
}
/**
* volatile 保证并发可见性
*/
private volatile int permits;
/**
* 一个并发非阻塞容器来保存处于等待中的 Thread
*/
private ConcurrentLinkedQueue<Thread> queue;
public LvjcSemaphore(int permits) {
this.permits = permits;
this.queue = new ConcurrentLinkedQueue<>();
}
public void acquire() throws InterruptedException {
//自旋
for (;;) {
int available = permits;
// permit 数量不足
if (available < 1) {
// 当前线程进入等待队列
queue.offer(Thread.currentThread());
// 阻塞当前线程,
// 不必担心其它线程已经先从队列取出当前 thread 进行了 unpark,导致下面的 park 无限阻塞,
// 如果 unpark 发生在 park 之前,被 unpark 的线程(如果已经 start)下一次 park 不会阻塞
// 但还是要在 park 之前再检查一次,以免在当前线程放入队列之前,其它线程已经 release,错过唤醒导致永远阻塞
if (permits < 1) {
LockSupport.park();
}
// 醒来之后
if (Thread.interrupted()) {
// 如果是被 interrupt 叫醒的,抛出 InterruptedException
throw new InterruptedException();
}
}
// cas 修改 permit 变量 -1
else if (casPermit(available, available - 1)) {
// 修改成功,退出
break;
}
}
}
public void acquireUninterruptibly() {
for (;;) {
int available = permits;
if (available < 1) {
queue.offer(Thread.currentThread());
if (permits < 1) {
LockSupport.park();
}
if (Thread.interrupted()) {
//相比 acquire 方法,只在这里有区别,这里不抛异常,仅设置 interrupt 状态
Thread.currentThread().interrupt();
}
} else if (casPermit(available, available - 1)) {
break;
}
}
}
public boolean tryAcquire() {
for(;;) {
int available = permits;
if (available < 1) {
return false;
} else if (casPermit(available, available - 1)) {
// cas 失败要重新进入循环,不能直接 return false
return true;
}
}
}
public void release() {
// 自旋
for (;;) {
int available = permits;
// cas 修改 permit 数量 +1
if (casPermit(available, available + 1)) {
// 修改成功,因为只放入 1 个 permit,所以只叫醒 1 个线程
Thread waitingThread = queue.poll();
if (waitingThread != null) {
// 如果有线程正在等待,将其唤醒
LockSupport.unpark(waitingThread);
}
break;
}
}
}
private boolean casPermit(int expected, int permit) {
return unsafe.compareAndSwapInt(this, permitOffset, expected, permit);
}
}
以上,已经实现了 Semaphore 的基本功能,性能应该也还好。不过这只是一个 unfair 的 Semaphore,fair 模式大同小异。
这个实现与 JDK 没有太大区别,JDK 的 Semaphore 用的是 AQS,本质上也是 volatile + cas + park / unpark。不同的是 AQS 用的队列是 AQS 内部自己实现的,并且在何时入队和出队上面做了比较多的优化。
7.2 Exchanger
Exchanger 用于两个线程之间的数据交换。
7.2.1 核心方法
Exchanger 类的核心方法就一个:
- exchange():提交自己的数据,阻塞,直到另一个线程提交数据,然后醒来,交换数据;或者对方线程先提交了数据,正在阻塞,自己线程提交数据,得到对方数据,对方线程被唤醒拿到自己的数据。
7.2.2 demo
package per.lvjc.concurrent.util;
import java.util.concurrent.Exchanger;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class ExchangerDemo {
private static Exchanger<String> exchanger = new Exchanger<>();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + ": 发起交易,等待对方同意");
try {
// 这里仅仅用来阻塞,无所谓对方给什么数据,等待对方调用 exchange 方法
exchanger.exchange("");
} catch (InterruptedException e) {
System.out.println(threadName + ": 交易失败");
return;
}
// 对方也调用 exchange 方法提交数据之后,当前线程就会被唤醒走到这里
System.out.println(threadName + ": 对方已同意,交易开始");
String goods = null;
try {
//自己提交了 “屠龙宝刀 × 1”, goods 为对方提交的数据
goods = exchanger.exchange("屠龙宝刀 * 1", 30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
// 打断走这里
System.out.println(threadName + ": 交易失败");
} catch (TimeoutException e) {
// 超时走这里
System.out.println(threadName + ": 交易超时,已取消");
}
System.out.println(threadName + ": 交易成功,获得 " + goods);
}, "伊莉雅").start();
Thread.sleep(200);
new Thread(() -> {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + ": 收到交易申请");
try {
// exchange 方法可以提交 null,对方也会得到 null
exchanger.exchange(null);
} catch (InterruptedException e) {
System.out.println(threadName + ": 交易失败");
return;
}
System.out.println(threadName + ": 已接受申请,交易开始");
String goods = null;
try {
goods = exchanger.exchange("2000000 金币", 30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
System.out.println(threadName + ": 交易失败");
} catch (TimeoutException e) {
System.out.println(threadName + ": 交易超时,已取消");
}
System.out.println(threadName + ": 交易成功,获得 " + goods);
}, "士郎").start();
}
}
输出:
伊莉雅: 发起交易,等待对方同意
士郎: 收到交易申请
伊莉雅: 对方已同意,交易开始
士郎: 已接受申请,交易开始
士郎: 交易成功,获得 屠龙宝刀 * 1
伊莉雅: 交易成功,获得 2000000 金币
7.2.3 注意点
- exchange 必须且仅仅发生在两个线程之间;
- Exchanger 自己保护好,别被第三者横插一脚,数据就被第三者劫走了。
7.2.4 手写
实现思路:
- 定义一个 data 变量存储 exchange 的值;
- 当一个线程调用 exchange 方法时,如果它是先到的,就把自己的值存到 data 变量,然后阻塞自己等待后到的线程唤醒;
- 如果一个线程调用 exchange 方法是,发现自己是后到的,就从 data 变量取出先到的线程放入的值,再把自己的值放入 data 变量,然后唤醒先到的线程,让它从 data 变量取出自己的值;
- 难点在于,当存在多于两个线程时,要处理竞争,可能 AB 交换,可能 BC 交换,也可能 AC 交换,看谁竞争胜出。
package per.lvjc.concurrent.util;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LvjcExchanger<V> {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private Object data;
private Thread thread;
public V exchange(V value) throws InterruptedException {
try {
lock.lockInterruptibly();
Thread currentThread = Thread.currentThread();
// 其它线程还没有提交数据
while (thread == null || thread == currentThread) {
data = value;
thread = currentThread;
condition.await();
if (thread != currentThread) {
Object o = data;
data = null;
thread = null;
return (V) o;
}
}
Object o = data;
data = value;
thread = currentThread;
condition.signal();
return (V) o;
} finally {
lock.unlock();
}
}
public V exchange(V value, long i, TimeUnit unit) throws InterruptedException, TimeoutException {
long remaining = TimeUnit.MILLISECONDS.convert(i, unit);
long deadline = System.currentTimeMillis() + remaining;
try {
lock.lockInterruptibly();
Thread currentThread = Thread.currentThread();
while (thread == null || thread == currentThread) {
data = value;
thread = currentThread;
condition.await(remaining, TimeUnit.MILLISECONDS);
if (thread != currentThread) {
Object o = data;
data = null;
thread = null;
return (V) o;
} else {
remaining = deadline - System.currentTimeMillis();
if (remaining <= 0) {
throw new TimeoutException();
}
}
}
Object o = data;
data = value;
thread = currentThread;
condition.signal();
return (V) o;
} finally {
lock.unlock();
}
}
}
这里直接用锁实现了,因为 Exchanger 对多线程竞争的处理要比 Semaphore 复杂得多,用 cas 需要考虑各种并发场景。JDK 是用 cas 不加锁实现的,相应的,性能也要优秀得多。
7.3 CountDownLatch
主要用于两组线程之间的交互:
- 一组是开关线程,当条件满足时,打开开关;
- 一组是工作线程,当所有开关都打开后,开始执行相关逻辑。
7.3.1 核心方法
- CountDownLatch(int):构造方法,设定一个初始 count;
- await():使当前线程阻塞,直到 count == 0;
- countDown():使 count - 1;
通常:
- 预先设定好开关个数,即 count 值;
- 工作线程使用 await 阻塞,等待所有开关都被打开,即 count == 0;
- 开关线程判断工作条件是否满足,当条件满足时,打开开关,即 count - 1。
7.3.2 demo
小明和小红两个线程 await,股价变动和银证转账两个线程 countDown:
package per.lvjc.concurrent.util;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class CountDownLatchDemo {
private static CountDownLatch countDownLatch = new CountDownLatch(2);
private static int price = 70;
private static int money = 10000;
public static void main(String[] args) {
new Thread(() -> {
System.out.println("小明: 当前股价: " + price + " 元");
System.out.println("小明: 可用金额: " + money + " 元");
//可以换成 if, 因为 CountDownLatch 不能重用,只能阻塞一次
while (price * 1000 > money) {
try {
//阻塞等待条件达成
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("小明: 知道啦,买了 10 手");
}).start();
new Thread(() -> {
System.out.println("小红: 股价跌了,小明你买了吗?");
try {
//阻塞在同一个 CountDownLatch 上面
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("小红: " + price + " 啦");
System.out.println("小红: 哦,买了呀,小明好厉害!");
}).start();
new Thread(() -> {
System.out.println("股价变动...");
while (price > 65) {
try {
TimeUnit.MILLISECONDS.sleep(500);
System.out.println("股价: " + price);
} catch (InterruptedException e) {
e.printStackTrace();
}
--price;
}
//条件 1 达成
countDownLatch.countDown();
}).start();
new Thread(() -> {
System.out.println("银证转账中...");
try {
TimeUnit.MILLISECONDS.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
money += 60000;
System.out.println("证券账户转入 60000 元");
//条件 2 达成
countDownLatch.countDown();
}).start();
}
}
输出:
小明: 当前股价: 70 元
小明: 可用金额: 10000 元
小红: 股价跌了,小明你买了吗?
股价变动...
银证转账中...
股价: 70
股价: 69
股价: 68
证券账户转入 60000 元
股价: 67
股价: 66
小明: 知道啦,买了 10 手
小红: 65 啦
小红: 哦,买了呀,小明好厉害!
7.3.3 注意点
- CountDownLatch 正常情况不可重用,因为 count 只能减不能加,但这个“因为”可以用反射来打破,所以如果考虑反射,CountDownLatch 也是可以重用的;
示例,使用反射修改 CountDownLatch 的 count 值:
package per.lvjc.concurrent.util;
import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class CountDownLatchTest {
//初值为 1
private static CountDownLatch countDownLatch = new CountDownLatch(1);
public static void main(String[] args) {
new Thread(() -> {
System.out.println("begin");
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end");
}).start();
new Thread(() -> {
try {
//反射拿到 CountDownLatch 的 sync 变量
Field sync = CountDownLatch.class.getDeclaredField("sync");
sync.setAccessible(true);
//反射获取 sync 变量值,是 AQS 的子类
AbstractQueuedSynchronizer synchronizer = (AbstractQueuedSynchronizer) sync.get(countDownLatch);
//反射拿到 sync 变量类型的父类,即 AQS 的 state 变量
Field state = synchronizer.getClass().getSuperclass().getDeclaredField("state");
state.setAccessible(true);
//反射修改 AQS 的 state 变量值
state.set(synchronizer, 5);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
//此处打印 5
System.out.println(countDownLatch.getCount());
//countDown 一次后还有 4,所以 await 线程醒不来
countDownLatch.countDown();
}).start();
}
}
- CountDownLatch 设置的初始 count 与线程数没有任何关系,不管使用什么手段,只要把 count 变成 0,再把 await 的线程叫醒,await 的线程就可以接着往下跑。
7.3.4 手写
JDK 实现方案:
- 使用了 AQS,用 AQS 的 state 变量存储 count 值;
- await:借助于 AQS 共享锁机制,利用获取共享锁失败来阻塞,因此要设定 state != 0 时获取共享锁失败;
- countDown:释放共享锁,不过要反过来,释放锁时,state 不增反减。
这里的实现方案:
- 不使用 AQS,定义一个 count 变量;
- 因为不用 AQS,所以还需要一个队列存放 await 的所有线程;
- await:考虑使用 park 阻塞,被唤醒时检查 count 值是否为 0,不为 0 继续阻塞;
- countDown:countDown 一次,count - 1,减到 0 时使用 unpark 唤醒队列里阻塞着的所有线程。
package per.lvjc.concurrent.util;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;
public class LvjcCountDownLatch {
/**
* 不使用 Unsafe 了,直接用 JDK 封装好的 Atomic 类
*/
private AtomicInteger atomicInteger;
private ConcurrentLinkedQueue<Thread> queue;
public LvjcCountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.atomicInteger = new AtomicInteger(count);
this.queue = new ConcurrentLinkedQueue<>();
}
public void await() throws InterruptedException {
while (atomicInteger.get() != 0) {
queue.offer(Thread.currentThread());
//注意必须再判断一次
if (atomicInteger.get() != 0) {
LockSupport.park();
}
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
}
public void countDown() {
if (atomicInteger.get() == 0) {
return;
}
if (atomicInteger.decrementAndGet() == 0) {
Thread thread;
while ((thread = queue.poll()) != null) {
LockSupport.unpark(thread);
}
}
}
}
7.4 CyclicBarrier
Barrier:障碍,在一组线程的执行过程中设一个障碍,必须所有线程都跑到障碍处,才能继续往下跑,否则全都被阻塞在障碍处;
Cyclic:循环,可以重用。
7.4.1 核心方法
核心方法:
- CyclicBarrier(int, Runnable):
- int:线程数,必须有这个数量的线程数跑到障碍处,大家才能继续往下跑;
- Runnable:barrier action,当所有线程都到达障碍处时,可以先完成一个任务,然后再让所有的线程继续往下跑;
- await():表示当前线程到达障碍处;
- await(long, TimeUnit):当前线程已到达障碍处,并且只会在障碍处等待指定的时间,超时还有线程没到,就不等了,自己先溜;
- reset():障碍重置,原来的障碍失效,在原障碍处等待的所有线程就懵逼了,抛出 BrokenBarrierException;
- isBroken():障碍破了,有三种情况:
- timeout:有线程已经等不及先溜了;
- interrupt:有线程被从阻塞中打断;
- reset:障碍被重置,原障碍则 broken;
- barrier action exception:所有线程都到达障碍处之后,在执行 barrier action 时抛异常。
异常场景:
- 有线程还没 await 就已经抛异常了:对其它线程无影响,但有个副作用,有线程死在了 await 途中,所以这些线程永远到不齐了,可能导致其它线程永远在 await;
- 执行 barrier action 抛异常:barrier action 由最后到的线程执行,所以 barrier action 中的异常会在最后到的线程中抛出,而其它线程全都被唤醒并抛出 BrokenBarrierException;
- TimeoutException:等不及的线程自己先走,抛出 TimeoutException,其它所有已经 await 的线程全都被唤醒并抛出 BrokenBarrierException;
- InterruptedException:有线程 await 被打断,被打断的线程抛出 InterruptedException,其它所有已经 await 的线程全都被唤醒并抛出 BrokenBarrierException;
- reset:barrier 重置,所有已经 await 的线程全都被唤醒并抛出 BrokenBarrierException。
7.4.2 demo
package per.lvjc.concurrent.util;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
private static final CyclicBarrier barrier = new CyclicBarrier(2, () -> {
System.out.println("补魔...");
});
public static void main(String[] args) {
Runnable runnable = () -> {
try {
String name = Thread.currentThread().getName();
System.out.println(name + ": 准备到房间补魔...");
System.out.println(name + ": 进入房间");
barrier.await();
System.out.println(name + ": 准备到客厅补魔...");
System.out.println(name + ": 进入客厅");
barrier.await();
System.out.println(name + ": 准备到浴室补魔...");
System.out.println(name + ": 进入浴室");
barrier.await();
System.out.println(name + ": 魔补满了全身都是力量");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
};
new Thread(runnable, "士郎").start();
new Thread(runnable, "凛").start();
}
}
输出结果:
士郎: 准备到房间补魔...
士郎: 进入房间
凛: 准备到房间补魔...
凛: 进入房间
补魔...
凛: 准备到客厅补魔...
凛: 进入客厅
士郎: 准备到客厅补魔...
士郎: 进入客厅
补魔...
士郎: 准备到浴室补魔...
士郎: 进入浴室
凛: 准备到浴室补魔...
凛: 进入浴室
补魔...
凛: 魔补满了全身都是力量
士郎: 魔补满了全身都是力量
7.4.3 注意点
- 初始 count 不可变(不考虑反射);
- 一个线程 await,count 固定 -1。
7.4.4 实现原理
- ReentrantLock,Condition,await,signalAll;
- await 一个线程,count - 1,
- count != 0 时,Condition.await 阻塞
- count == 0 时,Condition.signalAll 唤醒所有线程,重置 CyclicBarrier,如果 barrier action 不为空,在 signalAll 之前要先执行 barrier action 的 run 方法(所以是在最后一个 await 的线程里执行的)。
7.5 Phaser
Phaser 是比 CountDownLatch 和 CyclicBarrier 功能更加灵活的一种 barrier。
7.5.1 替代 CountDownLatch 使用
以下代码实现:必须 CDE 线程全都跑完,AB 线程才能继续往下跑。
package per.lvjc.concurrent.util.phaser;
import java.util.Random;
import java.util.concurrent.Phaser;
import java.util.concurrent.TimeUnit;
public class PhaserDemo1 {
//因为有 CDE 三个线程作为控制条件,因此初值设为 3
private static Phaser phaser = new Phaser(3);
public static void main(String[] args) {
new Thread(() -> {
System.out.println("A begin");
//动态增加一个参与的线程数,现在从 3 变成 4 了
phaser.register();
//在此阻塞
phaser.arriveAndAwaitAdvance();
//阻塞被唤醒后做自己的业务
System.out.println("A end");
}, "A").start();
Runnable runnable = () -> {
String name = Thread.currentThread().getName();
System.out.println(name + " begin...");
randomSleep();
System.out.println(name + " end...");
//作为控制条件的线程,则是所有工作完成后 arrive,不需要 awaitAdvance
phaser.arrive();
};
new Thread(runnable, "C").start();
//B 线程穿插在 CDE 线程中间跑
new Thread(() -> {
System.out.println("B begin");
phaser.register();
phaser.arriveAndAwaitAdvance();
System.out.println("B end");
}, "B").start();
new Thread(runnable, "D").start();
new Thread(runnable, "E").start();
}
private static void randomSleep() {
Random random = new Random(System.currentTimeMillis());
try {
TimeUnit.MILLISECONDS.sleep(random.nextInt(5000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
7.5.2 替代 CyclicBarrier 使用
上一节中 CyclicBarrier 的 demo,这里用 Phaser 实现:
package per.lvjc.concurrent.util.phaser;
import java.util.concurrent.Phaser;
public class PhaserTest2 {
private static Phaser phaser = new Phaser(2) {
// 对应 CyclicBarrier 的 barrier action,
// 但相比 Runnable 的无参,这里有两个参数,可以做一些额外的判断
// 相同的是,也是在最后一个到达的线程执行
@Override
protected boolean onAdvance(int phase, int registeredParties) {
System.out.println("补魔...");
return super.onAdvance(phase, registeredParties);
}
};
public static void main(String[] args) {
Runnable runnable = () -> {
String name = Thread.currentThread().getName();
System.out.println(name + ": 准备到房间补魔...");
System.out.println(name + ": 进入房间");
//对应 CyclicBarrier#await()
phaser.arriveAndAwaitAdvance();
System.out.println(name + ": 准备到客厅补魔...");
System.out.println(name + ": 进入客厅");
phaser.arriveAndAwaitAdvance();
System.out.println(name + ": 准备到浴室补魔...");
System.out.println(name + ": 进入浴室");
phaser.arriveAndAwaitAdvance();
System.out.println(name + ": 魔补满了真舒服");
};
new Thread(runnable, "士郎").start();
new Thread(runnable, "凛").start();
}
}
另外,对比 CyclicBarrier 的异常场景,Phaser 的行为有所不同:
- 对于 CyclicBarrier:除了抛出异常的线程以外,其它在等待的线程都会抛出 BrokenBarrierException;
- 对于 Phaser:除了抛出异常的线程以外,其它线程不会受到影响。
7.5.3 arrive,awaitAdvance,arriveAndAwaitAdvance,getPhase 方法
arrive:到达屏障处,记录已经 arrive 的数量 +1,然后不阻塞继续往下跑。
getPhase 和 arrive 的返回值有一定的联系:
-
getPhase 返回当前处于第几段;
-
arrive 返回当前到达的是第几段的屏障。
getPhase - arrive 可能有两个值:
- 0:arrive 之后,由于 arrive 的数量不足,还不能越过屏障;
- 1:当前就是最后一个 arrive 的,arrive 之后就越过屏障了,phase 就会进入下一个阶段,于是比 arrive 值大 1;
package per.lvjc.concurrent.util.phaser;
import java.util.concurrent.Phaser;
public class PhaserDemo3 {
private static Phaser phaser = new Phaser(2);
public static void main(String[] args) {
// arrive 的是第 0 个屏障,因此 arrive = 0
int arrive = phaser.arrive();
// 当前是第 0 个阶段,因此 phase = 0
int phase = phaser.getPhase();
System.out.println("arrive = " + arrive);
System.out.println("phase = " + phase);
System.out.println("-----------");
// 因为需要 arrive 两个才能越过屏障,所以上面第一次 arrive 没有越过屏障,当前仍处于第 0 阶段
// 第 2 次 arrive,此时到达的仍是第 0 阶段的屏障,因此 arrive = 0
arrive = phaser.arrive();
// 第 2 次 arrive 之后,已经越过屏障,进入了第 1 阶段,因此 phase = 1
phase = phaser.getPhase();
System.out.println("arrive = " + arrive);
System.out.println("phase = " + phase);
}
}
arrive,awaitAdvance,arriveAndAwaitAdvance 三个方法也有一些联系:
- arrive:到达屏障但不阻塞;
- awaitAdvance(int):接收一个 int 值,在指定的屏障处阻塞;
- arriveAndAwaitAdvance:到达屏障并阻塞;
- awaitAdvance(arrive()) = 到达屏障,在当前屏障处阻塞 = arriveAndAwaitAdvance()。
7.5.4 root 和 parent
Phaser 不仅可以单独使用,还可以将多个 Phaser 建立成树形结构,因此就有了 root 和 parent 的概念。
主要目的是在高并发场景下分散成多个相关联的 Phaser 减少竞争。
关于 root 和 parent 可以总结如下几点:
- 创建 Phaser 时,如果有 parent,并且设置的 parties(也就是构造方法中设置的那个 int 值,虽然后面也可以改)不为 0,那么将 parent 的 parties 加 1;
- 当 child Phaser 的 parties 被减到 0 的时候,自动将 parent 的 parties 减 1(与上一条呼应);
- 当 child Phaser 的所有 parties 都 arrive 的时候,在 parent 里面算作 1 个 arrive;
- 即使 child Phaser 所有的 parties 都 arrive,也不能越过屏障,必须等 parent 先越过屏障(非阻塞方法除外);
- 唤醒由 root Phaser 负责,遍历在队列中等待的线程逐一唤醒。
demo 如下:
package per.lvjc.concurrent.util.phaser;
import java.util.concurrent.Phaser;
public class PhaserTest4 {
private static Phaser rootPhaser = new Phaser(2);
private static Phaser phaser1 = new Phaser(rootPhaser, 1);
private static Phaser phaser2 = new Phaser(phaser1, 1);
private static Phaser phaser3 = new Phaser(phaser2, 1);
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("root begin");
System.out.println("root parties = " + rootPhaser.getRegisteredParties());
System.out.println("root arrived parties = " + rootPhaser.getArrivedParties());
rootPhaser.arriveAndAwaitAdvance();
System.out.println("root end");
}, "root").start();
Thread.sleep(1000);
new Thread(() -> {
System.out.println("1 begin");
System.out.println("phaser1 parties = " + phaser1.getRegisteredParties());
System.out.println("phaser1 arrived parties = " + phaser1.getArrivedParties());
phaser1.arriveAndAwaitAdvance();
System.out.println("1 end");
},"1").start();
Thread.sleep(1000);
new Thread(() -> {
System.out.println("2 begin");
System.out.println("phaser2 parties = " + phaser2.getRegisteredParties());
System.out.println("phaser2 arrived parties = " + phaser2.getArrivedParties());
phaser2.arriveAndAwaitAdvance();
System.out.println("2 end");
}, "2").start();
Thread.sleep(1000);
new Thread(() -> {
System.out.println("3 begin");
System.out.println("phaser3 parties = " + phaser3.getRegisteredParties());
int arrive = phaser3.arrive();
System.out.println("phaser3 arrived parties = " + phaser3.getArrivedParties());
phaser3.awaitAdvance(arrive);
System.out.println("3 end");
}, "3").start();
//rootPhaser.arrive();
}
}
输出结果:
root begin
root parties = 3
root arrived parties = 0
1 begin
phaser1 parties = 2
phaser1 arrived parties = 0
2 begin
phaser2 parties = 2
phaser2 arrived parties = 0
3 begin
phaser3 parties = 1
phaser3 arrived parties = 1
(未完,永久阻塞...)
可以看出:
- rootPhaser,phaser1,phaser2 调用 getRegisteredParties 方法得到的值都比构造方法中设的值多 1 个,而且也没有调用 register 方法,这就是上面总结的第一点;
- phaser3 只有 1 个 parties,并且它自己已经 arrive 了,但它依然被阻塞在屏障处,因为它在等 phaser2 醒来,phaser2 在等 phaser1 醒来,phaser1 在等 rootPhaser 醒来,但 rootPhaser 因为还少一个 arrive 所以醒不来,导致所有线程都被阻塞着,只需把最后注释的一行代码打开就可以了;
- 谁的 end 先打印出来是不确定的,因为 rootPhaser 会一下把在 phaser1,phaser2,phaser3 阻塞的线程全部唤醒。