一、通过对象锁及Wait / Notify方式实现
核心要点:
- 定义一个对象锁lock,通过synchronized关键字来保证它的访问。
- 交互逻辑是:
- 拿到锁之后打印,并自增;
- 唤醒另外一个线程来处理,自己释放锁。
- 线程的正常退出。不能让每个线程都处在循环的逻辑中。
代码实现:
public class AlternatelyPrint {
@SneakyThrows
public static void main(String[] args) {
waitNotify();
}
@SneakyThrows
@SuppressWarnings("all")
private static void waitNotify() {
Thread wOdd = new Thread(new WaitNotifyImpl(), "奇数线程");
Thread wEven = new Thread(new WaitNotifyImpl(), "偶数线程");
// 只能外部操作,来控制两个线程的顺序
wOdd.start();
TimeUnit.MILLISECONDS.sleep(3L);
wEven.start();
}
}
/**
* 对象通知模型,达到交替输出的效果
*/
class WaitNotifyImpl implements Runnable {
private static Integer count = 1;
/**
* 1. 终止的条件
* 2. 由于是不变的final对象,所以同时将它当作锁
*/
private static final Integer NUM = 100;
@Override
public void run() {
while (count <= NUM) {
synchronized (NUM) {
System.out.println(Thread.currentThread().getName() + " print count = " + count++);
NUM.notify();
// 该条件,保证线程能够正常结束
if (count <= NUM) {
try {
NUM.wait();
} catch (InterruptedException ignored) {
}
}
}
}
}
}
- 进入等待状态有一个判断count和NUM的逻辑。去掉后会导致两个线程都无法正常结束。
二、通过Exchanger来实现
概念解读
Exchanger是两个线程进行交互时数据传递的工具。交换数据依赖于其exchange方法:
public class Exchanger<V> {
@SuppressWarnings("unchecked")
public V exchange(V x) throws InterruptedException {
// 省略具体实现
}
}
- 入参是当前线程要进行交换的数据。
- 返回值是另一个线程返回过来的数据。
从入参和返回值就可以知道,当前线程执行exhcange方法的时候,必须要等到另外一个线程也执行到这个方法,交换数据之后,各自的线程才会继续执行下去。
换言之,这是一个阻塞的方法。
分析设计
1) 数据传递的过程骨架
从上面入参和返回值可以知道,exchange是一个双向交换数据的设计。
而我们这里要实现的是一个交替打印递增序列的问题。这里其实是只需要单向传递的过程,所以其中有一方传递的值始终是null。
| 交换次数 | 线程A | 线程B |
|---|---|---|
| ① | 1 | null |
| ② | null | 2 |
| ③ | 3 | null |
| ... | ... | ... |
| 九十九 | 99 | null |
| 一百 | null | 100 |
到这里基本的过程骨架就出来了。
2) 自增操作由谁负责?
然后就是自增和传递两个过程的先后,我们有两种方案:
- 生产者负责自增。拿到数据,先消费(打印),而后自增,再传递出去。
- 消费者负责自增。拿到数据,先自增,再消费(打印)它,然后再传递出去。
显然这不是重点,都能做到,所以我们直接采用的是第一种方案。
3) 线程正常结束
按照这个思路,代码其实是可以运行实现了,但不能让两个线程都一直跑着占用资源吧。
结合上面的表格看,问题的本质是,执行到100次的时候,如何让两个线程都决定不再继续和对方进行数据交换:
- 对于线程A(奇数线程)来说,它当前待传出的值是null,无法通过当前的值来判断应该结束。所以在这里,我们需要加一个变量,保存了上一次收到的值。
- 对于线程B(偶数线程)来说,它当前待传出的值是100,可以判断出应该结束 。
4) 最终代码实现
public class AlternatelyPrint {
@SneakyThrows
public static void main(String[] args) {
exchanger();
}
@SneakyThrows
@SuppressWarnings("all")
private static void exchanger() {
Exchanger<Integer> numExchanger = new Exchanger<>();
// 通过给的值来
Thread odd = new Thread(new ExchangerImpl(numExchanger, 1), "odd");
Thread even = new Thread(new ExchangerImpl(numExchanger, null), "even");
odd.start();
TimeUnit.MILLISECONDS.sleep(3L);
even.start();
}
}
/**
* Exchanger是两个线程交换空间,执行到exchange方法时候:<p> 
* 交出数据的线程会被阻塞,直到另一个线程与它进行交换。
*/
class ExchangerImpl implements Runnable {
private final Exchanger<Integer> numExchanger;
/**
* 当次待操作的值
*/
private Integer numLocal;
/**
* 缓存上一次操作的数据,用于判断结束条件
*/
private Integer lastReceived;
private static final int NUM = 100;
public ExchangerImpl(Exchanger<Integer> numExchanger, Integer numLocal) {
this.numExchanger = numExchanger;
this.numLocal = numLocal;
}
@Override
@SneakyThrows
public void run() {
// 1. 如果一个线程本地的数据是null, 且小于,则直接递出去
// 2. 如果本地的不是空值,则打印并自增,再递出去
while (this.numLocal == null || this.numLocal <= NUM) {
// 处理odd线程的退出
boolean oddOver = this.lastReceived != null && this.lastReceived == NUM;
if (oddOver) {
break;
}
if (this.numLocal != null) {
System.out.println(Thread.currentThread().getName() + " print count = " + numLocal++);
// 处理even线程的退出
if (this.numLocal == NUM + 1) {
break;
}
}
// 最后保存时候,一定要先将上次的值做缓存
this.lastReceived = this.numLocal;
this.numLocal = numExchanger.exchange(this.numLocal);
}
}
}
可以细看一下:
- 增加一个变量保存上次收到的值,每次要替换当前值的时候,将当前值缓存到该字段。
- 对于奇数线程和偶数线程的不同判断结束的方式。