写点什么

Java 并发基础(五):面试实战之多线程顺序打印

用户头像
看山
关注
发布于: 2021 年 04 月 11 日
Java 并发基础(五):面试实战之多线程顺序打印

你好,我是看山。


来个面试题,让大家练练手。这个题在阿里和小米都被问过,所以放在这个抛砖引玉,期望能够得到一个更佳的答案。


实现 3 个线程 A、B、C,A 线程持续打印“A”,B 线程持续打印“B”,C 线程持续打印“C”,启动顺序是线程 C、线程 B、线程 A,打印的结果是:ABC。

解法一:状态位变量控制

这个问题考察的是多线程协同顺序执行。也就是第一个线程最先达到执行条件,开始执行,执行完之后,第二个线程达到执行条件,开始执行,以此类推。可以想到的是,通过状态位来表示线程执行的条件,多个线程自旋等待状态位变化。


线上代码:


import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;
class ABCThread { private static final Lock lock = new ReentrantLock(); private static volatile int state = 0;
private static final Thread threadA = new Thread(() -> { while (true) { lock.lock(); try { if (state % 3 == 0) { System.out.println("A"); state++; break; } else { System.out.println("A thread & state = " + state); } } finally { lock.unlock(); } } });
private static final Thread threadB = new Thread(() -> { while (true) { lock.lock(); try { if (state % 3 == 1) { System.out.println("B"); state++; break; } else { System.out.println("B thread & state = " + state); } } finally { lock.unlock(); } } });
private static final Thread threadC = new Thread(() -> { while (true) { lock.lock(); try { if (state % 3 == 2) { System.out.println("C"); state++; break; } else { System.out.println("C thread & state = " + state); } } finally { lock.unlock(); } } });
public static void main(String[] args) { threadC.start(); threadB.start(); threadA.start(); }}
复制代码


可以看到,状态位state使用volatile修饰,是希望一个线程修改状态位值之后,其他线程可以读取到刚修改的数据,这个属于 Java 内存模型的范围,后续会有单独的章节描述。


这个可以解题,但是却有很多性能上的损耗。因为每个进程都在自旋检查状态值state是否符合条件,而且自旋过程中会有获取锁的过程,代码中在不符合条件时打印了一些内容,比如:System.out.println("A thread & state = " + state);,我们可以运行一下看看结果:


C thread & state = 0...67行C thread & state = 0B thread & state = 0...43行B thread & state = 0AC thread & state = 1...53行C thread & state = 1BC
复制代码


可以看到,在 A 线程获取到锁之前,C 线程和 B 线程自旋了 100 多次,然后 A 线程才获取机会获取锁和打印。然后在 B 线程获取锁之前,C 线程又自旋了 53 次。性能损耗可见一斑。

解法二:Condition 实现条件判断

既然无条件自旋浪费性能,那就加上条件自旋。


代码如下:


import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;
class ABCThread2 { private static final Lock lock = new ReentrantLock(); private static volatile int state = 0; private static final Condition conditionA = lock.newCondition(); private static final Condition conditionB = lock.newCondition(); private static final Condition conditionC = lock.newCondition();
private static final Thread threadA = new Thread(() -> { while (true) { lock.lock(); try { while(state % 3 != 0) { System.out.println("A await start"); conditionA.await(); System.out.println("A await end"); } System.out.println("A"); state++; conditionB.signal(); break; } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } });
private static final Thread threadB = new Thread(() -> { while (true) { lock.lock(); try { while(state % 3 != 1) { System.out.println("B await start"); conditionB.await(); System.out.println("B await end"); } System.out.println("B"); state++; conditionC.signal(); break; } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } });
private static final Thread threadC = new Thread(() -> { while (true) { lock.lock(); try { while(state % 3 != 2) { System.out.println("C await start"); conditionC.await(); System.out.println("C await end"); } System.out.println("C"); state++; break; } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } });
public static void main(String[] args) { threadC.start(); threadB.start(); threadA.start(); }}
复制代码


通过Lock锁的Condition实现有条件自旋,运行结果如下:


C await startB await startAB await endBC await endC
复制代码


可以从运行结果看到,C 线程发现自己不符合要求,就通过conditionC.await();释放锁,然后等待条件被唤醒后重新获得锁。然后是 B 线程,最后是 A 线程开始执行,发现符合条件,直接运行,然后唤醒 B 线程的锁条件,依次类推。这种方式其实和信号量很类似。

解法三:信号量

先上代码:


import java.util.concurrent.Semaphore;
class ABCThread3 { private static Semaphore semaphoreA = new Semaphore(1); private static Semaphore semaphoreB = new Semaphore(1); private static Semaphore semaphoreC = new Semaphore(1);
private static final Thread threadA = new Thread(() -> { try { semaphoreA.acquire(); System.out.println("A"); semaphoreB.release(); } catch (InterruptedException e) { e.printStackTrace(); } });
private static final Thread threadB = new Thread(() -> { try { semaphoreB.acquire(); System.out.println("B"); semaphoreC.release(); } catch (InterruptedException e) { e.printStackTrace(); } });
private static final Thread threadC = new Thread(() -> { try { semaphoreC.acquire(); System.out.println("C"); } catch (InterruptedException e) { e.printStackTrace(); } });
public static void main(String[] args) throws InterruptedException { semaphoreB.acquire(); semaphoreC.acquire(); threadC.start(); threadB.start(); threadA.start(); }}
复制代码


代码中执行前先执行了semaphoreB.acquire();semaphoreC.acquire();,是为了将 B 和 C 的信号释放,这个时候,就能够阻塞 B 线程、C 线程中信号量的获取,直到顺序获取了信号值。

文末总结

这个题是考察大家对线程执行顺序和线程之间协同的理解,文中所实现的三种方式,都能解题,只不过代码复杂度和性能有差异。因为其中涉及很多多线程的内容,后续会单独开文说明每个知识点。

推荐阅读




你好,我是看山,公众号:看山的小屋,10 年老猿,Apache Storm、WxJava、Cynomys 开源贡献者。游于码界,戏享人生。



发布于: 2021 年 04 月 11 日阅读数: 33
用户头像

看山

关注

公众号「看山的小屋」 2017.10.26 加入

游于码界,戏享人生。 未来不迎,当时不杂,既过不恋。

评论

发布
暂无评论
Java 并发基础(五):面试实战之多线程顺序打印