题目描述以下:java
编写一个程序,开启三个线程,这三个线程的 ID 分别是 A、B 和 C,每一个线程把本身的 ID 在屏幕上打印 10 遍,要求输出结果必须按 ABC 的顺序显示,如 ABCABCABC... 依次递推
这是一道经典的多线程编程面试题,首先吐槽一下,这道题的需求非常奇葩,先开启多线程,而后再串行打印 ABC,这不是吃饱了撑的吗?不过既然是道面试题,就无论这些了,其目的在于考察你的多线程编程基础。就这道题,你要是写不出个三四种解法,你都很差意思说你学过多线程。哈哈开玩笑,下面就为你介绍一下本题的几种解法。面试
LockSupport 是java.util.concurrent.locks
包下的工具类,它的静态方法unpark()
和park()
能够分别实现阻塞当前线程和唤醒指定线程的效果,因此用它解决这样的问题简直是小菜一碟,代码以下:shell
public class PrintABC { static Thread threadA, threadB, threadC; public static void main(String[] args) { threadA = new Thread(() -> { for (int i = 0; i < 10; i++) { // 打印当前线程名称 System.out.print(Thread.currentThread().getName()); // 唤醒下一个线程 LockSupport.unpark(threadB); // 当前线程阻塞 LockSupport.park(); } }, "A"); threadB = new Thread(() -> { for (int i = 0; i < 10; i++) { // 先阻塞等待被唤醒 LockSupport.park(); System.out.print(Thread.currentThread().getName()); // 唤醒下一个线程 LockSupport.unpark(threadC); } }, "B"); threadC = new Thread(() -> { for (int i = 0; i < 10; i++) { // 先阻塞等待被唤醒 LockSupport.park(); System.out.print(Thread.currentThread().getName()); // 唤醒下一个线程 LockSupport.unpark(threadA); } }, "C"); threadA.start(); threadB.start(); threadC.start(); } }
执行结果以下:编程
ABCABCABCABCABCABCABCABCABCABC Process finished with exit code 0
这种方法就是直接使用 Java 的 synchronized 关键字,配合 Object 的 wait()
和notifyAll()
方法实现线程交替打印的效果,不过这种写法的复杂度和代码量都偏大。因为notify()
和notifyAll()
方法都不能唤醒指定的线程,因此须要三个布尔变量对线程执行顺序进行控制。另外要注意的就是,for 循环中的 i++
须要在线程打印以后执行,不然每次被唤醒后,不论是不是轮到当前线程打印都会执行i++
,这显然不是咱们想要的。代码以下 (通常B、C线程和A线程的执行逻辑相似,只在A线程代码中进行详细注释说明):多线程
public class PrintABC { // 使用布尔变量对打印顺序进行控制,true表示轮到当前线程打印 private static boolean startA = true; private static boolean startB = false; private static boolean startC = false; public static void main(String[] args) { // 做为锁对象 final Object o = new Object(); // A线程 new Thread(() -> { synchronized (o) { for (int i = 0; i < 10; ) { if (startA) { // 表明轮到当前线程打印 System.out.print(Thread.currentThread().getName()); // 下一个轮到B打印,因此把startB置为true,其它为false startA = false; startB = true; startC = false; // 唤醒其余线程 o.notifyAll(); // 在这里对i进行增长操做 i++; } else { // 说明没有轮到当前线程打印,继续wait try { o.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }, "A").start(); // B线程 new Thread(() -> { synchronized (o) { for (int i = 0; i < 10; ) { if (startB) { System.out.print(Thread.currentThread().getName()); startA = false; startB = false; startC = true; o.notifyAll(); i++; } else { try { o.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }, "B").start(); // C线程 new Thread(() -> { synchronized (o) { for (int i = 0; i < 10; ) { if (startC) { System.out.print(Thread.currentThread().getName()); startA = true; startB = false; startC = false; o.notifyAll(); i++; } else { try { o.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }, "C").start(); } }
执行结果以下:并发
ABCABCABCABCABCABCABCABCABCABC Process finished with exit code 0
使用 synchronized 锁机制的写法着实有些复杂,何不试试 ReentrantLock?这是java.util.concurrent.locks
包下的锁实现类,它拥有更灵活的 API,可以对多线程执行流程实现更精细的控制,特别是在搭配 Condition 使用的状况下,能够为所欲为地控制多个线程的执行顺序,来看看这个组合在本题中的使用吧,代码以下:工具
public class PrintABC { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); // 使用ReentrantLock的newCondition()方法建立三个Condition // 分别对应A、B、C三个线程 Condition conditionA = lock.newCondition(); Condition conditionB = lock.newCondition(); Condition conditionC = lock.newCondition(); // A线程 new Thread(() -> { try { lock.lock(); for (int i = 0; i < 10; i++) { System.out.print(Thread.currentThread().getName()); // 叫醒B线程 conditionB.signal(); // 本线程阻塞 conditionA.await(); } // 这里有个坑,要记得在循环以后调用signal(),不然线程可能会一直处于 // wait状态,致使程序没法结束 conditionB.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 在finally代码块调用unlock方法 lock.unlock(); } }, "A").start(); // B线程 new Thread(() -> { try { lock.lock(); for (int i = 0; i < 10; i++) { System.out.print(Thread.currentThread().getName()); conditionC.signal(); conditionB.await(); } conditionC.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }, "B").start(); // C线程 new Thread(() -> { try { lock.lock(); for (int i = 0; i < 10; i++) { System.out.print(Thread.currentThread().getName()); conditionA.signal(); conditionC.await(); } conditionA.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }, "C").start(); } }
执行结果以下:ui
ABCABCABCABCABCABCABCABCABCABC Process finished with exit code 0
semaphore中文意思是信号量,本来是操做系统中的概念,JUC下也有个 Semaphore 的类,可用于控制并发线程的数量。Semaphore 的构造方法有个 int 类型的 permits 参数,以下:操作系统
public Semaphore(int permits) {...}
其中 permits 指的是该 Semaphore 对象可分配的许可数,一个线程中的 Semaphore 对象调用acquire()
方法可让线程获取许可继续运行,同时该对象的许可数减一,若是当前没有可用许可,线程会阻塞。该 Semaphore 对象调用release()
方法能够释放许可,同时其许可数加一。Talk is cheap, show me the code!线程
public class PrintABC { public static void main(String[] args) { // 初始化许可数为1,A线程能够先执行 Semaphore semaphoreA = new Semaphore(1); // 初始化许可数为0,B线程阻塞 Semaphore semaphoreB = new Semaphore(0); // 初始化许可数为0,C线程阻塞 Semaphore semaphoreC = new Semaphore(0); new Thread(() -> { for (int i = 0; i < 10; i++) { try { // A线程得到许可,同时semaphoreA的许可数减为0,进入下一次循环时 // A线程会阻塞,知道其余线程执行semaphoreA.release(); semaphoreA.acquire(); // 打印当前线程名称 System.out.print(Thread.currentThread().getName()); // semaphoreB许可数加1 semaphoreB.release(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "A").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { semaphoreB.acquire(); System.out.print(Thread.currentThread().getName()); semaphoreC.release(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "B").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { semaphoreC.acquire(); System.out.print(Thread.currentThread().getName()); semaphoreA.release(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "C").start(); } }
执行结果以下:
ABCABCABCABCABCABCABCABCABCABC Process finished with exit code 0
本文一共介绍了四种三个线程交替打印的实现方法,其中第一种方法最简单易懂,可是更能考察多线程编程功底的应该是第二和第三种方法,在面试中也更加分。只要把这几种方法熟练掌握并完全理解,之后碰到此类题型就不用慌了。