书接上篇:juejin.cn/post/742655... (建议先读)
三、如何才能实现设想
🧐 对于按照线程均分思想,如何才能实现了?那就看看 ReentrantLock 吧
ReentrantLock: 可以创建多个Condition对象,每个Condition对象可以绑定一个或多个线程,实现对不同线程的精确控制,用于实现线程间的条件等待和唤醒
ReentrantLock、Condition 完全切合我的想法。 那么用 ReentrantLock 实现看看。
Java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockPrint {
static volatile int state = 1;
// 所有同步逻辑需要放在 lock.lock ~~lock.unlock 之间。类似于 Synchronized
static ReentrantLock lock = new ReentrantLock();
// 每个线程绑定一个条件
static Condition conditionA = lock.newCondition();
static Condition conditionB = lock.newCondition();
static Condition conditionC = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
for (; ; ) {
lock.lock();
try {
while (state != 1) {
try {
// 标配,如果不符合则 await()
conditionA.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("A");
state = 2;
// 唤醒线程 B
conditionB.signal();
} finally {
lock.unlock();
}
}
}, "A");
Thread threadB = new Thread(() -> {
for (; ; ) {
lock.lock();
try {
while (state != 2) {
try {
conditionB.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("B");
state = 3;
// 唤醒线程 C
conditionC.signal();
} finally {
lock.unlock();
}
}
}, "B");
Thread threadC = new Thread(() -> {
for (; ; ) {
lock.lock();
try {
while (state != 3) {
try {
conditionC.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("C");
state = 1;
// 唤醒线程 C
conditionA.signal();
} finally {
lock.unlock();
}
}
}, "A");
threadA.start();
threadB.start();
threadC.start();
threadA.join();
threadB.join();
threadC.join();
}
}
通过上面的方式,实现了构想。接下来看看性能使用情况:
- CPU 使用率大约 10%
- 线程运行时间下降到平均值
从结果看是不错的。 当线程不打印时,停止运行,需要时被唤醒执行打印,那么 ReentrantLock 是如何实现这个能力的呢?
3.2 ReentrantLock 原理是什么
ReentrantLock 实现了 Lock 接口获取锁与释放锁的相关方法,定义了同步器 Sync。
Sync继承了AbstractQueuedSynchronizer,是 AQS 的具体实现。
Sync有两个子类:NonfairSync(非公平锁同步器)与FairSync(公平锁同步器)。
NonfairSync与FairSync重写了lock方法与tryAcquire方法。
ReentrantLock 借助了 AQS 的能力(AQS 被使用得太多,后续会单独章节讲解),从而实现了上面功能。
当我再一次看线程的状态流转时,还有一组 API 可能也适合需求,那就是 LockSupport 类中的接口。
Java
import java.util.concurrent.locks.LockSupport;
public class LockSupportPrint {
// 定义三个常量。 通过 LockSupport.unpark() 唤醒不同的线程从而实现循环
public static Thread threadA, threadB, threadC;
public static void main(String[] args) throws InterruptedException {
threadA = new Thread(new Runnable() {
@Override
public void run() {
for (; ; ) {
// 这里的代码和 B、C 有一点点差异
// System.out.println("-----------");
System.out.println("A");
// 唤醒 B 线程
LockSupport.unpark(threadB);
LockSupport.park();
}
}
}, "A");
threadB = new Thread(new Runnable() {
@Override
public void run() {
for (; ; ) {
// 当前线程等待,直到被唤醒
LockSupport.park();
System.out.println("B");
// 唤醒线程 C
LockSupport.unpark(threadC);
}
}
}, "B");
threadC = new Thread(new Runnable() {
@Override
public void run() {
for (; ; ) {
// 当前线程等待,直到被唤醒
LockSupport.park();
System.out.println("C");
// 唤醒线程 A
LockSupport.unpark(threadA);
}
}
}, "C");
threadA.start();
threadB.start();
threadC.start();
// main 等 ABC 线程执行结束
threadA.join();
threadB.join();
threadC.join();
}
}
LockSupport 使用 private static final sun.misc.Unsafe UNSAFE 能力实现线程挂起和唤醒。
LockSupport 代码是最好理解的,逻辑清晰易懂,是不错的方式。看看性能消耗情况:
- CPU 使用下降,约 10%
- 线程运行时间下降了,33.3% 以下
显然 LockSupport 是一个很好的方案!
到这里我发现了一个规律:如果在当前线程中能让其线程阻塞,那么循环打印 ABC 是一件理论上可行的事情!
带着猜想,看了一下并发包中的一些工具类,于是发现了一些有趣的代码
四、阻塞等待规律探究落地
按照阻塞等待的思路,来实现代码。
先前写了《闲谈一下 Semaphore》和 《闲谈一下 CountDownLatch》,那么用 semaphore 、countDownLatch 试试看。
4.1 Semaphore (信号量)
关键 api:acquire() 如果不能获取,则等待
代码实现:
- 定义三个 Semaphore, 每个线程绑定一个
- 每个线程通过 acquire() 方法等待
- 通过上一个线程 调用 release() 释放许可,让本线程获取执行机会。
- 初始化 Semaphore 为 0,都等待; 在 main 线程 semaphoreA.release(), 使用线程开始运行
Java
import java.util.concurrent.Semaphore;
public class SemaphorePrint {
static Semaphore semaphoreA = new Semaphore(0);
static Semaphore semaphoreB = new Semaphore(0);
static Semaphore semaphoreC = new Semaphore(0);
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(new Runnable() {
public void run() {
for (; ; ) {
try {
// 阻塞等待,知道获取许可
semaphoreA.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("A");
// 让线程B 获得许可
semaphoreB.release();
}
}
}, "A");
Thread threadB = new Thread(new Runnable() {
public void run() {
for (; ; ) {
try {
// 阻塞等待,知道获取许可
semaphoreB.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("B");
// 让线程C 获得许可
semaphoreC.release();
}
}
}, "B");
Thread threadC = new Thread(new Runnable() {
public void run() {
for (; ; ) {
try {
// 阻塞等待,知道获取许可
semaphoreC.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("C");
// 让线程A 获得许可
semaphoreA.release();
}
}
}, "C");
// 启动
semaphoreA.release();
threadA.start();
threadB.start();
threadC.start();
threadA.join();
threadB.join();
threadC.join();
}
}
分析 cpu 使用情况、以及 running 使用时间
性能消耗:
- CPU 使用下降,约 10%
- 线程运行时间下降了,33.3% 以下
验证结果,发现效果还是不错。接下来看看 CountDownLatch
Java
import java.util.concurrent.CountDownLatch;
public class CountDownLatchPrint {
// 通过三个 CountDownLatch 来控制打印
static CountDownLatch countDownLatchA = new CountDownLatch(1);
static CountDownLatch countDownLatchB = new CountDownLatch(1);
static CountDownLatch countDownLatchC = new CountDownLatch(1);
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
for (; ; ) {
try {
// 等待
countDownLatchA.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 重置 countDownLatchA
countDownLatchA = new CountDownLatch(1);
// System.out.println("-----------");
System.out.println("A");
countDownLatchB.countDown();
}
}, "A");
Thread threadB = new Thread(() -> {
for (; ; ) {
try {
// 等待
countDownLatchB.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 重置 countDownLatchB
countDownLatchB = new CountDownLatch(1);
System.out.println("B");
countDownLatchC.countDown();
}
}, "B");
Thread threadC = new Thread(() -> {
for (; ; ) {
try {
// 等待
countDownLatchC.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 重置 countDownLatchC
countDownLatchC = new CountDownLatch(1);
System.out.println("C");
countDownLatchA.countDown();
}
}, "C");
threadA.start();
threadB.start();
threadC.start();
// 启动打印
countDownLatchA.countDown();
threadA.join();
threadB.join();
threadC.join();
}
}
分析性能消耗:
- cpu 使用率低
- 线程运行时间低
唯一不足:就是需要反复创建新的 countDownLatch 对象
4.3 CyclicBarrier
CyclicBarrier 和 CountDownLatch 很相似。 CyclicBarrier 当计数器为 0 后会重置为最初值,所以不用重新复制。
代码如下:
Java
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierPrint {
// 控制变量
static volatile int state = 1;
// 可以循环使用
static CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
for (; ; ) {
while (state == 1) {
// System.out.println("-------");
System.out.println("A");
state = 2;
try {
cyclicBarrier.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}, "A");
Thread threadB = new Thread(() -> {
for (; ; ) {
while (state == 2) {
System.out.println("B");
state = 3;
try {
cyclicBarrier.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}, "B");
Thread threadC = new Thread(() -> {
for (; ; ) {
while (state == 3) {
System.out.println("C");
state = 1;
try {
cyclicBarrier.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}, "C");
threadA.start();
threadB.start();
threadC.start();
threadA.join();
threadB.join();
threadC.join();
}
}
但是 CyclicBarrier 比较特殊。分析代码执行情况:
当 state = 1 的时候; A 线程停止; BC都在运行
当 state = 2 的时候;B 线程停止,同时 A 线程依然停止;只有 C继续运行
当 state = 3 的时候,满足 CyclicBarrier 的机制; ABC 三个线程又恢复运行
因此从理论上:C 线程 running 状态时间最长 (目前只能使用 CyclicBarrier 写出这样的代码)
性能消耗情况:
- CPU 使用 17% 左右。即 2 核
- C 线程使用占比100%,其他线程低于50%
相比其他几个工具类,耗能偏高了。
难道阻塞就可以?
那么试试阻塞队列吧
4.4 阻塞队列
以 LinkedBlockingDeque 为例子,核心api:
put(E e)
:如果双端队列未满,立即插入元素 e;如果双端队列已满, 阻塞 等待直到有空间。take()
:如果双端队列非空,移除并返回头部元素;如果双端队列为空, 阻塞 等待直到有元素可用
代码如下:
- 设置每个阻塞队列容量为 1
- A 线程往 B的阻塞队列插入元素;B 往 C 插入元素;C 往 A 插入元素。 每个线程从各自的阻塞队列取元素。如果没有元素就阻塞。
- 如果队列中有1个元素就阻塞
Java
import java.util.concurrent.LinkedBlockingDeque;
public class LinkedBlockingDequePrint {
// 使用阻塞队列完成
static LinkedBlockingDeque<String> dequeA = new LinkedBlockingDeque<>(1);
static LinkedBlockingDeque<String> dequeB = new LinkedBlockingDeque<>(1);
static LinkedBlockingDeque<String> dequeC = new LinkedBlockingDeque<>(1);
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
for (; ; ) {
try {
// 阻塞获取
String take = dequeA.take();
System.out.println("----------");
System.out.println(take);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
// 放入 B 队列
dequeB.put("B");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "A");
Thread threadB = new Thread(() -> {
for (; ; ) {
try {
String take = dequeB.take();
System.out.println(take);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
// 放入 C 队列
dequeC.put("C");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "B");
Thread threadC = new Thread(() -> {
for (; ; ) {
try {
String take = dequeC.take();
System.out.println(take);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
dequeA.put("A");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "C");
threadA.start();
threadB.start();
threadC.start();
// 启动
dequeA.put("A");
threadA.join();
threadB.join();
threadC.join();
}
}
SynchronousQueue 、DelayQueue 等阻塞队列的相关阻塞方法也是可以的
耗能分析
- cpu 使用率小于 10%
- 线程 running 时间小于 30%
通过阻塞方式,可以协助线程完成等待。通过条件的修改,使得多个线程可以按照特定顺序进行打印。
到目前为止,已经初步得到一个结论。可以阻塞其他线程都可以被用来实现 ABC 的循环打印。
特别说明:本次比较是基于上面的代码,有可能你通过这些工具能将代码写得更好!
4.5 方法汇总比较
简单比较
注意上面的写法不是标准答案,仅作参考。
五、最后
5.1 其他问题
问题一:Thread 调用 run() 方法而不是 start() 方法会怎么样?
答:可以调用,调用 run() 方法是同步调用,属于同一个线程,而 start() 是另外一个线程去执行方法,是两个线程
问题二: Thread 调用两次 start() 方法会怎么样?
答:会报错,每一个阶段都有一个自己的线程状态,调用 start() 方法会将线程状态从 new 编程 runnable。每次调用 start() 都会进行状态检测,所以报错
Java
if (threadStatus != 0)
throw new IllegalThreadStateException();
5.2 总结
通过深入的了解,以及各种性能的比较,对线程之间的协作有了更深入的理解;如果再遇到这道题的时候,至少不会手足无措了。
📽 故事结尾:很遗憾,那次面试很糟糕,但下次应该不会了。