目标

三个线程分别打印a,b,c,需要保证打印顺序是abc

思路

需要保证线程的执行顺序,本质还是线程之间通信,线程之间的通信有以下几种方式

wait()/notify()/notifyAll()

使用wait/notify,需要通过临时变量来控制,当前是否打印,在线程打印完毕后更新临时变量的值,并唤醒所有等待中的线程,等待中的线程唤醒后,通过比较新的临时变量值,是否是自己想要的值来决定是继续等待,还是执行打印。

比方说a,b,c,对应的打印条件分别是flag=1,2,3,那么当flag=1的时候,a执行打印,然后flag+1,调用notifyAll()唤醒其他线程;此时flag已经是2,线程b唤醒后,比较flag 发现是2,执行打印逻辑,线程c被唤醒后,发现flag是2,但是自己的打印条件是flag=3,继续等待。通过while循环来控制等待-比较-更新的逻辑,代码如下

class WaitNotifyPrinter {
/*** 循环次数*/
private int loopNum;
private int flag;
public WaitNotifyPrinter(int loopNum, int flag) {
this.loopNum = loopNum;
this.flag = flag;
}
/*** 打印消息** @param message 要打印的内容* @param current 当前flag* @param next 要唤醒的flag*/
public void print(String message, int current, int next) {
synchronized (this) {
for (int i = 0; i < loopNum; i++) {
//判断flag是否和当前一致,一致就打印 while (flag != current) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(message);
//更新flag flag = next;
//更新完需要唤醒正在等待的线程 notifyAll();
}
}
}
}

测试代码

WaitNotifyPrinter p = new WaitNotifyPrinter(5, 1);
new Thread(() -> {
p.print("a", 1, 2);
}).start();
new Thread(() -> {
p.print("b", 2, 3);
}).start();
new Thread(() -> {
p.print("c", 3, 1);
}).start();
LockSupport.park()/unpark()

与wait/notify不同的是,notify唤醒的是所有等待的线程,而LockSupport.park()可以精确到某一个需要唤醒的线程,因此相比wait/notify,无需通过flag来判断线程是否需要打印,只要线程能正确被唤醒,那么即可执行打印逻辑,基于以上代码做小小的改动即可实现

class LockSupportPrinter {
private int loopNum;
public LockSupportPrinter(int loopNum) {
this.loopNum = loopNum;
}
public void print(String message, Thread current, Thread next) {
for (int i = 0; i < loopNum; i++) {
//阻塞当前线程 LockSupport.park(current);
//线程被唤醒后打印 System.out.print(message);
//唤醒下一个线程 LockSupport.unpark(next);
}
}
}

测试代码

LockSupportPrinter lockSupportPrinter = new LockSupportPrinter(5);
a = new Thread(() -> {
lockSupportPrinter.print("a", a, b);
});
b = new Thread(() -> {
lockSupportPrinter.print("b", b, c);
});
c = new Thread(() -> {
lockSupportPrinter.print("c", c, a);
});
a.start();
b.start();
c.start();

//主线程唤醒a,不需要等待a启动后就可以唤醒,因为提前唤醒,下一次LockSupport.park(a)将不被阻塞 LockSupport.unpark(a);

Condition.await()/signal()/signalAll()

Condition是J.U.C并发包下重入锁ReentrantLock的工具类,其功能和wait/notify类似,我们知道wait/notify需要拿到对象的锁才能执行唤醒操作,但是调用notifyAll唤醒的是所有等待中的线程,与Objct.wait()不一样的是,一个ReentrantLock可以拥有多个condition,通过调用指定condition的await()/signal()方法,可以唤醒指定condition中等待的线程。

那么本例子中,三个线程分别打印a,b,c,可以理解为3个condition,打印A的conditionA,打印B的conditionB,打印c的conditionC,在conditionA被唤醒后,说明a打印完了,此时唤醒conditionB,让B线程打印b,然后唤醒conditionC,然后C线程打印c并唤醒conditionA,具体代码见下:

class ConditionPrinter {
private int loopNum;
private ReentrantLock lock;
public ConditionPrinter(int loopNum, ReentrantLock lock) {
this.loopNum = loopNum;
this.lock = lock;
}
public void print(String message, Condition current, Condition next) {
lock.lock();
try {
for (int i = 0; i < loopNum; i++) {
try {
//当前线程等待唤醒 current.await();
//当前线程已经唤醒,执行打印 System.out.print(message);
//唤醒下一个线程 next.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock();
}
}
}

测试代码

ReentrantLock lock = new ReentrantLock();
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
Condition conditionC = lock.newCondition();
ConditionPrinter conditionPrinter = new ConditionPrinter(5, lock);
new Thread(() -> {
conditionPrinter.print("a", conditionA, conditionB);
}).start();
new Thread(() -> {
conditionPrinter.print("b", conditionB, conditionC);
}).start();
new Thread(() -> {
conditionPrinter.print("c", conditionC, conditionA);
}).start();
//等打印a的线程启动后,主线程唤醒该线程 try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
//需要拿到锁才能调用condition方法 lock.lock();
try {
conditionA.signal();
} finally {
lock.unlock();
}