建立三个线程A、B、C,A线程字母A,B线程字母B,C线程字母C,但是要求三个线程同时运行,并且实现交替顺序打印,即按照ABC ABC ABC的顺序打印。
这个题算是个臭名昭著的多线程题了,我是没想出来有啥地方需要这样使用多线程。无论是对现代分布式服务端应用,还是客户端Android应用开发都没有这样使用多线程的。保存中间状态对某些面试官来说是什么很困难的事情么。服务端操作系统基本都是非实时系统,从根本原理上讲并发保序性就是不保证的。这种强行反工程学的东西只是一个无脑的八股文。
这题有多种变种,无非是添加打印次数之类的截止点,添加计数器即可解决。
网上有使用Synchroized关键字和线程的wait,notify方法去解决的,写的实在是太复杂了。对现代java(java 8+版本),没有任何理由使用Synchroized,wait,notify这些Java 1.0时代的老古董,难以理解又使用不便。
Java 5后Java新增了李大神的并发工具包java.util.concurrent,里面有了可重入锁ReentrantLock,信号量Semaphore等,通过这些工具可以实现三线程交替打印,但是代码复杂度都不低。
实现三个线程交替顺序,核心思路无非是控制并发度和根据状态转移。控制并发度,同一时刻只能有一个线程运行,否则多线程并发,系统不保证顺序,就会乱打,或者一个线程打印多次。根据状态转移,也就是根据状态切换到一个状态的线程,必须记录当前状态,然后根据当前状态进行转移,才能实现顺序打印。
ReentrantLock锁的方案通过锁可以实现控制并发度
通过ReentrantLock,我们可以很方便的进行显式的锁操作,即获取锁和释放锁,对于同一个对象锁而言,统一时刻只可能有一个线程拿到了这个锁,此时其他线程通过lock.lock()来获取对象锁时都会被阻塞,直到这个线程通过lock.unlock()操作释放这个锁后,其他线程才能拿到这个锁。
按顺序释放锁可以实现根据状态转移
实现交替顺序打印的关键就在于,每个线程按顺序释放锁,并唤醒下个线程。此时已知当前线程的状态,释放锁后需要唤醒下个状态的线程。
但直接释放锁,唤醒哪个线程可以说是随机的。有没有办法唤醒指定线程呢?
其实唤醒的线程是不随机的,唤醒的是锁等待队列的头线程,但是哪个线程在排队时成为头线程是随机的,可以认为随机唤醒线程。
通过并发工具包java.util.concurrent提供的,配合ReentrantLock使用的Condition条件唤醒工具。可以实现唤醒指定线程。
与ReentrantLock搭配使用的Condition
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
condition.await();//this.wait();
condition.signal();//this.notify();
condition.signalAll();//this.notifyAll();
Condition是被绑定到Lock上的,必须使用lock.newCondition()才能创建一个Condition。从上面的代码可以看出,Synchronized能实现的通信方式,Condition都可以实现,功能类似的代码写在同一行中。但是Condition可以实现,指定Condition去阻塞线程和唤醒线程,而wait,notify是无目标的,所以说当前无任何必要使用wait,notify这类老家伙。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;public class MultiThreadPrint {public void threeThreadAlternatelyPrint(){Lock lock = new ReentrantLock();Condition A = lock.newCondition();Condition B = lock.newCondition();Condition C = lock.newCondition();var a = new Thread(()->{try {while (true) {lock.lock();System.out.print("A");B.signal(); // A执行完唤醒B线程A.await(); // A释放lock锁进入等待}} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}});var b = new Thread(()->{try {while (true) {lock.lock();System.out.print("B");C.signal();// B执行完唤醒C线程B.await();// B释放lock锁,进入等待}} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}});var c = new Thread(()->{try {while (true) {lock.lock();System.out.print("C");A.signal();// C执行完唤醒A线程C.await();// C释放lock锁进入等待}} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}});a.start();b.start();c.start();}
}
可以看到复杂度是着实不低。由于每个线程需要按顺序唤醒下一个线程,所以每个线程体内都包含了加锁解锁,等待唤醒的状态转移过程。
LockSupport最开始已经说了,交替打印核心思路无非是控制并发度和根据状态转移。锁的能力,主要还是控制代码段的竞争,只是暂停线程还有更方便的工具。那就是LockSupport,它可以很方便的挂起线程和唤醒指定线程,一次性满足控制并发度和根据状态转移的要求。
Thread t = new Thread(()->{LockSupport.park(); //可以直接挂起线程
})LockSupport.unpark(t); //可以指定线程唤醒
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.LockSupport;public class MultiThreadPrint {//保存当前状态static volatile String str = "";public void threeThreadAlternatelyPrint(){HashMap tMap = new HashMap();Runnable s = ()-> {//这段线程休眠代码可以不加 不加就是会打印的非常快 //打印太快可能会卡死编辑器或者空台 也可以通过限制打印次数来规避try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//每个线程启动后都直接暂停 等待开始信号 LockSupport.park();str = Thread.currentThread().getName();System.out.print(str);//唤醒下一个线程 线程传递关系保存在tMap里LockSupport.unpark(tMap.get(str));};var a = new Thread(s,"a");var b = new Thread(s,"b");var c = new Thread(s,"c");//tMap保存交替顺序tMap.put(a.getName(),b);tMap.put(b.getName(),c);tMap.put(c.getName(),a);a.start();b.start();c.start();//唤醒a线程 后续就会按顺序打印了LockSupport.unpark(a);}}
}
打印结果
abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc
核心思路是控制并发度和根据状态转移。Synchroized关键字和线程的wait,notify早已过时,ReentrantLock太过复杂,用线程挂起工具LockSupport可以较简单的实现。