Photo By Instagram
第 19 题
朋友们许久不见,你们还好吗?
这段时间里,我也悄咪咪的去试了试外面的机会,2 年没有参加面试发现各大厂的面试风格已经悄悄的发生了变化。
前俩年都是喜欢上来一套 JUC 三连炮问到你懵圈为止,要不就是一套 Mysql 事务三连炮问到你瑟瑟发抖。而现在呢,面试官们喜欢揪着你的项目刨根问底。你可能会说,唉我的项目就是一堆 CRUD,没啥可说的。
不用担心,面试官会就你的业务场景,改造一下,现场创造一些难点来让你提出解决方案。你以为想到解决方案就完了吗?too naive!这时候你才刚刚走进面试官的陷阱,接着面试官会开始对你进行惨无人道(都是搬砖的,相煎何太急)的狂轰乱炸。
此时没有准备充分的你,就像一只站在大灰狼面前的小肥羊,孤独无助,任由大灰狼的皮鞭肆虐。
所以朋友们面试之前一定要多多准备,找几家不想去的公司试试水,同时也要多刷刷数据结构,线程方面的算法题。
这不,我在阿里第一轮笔试的结束的时候就碰到了这么一道题:
使用 3 个线程 A,B,C 来按顺序依次打印 1 - 100 的数字。
效果为:A => 1,B => 2,C => 3,A => 4,B => 5,C => 6,A => 7,…..
相信 synchronized 使用熟练的同学可以很快的输出如下一个解决方案:
public class ThreadPrint {
private static int number = 1 ;
private static final Object LOCK = new Object();
public static void main (String[] args) {
Thread a = new Thread( new Runnable(){
@Override
public void run () {
while ( true ) {
synchronized (LOCK) {
if (number > 100 ) {
System.out.println( "A over" );
LOCK.notifyAll();
break ;
}
if (number % 3 == 1 ) {
System.out.println(Thread.currentThread().getName() + " => " + number);
number++;
} else {
try {
LOCK.notifyAll();
LOCK.wait();
} catch (Exception ex) {
System.out.println(ex);
}
}
}
}
}
}, "Thread-A" );
a.start();
Thread b = new Thread( new Runnable(){
@Override
public void run () {
while ( true ) {
synchronized (LOCK) {
if (number > 100 ) {
System.out.println( "B over" );
LOCK.notifyAll();
break ;
}
if (number % 3 == 2 ) {
System.out.println(Thread.currentThread().getName() + " => " + number);
number++;
} else {
try {
LOCK.notifyAll();
LOCK.wait();
} catch (Exception ex) {
System.out.println(ex);
}
}
}
}
}
}, "Thread-B" );
b.start();
Thread c = new Thread( new Runnable(){
@Override
public void run () {
while ( true ) {
synchronized (LOCK) {
if (number > 100 ) {
System.out.println( "C over" );
LOCK.notifyAll();
break ;
}
if (number % 3 == 0 ) {
System.out.println(Thread.currentThread().getName() + " => " + number);
number++;
} else {
try {
LOCK.notifyAll();
LOCK.wait();
} catch (Exception ex) {
System.out.println(ex);
}
}
}
}
}
}, "Thread-C" );
c.start();
}
}
当你乐呵呵的告诉面试官写完了的时候,他会微微点头告诉你,嗯,首先这是一个可以 work 的方案,但是呢代码稍微有些冗余,可以优化一下吗。
这三个线程的内部运转逻辑基本一致,唯一区别就是取模运算时候的值不同,因此我们可以将代码改进一下:
public class ThreadPrint {
private static final Object LOCK = new Object();
public static void main (String[] args) {
new PrintThread( 1 , "Thread-A" , LOCK).start();
new PrintThread( 2 , "Thread-B" , LOCK).start();
new PrintThread( 0 , "Thread-C" , LOCK).start();
}
}
class PrintThread extends Thread {
private int mod;
private String name;
private static int number = 1 ;
private Object LOCK;
public PrintThread ( int mod, String name, Object lock) {
this .mod = mod;
this .name = name;
this .LOCK = lock;
}
@Override
public void run () {
while ( true ) {
synchronized (LOCK) {
if (number > 100 ) {
System.out.println(name + "over" );
LOCK.notifyAll();
break ;
}
if (number % 3 == mod) {
System.out.println(name + " => " + number);
number++;
} else {
try {
LOCK.notifyAll();
LOCK.wait();
} catch (Exception ex) {
System.out.println(ex);
}
}
}
}
}
}
当你坑此坑次的精简了代码后,面试官笑嘻嘻的说,嗯,现在精简了不少。突然他脸上漏出了狡黠的笑容,还有其他解法吗。
嗯,我想想,你陷入了沉思。突然灵光一闪,我可以用信号量来替换管程,接着你写出了如下的代码:
import java.util.concurrent.Semaphore;
public class ThreadPrint {
private static final Object LOCK = new Object();
private static final Semaphore semaphore = new Semaphore( 1 , false );
public static void main (String[] args) {
new PrintThread( 1 , "Thread-A" , semaphore).start();
new PrintThread( 2 , "Thread-B" , semaphore).start();
new PrintThread( 0 , "Thread-C" , semaphore).start();
}
}
class PrintThread extends Thread {
private int mod;
private String name;
private static int number = 1 ;
private Semaphore semaphore;
public PrintThread ( int mod, String name, Semaphore semaphore) {
this .mod = mod;
this .name = name;
this .semaphore = semaphore;
}
@Override
public void run () {
while ( true ) {
try {
semaphore.acquire();
if (number > 100 ) {
System.out.println(name + "over" );
semaphore.release();
break ;
}
if (number % 3 == mod) {
System.out.println(name + " => " + number);
number++;
}
semaphore.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
没错,使用管程可以解决的线程间互斥问题,采用信号量一样可以解决。当你认为终于可以告一段落的时候,面试官仍然不会放过你,还有其他方法吗?
你假装思索片刻,奥奥,还有一种办法可以使用 ReentrantLock 来解决:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadPrint {
private static final Lock lock = new ReentrantLock( false );
public static void main (String[] args) {
new PrintThread( 1 , "Thread-A" , lock).start();
new PrintThread( 2 , "Thread-B" , lock).start();
new PrintThread( 0 , "Thread-C" , lock).start();
}
}
class PrintThread extends Thread {
private int mod;
private String name;
private static int number = 1 ;
private Lock lock;
public PrintThread ( int mod, String name, Lock lock) {
this .mod = mod;
this .name = name;
this .lock = lock;
}
@Override
public void run () {
while ( true ) {
try {
lock.lock();
if (number > 100 ) {
System.out.println(name + "over" );
break ;
}
if (number % 3 == mod) {
System.out.println(name + " => " + number);
number++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
你飞速的敲击键盘,将原本采用 Semaphore 的方案替换为了 ReentrantLock,心里想,这下总该满意了吧。
然而并不像你想象的那么简单,面试官再次漏出狡黠的笑容,你现在的方案呢存在一个问题,就是三个线程会同时争抢资源,这样会导致某个线程抢占到了锁资源,但是又没到它打印的数字,有没有办法优化一下这个问题呢?
你陷入了沉思,不同时竞争锁,打印又是有序的,那就由 A 线程打印完数字后,A 去唤醒 B,然后 A进入睡眠;接着 B 打印完数字唤醒 C,然后 B 进入睡眠;最后 C 打印数字,然后唤醒 A,接着 C 进入睡眠,以此类推即可完成循环打印,并且同时只有一个线程在运行。
那使用什么方式来实现呢,聪明的你一定想到了 Condition:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadPrint {
private static final Lock lock = new ReentrantLock( false );
private static Condition cOver = lock.newCondition();
private static Condition aOver = lock.newCondition();
private static Condition bOver = lock.newCondition();
public static void main (String[] args) {
new PrintThread( 1 , "Thread-A" , lock, cOver, bOver).start();
new PrintThread( 2 , "Thread-B" , lock, aOver, cOver).start();
new PrintThread( 0 , "Thread-C" , lock, bOver, aOver).start();
}
}
class PrintThread extends Thread {
private int mod;
private String name;
private static int number = 1 ;
private Lock lock;
private Condition wait;
private Condition notify;
public PrintThread ( int mod, String name, Lock lock, Condition wait, Condition notify) {
this .mod = mod;
this .name = name;
this .lock = lock;
this .notify = notify;
this .wait = wait;
}
@Override
public void run () {
while ( true ) {
try {
lock.lock();
if (number > 100 ) {
System.out.println(name + "over" );
notify.signal();;
break ;
}
if (number % 3 == mod) {
System.out.println(name + " => " + number);
number++;
}
notify.signal();
wait.await();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
相信当你作出如上的解决方案后,面试官应该会漏出慈母般的微笑,并且伴随着频频点头,心里已经对你扎实的基本功竖起来大拇指。
如上即为我们今天要介绍的这道并发编程面试题,里面总共采用了四种方法去解决这个问题,层层递进不断优化。
如果你对上面的几种解法还不能烂熟于心,那你要抓紧巩固哦,不然容易给面试官留下一个理论王者的面评就不好啦。
金三银四啦, 每天一道题目,让 offer 来得简单点。
感谢你的阅读,我为你准备了一份《高级 Java 面试指南》,点击在看,关注公众号,回复 " 礼物 " 获取。
往期精彩
1 分钟看穿零拷贝技术,看不懂你打我
为什么 Java 程序员必须要懂类加载机制?
高级 Java 面试必问的三大 IO 模型,你 get 了吗?
细嚼慢咽 Java 线程池,你品你细品
分享一道美团一面的面试题,简单又细腻
final 这道送分题,你答对了吗?
面试官问我 volatile 是否存在伪共享问题?我懵逼了
Java 垃圾回收器很难?是你学的方法不对
当我们在谈论内存时,我们在谈论什么
聊聊 Java 的几把 JVM 级锁
你的每个在看,我都认真当成了喜欢