朋友們許久不見,你們還好嗎?
這段時間裏,我也悄咪咪的去試了試外面的機會,2 年沒有參加面試發現各大廠的面試風格已經悄悄的發生了變化。
前倆年都是喜歡上來一套 JUC 三連炮問到你懵圈爲止,要不就是一套 Mysql 事務三連炮問到你瑟瑟發抖。而現在呢,面試官們喜歡揪着你的項目刨根問底。你可能會說,唉我的項目就是一堆 CRUD,沒啥可說的。
不用擔心,面試官會就你的業務場景,改造一下,現場創造一些難點來讓你提出解決方案。你以爲想到解決方案就完了嗎?too naive!這時候你纔剛剛走進面試官的陷阱,接着面試官會開始對你進行慘無人道(都是搬磚的,相煎何太急)的狂轟亂炸。
此時沒有準備充分的你,就像一隻站在大灰狼面前的小肥羊,孤獨無助,任由大灰狼的皮鞭肆虐。
所以朋友們面試之前一定要多多準備,找幾家不想去的公司試試水,同時也要多刷刷數據結構,線程方面的算法題。
這不,我在阿里第一輪筆試的結束的時候就碰到了這麼一道題:
使用 3 個線程 A,B,C 來按順序依次打印 0 - 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 面試指南》,點擊在看,關注公衆號,回覆 "禮物" 獲取。