「面試」如果把線程當作一個人來對待,所有問題都瞬間明白了,你懂嗎

多線程的問題都曾經困擾過每個開發人員,今天將從全新視角來解說,希望讀者都能明白。

強烈建議去運行下文章中的示例代碼,自己體會下。

要是喜歡的點點關注,點點贊。

對Java技術,架構技術感興趣的朋友,歡迎加QQ羣728821520,一起學習,相互討論。

問題究竟出在哪裏?

一個線程執行,固然是安全的,但是有時太慢了,怎麼辦?

老祖宗告訴我們,“一方有難,八方支援”,那不就是多叫幾個線程來幫忙嘛,好辦呀,多new幾個不就行了,又不要錢。這樣能管用嗎?繼續往下看。

俗話說,“在家靠父母,出門靠朋友”。有了朋友的幫助,就會事半功倍。是這樣的嗎?

不一定,如果朋友“不靠譜”,結果竟是在“添亂”。於是就演變爲,“不怕神一樣的對手,就怕豬一樣的隊友”。可見“人多力量大”縱然是對的,但也要配合好才能成事。

人和人是朋友,那線程和線程也是“朋友”,如果多線程之間不能配合好的話,最終也會變爲“豬一樣的隊友”。事實證明,這也不是一件易事。且容我慢慢道來。

開發是一門技術,管理是一門藝術。也許你正想帶着兄弟們大幹一場,可偏偏就有人要辭職。或者你付出了這麼多,但別人從來沒有感動過。爲什麼會這樣呢?

因爲你面對的是人。每個人都是獨立的個體,有思想,有靈魂,有情感,有三觀。能夠接受外界的“輸入”,經過“處理”後,能夠產生“輸出”。

說白了就是會自主的分析問題,並做出決定。這叫什麼呢?答案就是,主觀能動性。

擁有主觀能動性的物體(比如人),你需要和它協商着或配合着來共同完成一件事情,而不能“強迫”它去做什麼,因爲這樣往往不會有好的結果。

費了這麼多口舌,就是希望把問題儘量的簡單化。終於可以回到程序了,那線程的情況是不是類似的呢?答案是肯定的。

一個線程準備好後,經過CPU的調度,就可以自主的運行了。此時它儼然成了一個獨立的個體,且具有主觀能動性。

這本是一件好事,但卻也有不好的一面,那就是你對它的“掌控”能力變弱了,頗有一種“將在外,君命有所不受”的感覺。

可能你不同意這種看法,說我可以“強迫”它停止運行,調用Thread類的stop()方法來直接把它“掐死”,不好意思,該方法已廢棄。

因爲線程可能在運行一些“關鍵”代碼(比如轉賬),此刻不能被終止。Thread類還有一些其它的方法也都廢棄了,大抵原因其實都差不多。

講了這麼多,相信你已經明白了,簡單總結一下:

事情起因:線程可以獨立自主的運行,可以認爲它具有主觀能動性。

造成結果:對它的掌控能力變弱了,而且又不能直接把它“幹掉”。

解決方案:凡事商量着來,互相配合着把事情完成。

作者觀點:其實就是把線程當作人來對待。

小試牛刀一下

一旦把線程當成人,就來到了人類的世界,這我們太熟悉了,所以很多問題都會變得非常簡單明瞭。一起來看看吧。

場景一,停止

“大胖,大胖,12點了,該去吃飯了,別寫了”

“好的,好的,稍等片刻,把這幾行代碼寫完就走”

要點:把停止的信號傳達給別人,別人處理完手頭的事情就自己主動停止了。

static void stopByFlag() {

ARunnable ar = new ARunnable();

new Thread(ar).start();

ar.tellToStop();

}

static class ARunnable implements Runnable {

volatile boolean stop;

void tellToStop() {

stop = true;

}

@Override

public void run() {

println(“進入不可停止區域 1。。。”);

doingLongTime(5);

println(“退出不可停止區域 1。。。”);

println(“檢測標誌stop = %s”, String.valueOf(stop));

if (stop) {

println(“停止執行”);

return;

}

println(“進入不可停止區域 2。。。”);

doingLongTime(5);

println(“退出不可停止區域 2。。。”);

}

}

解說:線程在預設的地點檢測flag,來決定是否停止。

場景二,暫停/恢復

“大胖,大胖,先別發請求了,對方服務器快掛了”

“好的,好的,等這個執行完就不發了”

過了一會

“大胖,大胖,可以重新發請求了”

“好的,好的”

要點:把暫停的信號傳達給別人,別人處理完手頭的事情就自己主動暫停了。但是恢復是無法自主進行的,只能由操作系統來恢復線程的執行。

static void pauseByFlag() {

BRunnable br = new BRunnable();

new Thread(br).start();

br.tellToPause();

sleep(8);

br.tellToResume();

}

static class BRunnable implements Runnable {

volatile boolean pause;

void tellToPause() {

pause = true;

}

void tellToResume() {

synchronized (this) {

this.notify();

}

}

@Override

public void run() {

println(“進入不可暫停區域 1。。。”);

doingLongTime(5);

println(“退出不可暫停區域 1。。。”);

println(“檢測標誌pause = %s”, String.valueOf(pause));

if (pause) {

println(“暫停執行”);

try {

synchronized (this) {

this.wait();

}

} catch (InterruptedException e) {

e.printStackTrace();

}

println(“恢復執行”);

}

println(“進入不可暫停區域 2。。。”);

doingLongTime(5);

println(“退出不可暫停區域 2。。。”);

}

}

解說:還是在預設的地點檢測flag。然後就是wait/notify配合使用。

場景三,插隊

“大胖,大胖,讓我站到你前面,不想排隊了”

“好吧”

要點:別人插隊到你前面,必須等他完事後才輪到你。

static void jqByJoin() {

CRunnable cr = new CRunnable();

Thread t = new Thread(cr);

t.start();

sleep(1);

try {

t.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

println(“終於輪到我了”);

}

static class CRunnable implements Runnable {

@Override

public void run() {

println(“進入不可暫停區域 1。。。”);

doingLongTime(5);

println(“退出不可暫停區域 1。。。”);

}

}

解說:join方法可以讓某個線程插到自己前面,等它執行完,自己纔會繼續執行。

場景四,叫醒

“大胖,大胖,醒醒,醒醒,看誰來了”

“誰啊,我去”

要點:要把別人從睡夢中叫醒,一定要採取稍微暴力一點的手段。

static void stopByInterrupt() {

DRunnable dr = new DRunnable();

Thread t = new Thread(dr);

t.start();

sleep(2);

t.interrupt();

}

static class DRunnable implements Runnable {

@Override

public void run() {

println(“進入暫停。。。”);

try {

sleep2(5);

} catch (InterruptedException e) {

println(“收到中斷異常。。。”);

println(“做一些相關處理。。。”);

}

println(“繼續執行或選擇退出。。。”);

}

}

解說:線程在sleep或wait時,是處於無法交互的狀態的,此時只能使用interrupt方法中斷它,線程會被激活並收到中斷異常。

常見的協作配合

上面那些場景,其實都是對一個線程的操作,下面來看多線程間的一些配合。

事件一,考試

假設今天考試,20個學生,1個監考老師。規定學生可以提前交卷,即把卷子留下,直接走人就行了。

但老師必須等到所有的學生都走後,纔可以收卷子,然後裝訂打包。

如果把學生和老師都看作線程,就是1個線程和20個線程的配合問題,即等20個線程都結束了,這1個線程纔開始。

比如20個線程分別在計算數據,等它們都結束後得到20箇中間結果,最後這1個線程再進行後續彙總、處理等。

static final int COUNT = 20;

static CountDownLatch cdl = new CountDownLatch(COUNT);

public static void main(String[] args) throws Exception {

new Thread(new Teacher(cdl)).start();

sleep(1);

for (int i = 0; i < COUNT; i++) {

new Thread(new Student(i, cdl)).start();

}

synchronized (ThreadCo1.class) {

ThreadCo1.class.wait();

}

}

static class Teacher implements Runnable {

CountDownLatch cdl;

Teacher(CountDownLatch cdl) {

this.cdl = cdl;

}

@Override

public void run() {

println(“老師髮捲子。。。”);

try {

cdl.await();

} catch (InterruptedException e) {

e.printStackTrace();

}

println(“老師收卷子。。。”);

}

}

static class Student implements Runnable {

CountDownLatch cdl;

int num;

Student(int num, CountDownLatch cdl) {

this.num = num;

this.cdl = cdl;

}

@Override

public void run() {

println(“學生(%d)寫卷子。。。”, num);

doingLongTime();

println(“學生(%d)交卷子。。。”, num);

cdl.countDown();

}

}

解說:每完成一個線程,計數器減1,當減到0時,被阻塞的線程自動執行。

事件二,旅遊

最近景色宜人,公司組織去登山,大夥都來到了山腳下,登山過程自由進行。

但爲了在特定的地點拍集體照,規定1個小時後在半山腰集合,誰最後到的,要給大家表演一個節目。

然後繼續登山,在2個小時後,在山頂集合拍照,還是誰最後到的表演節目。

接着開始下山了,在2個小時後在山腳下集合,點名回家,最後到的照例表演節目。

static final int COUNT = 5;

static CyclicBarrier cb = new CyclicBarrier(COUNT, new Singer());

public static void main(String[] args) throws Exception {

for (int i = 0; i < COUNT; i++) {

new Thread(new Staff(i, cb)).start();

}

synchronized (ThreadCo2.class) {

ThreadCo2.class.wait();

}

}

static class Singer implements Runnable {

@Override

public void run() {

println(“爲大家唱歌。。。”);

}

}

static class Staff implements Runnable {

CyclicBarrier cb;

int num;

Staff(int num, CyclicBarrier cb) {

this.num = num;

this.cb = cb;

}

@Override

public void run() {

println(“員工(%d)出發。。。”, num);

doingLongTime();

println(“員工(%d)到達地點一。。。”, num);

try {

cb.await();

} catch (Exception e) {

e.printStackTrace();

}

println(“員工(%d)再出發。。。”, num);

doingLongTime();

println(“員工(%d)到達地點二。。。”, num);

try {

cb.await();

} catch (Exception e) {

e.printStackTrace();

}

println(“員工(%d)再出發。。。”, num);

doingLongTime();

println(“員工(%d)到達地點三。。。”, num);

try {

cb.await();

} catch (Exception e) {

e.printStackTrace();

}

println(“員工(%d)結束。。。”, num);

}

}

解說:某個線程到達預設點時就在此等待,等所有的線程都到達時,大家再一起向下個預設點出發。如此循環反覆下去。

事件三,勞動

大胖和小白去了創業公司,公司爲了節約開支,沒有請專門的保潔人員。讓員工自己掃地和擦桌。

大胖覺得擦桌輕鬆,就讓小白去掃地。可小白覺得掃地太累,也想擦桌。

爲了公平起見,於是決定,每人先幹一半,然後交換工具,再接着幹對方剩下的那一個半。

static Exchanger ex = new Exchanger<>();

public static void main(String[] args) throws Exception {

new Thread(new Staff(“大胖”, new Tool(“笤帚”, “掃地”), ex)).start();

new Thread(new Staff(“小白”, new Tool(“抹布”, “擦桌”), ex)).start();

synchronized (ThreadCo3.class) {

ThreadCo3.class.wait();

}

}

static class Staff implements Runnable {

String name;

Tool tool;

Exchanger ex;

Staff(String name, Tool tool, Exchanger ex) {

this.name = name;

this.tool = tool;

this.ex = ex;

}

@Override

public void run() {

println("%s拿的工具是[%s],他開始[%s]。。。", name, tool.name, tool.work);

doingLongTime();

println("%s開始交換工具。。。", name);

try {

tool = ex.exchange(tool);

} catch (Exception e) {

e.printStackTrace();

}

println("%s的工具變爲[%s],他開始[%s]。。。", name, tool.name, tool.work);

}

}

static class Tool {

String name;

String work;

Tool(String name, String work) {

this.name = name;

this.work = work;

}

}

解說:兩個線程在預設點交換變量,先到達的等待對方。

事件四,魔性遊戲

這是一個充滿魔性的小遊戲,由一個團隊一起參加。所有人每隔5秒鐘抽一次籤,每個人有50%的概率留下來或被淘汰。

留下來的人下次抽籤時同樣有50%的概率被淘汰。被淘汰的人下次抽籤時同樣有50%的概率復活。

團隊所有成員都被淘汰完,爲挑戰失敗,團隊所有成員都回到遊戲中(除剛開始外),爲挑戰成功。

比如一開始10人蔘與遊戲,第一輪抽籤後,6人留下,4人淘汰。

第二輪抽籤後,留下的6人中4人被淘汰,淘汰的4人中2人復活,那麼目前是4人在遊戲中,6人被淘汰。

一直如此繼續下去,直到10人全部被淘汰,或全部回到遊戲中。

可見,人數越多,全部被淘汰的概率越小,但全部回到遊戲中的概率也越小。

反之,人數越少,全部回到遊戲中的概率越大,但全部被淘汰的概率也越大。

是不是很有魔性啊。哈哈。

static final int COUNT = 6;

static Phaser ph = new Phaser() {

protected boolean onAdvance(int phase, int registeredParties) {

println2(“第(%d)局,剩餘[%d]人”, phase, registeredParties);

return registeredParties == 0 ||

(phase != 0 && registeredParties == COUNT);

};

};

public static void main(String[] args) throws Exception {

new Thread(new Challenger(“張三”)).start();

new Thread(new Challenger(“李四”)).start();

new Thread(new Challenger(“王五”)).start();

new Thread(new Challenger(“趙六”)).start();

new Thread(new Challenger(“大胖”)).start();

new Thread(new Challenger(“小白”)).start();

synchronized (ThreadCo4.class) {

ThreadCo4.class.wait();

}

}

static class Challenger implements Runnable {

String name;

int state;

Challenger(String name) {

this.name = name;

this.state = 0;

}

@Override

public void run() {

println("[%s]開始挑戰。。。", name);

ph.register();

int phase = 0;

int h;

while (!ph.isTerminated() && phase < 100) {

doingLongTime(5);

if (state == 0) {

if (Decide.goon()) {

h = ph.arriveAndAwaitAdvance();

if (h < 0)

println(“No%d.[%s]繼續,但已勝利。。。”, phase, name);

else

println(“No%d.[%s]繼續at(%d)。。。”, phase, name, h);

} else {

state = -1;

h = ph.arriveAndDeregister();

println(“No%d.[%s]退出at(%d)。。。”, phase, name, h);

}

} else {

if (Decide.revive()) {

state = 0;

h = ph.register();

if (h < 0)

println(“No%d.[%s]復活,但已失敗。。。”, phase, name);

else

println(“No%d.[%s]復活at(%d)。。。”, phase, name, h);

} else {

println(“No%d.[%s]沒有復活。。。”, phase, name);

}

}

phase++;

}

if (state == 0) {

ph.arriveAndDeregister();

}

println("[%s]結束。。。", name);

}

}

static class Decide {

static boolean goon() {

return random(9) > 4;

}

static boolean revive() {

return random(9) < 5;

}

}

解說:某個線程到達預設點後,可以選擇等待同伴或自己退出,等大家都到達後,再一起向下一個預設點出發,隨時都可以有新的線程加入,退出的也可以再次加入。

生產與銷售的問題

在現實中,工廠生產出來的產品會先放到倉庫存儲,銷售人員簽了單子後,會從倉庫把產品發給客戶。

如果生產的過快,倉庫裏產品越堆越多,直到把倉庫堆滿,那就必須停止生產,因爲沒地方放了。

此時只能讓銷售人員趕緊出去簽單子,把產品發出去,倉庫就有了空間,可以恢復生產了。

如果銷售的過快,倉庫裏產品越來越少,直到把倉庫清空,那就必須停止銷售,因爲沒產品了。

此時只能讓生產人員趕緊生產產品,把產品放到倉庫裏,倉庫裏就有了產品,可以恢復銷售了。

可能會有人問,爲什麼不讓生產和銷售直接掛鉤呢,把倉庫這個環節去掉?

這樣會造成兩種不好的情況:

一是突然來了很多單子,生產人員累成死Dog也生產不出來。

二是很長時間沒有單子,生產人員閒成廢Dog也無事可做。

用稍微“專業”點的術語就是此時的生產和銷售是一種強耦合的關係,銷售的波動對生產影響太大。

倉庫就是一個緩衝區,能有效的吸收波動,很大程度上減少波動的傳遞,起到一種解耦作用,由強耦合變成一種鬆散耦合。

這其實就對應計算機裏經典的生產者和消費者問題。

經典的生產者和消費者

一到多個線程充當生產者,生產元素。一到多個線程充當消費者,消費元素。

在兩者之間插入一個隊列(Queue)充當緩衝區,建立起生產者和消費者的鬆散耦合。

正常情況下,即生產元素的速度和消費元素的速度差不多時,生產者和消費者其實是不需要去關注對方的。

生產者可以一直生產,因爲隊列裏總是有空間。消費者可以一直消費,因爲隊列裏總是有元素。即達到一個動態的平衡。

但在特殊情況下,比如生產元素的速度很快,隊列裏沒有了空間,此時生產者必須自我“ba工”,開始“睡大覺”。

一旦消費者消費了元素之後,隊列裏纔會有空間,生產者纔可以重啓生產,所以,消費者在消費完元素後有義務去叫醒生產者復工。

更準確的說法應該是,只有在生產者“睡大覺”時,消費者消費完元素後才需要去叫醒生產者。否則,其實可以不用叫醒,因爲人家本來就沒睡。

反之,如果消費元素的速度很快,隊列裏沒有了元素,只需把上述情況顛倒過來即可。

但這樣的話就會引入一個新的問題,就是要能夠準備的判斷出對方有沒有在睡大覺,爲此就必須定義一個狀態變量,在自己即將開始睡大覺時,自己設置下這個變量。

對方通過檢測這個變量,來決定是否進行叫醒操作。當自己被叫醒後,首先要做的就是清除一下這個變量,表明我已經醒來複工了。

這樣就需要多維護一個變量和多了一部分判斷邏輯。可能有些人會覺得可以通過判斷隊列的“空”或“滿”(即隊列中的元素數目)來決定是否進行叫醒操作。

在高併發下,可能剛剛判斷隊列不爲空,瞬間之後隊列可能已經變爲空的了,這樣會導致邏輯出錯。線程可能永遠無法被叫醒。

因此,綜合所有,生產者每生產一個元素後,都會通知消費者,“現在有元素的,你可以消費”。

同樣,消費者每消費一個元素後,也會通知生產者,“現在有空間的,你可以生產”。

很明顯,這些通知很多時候(即對方沒有睡大覺時)是沒有真正意義的,不過無所謂,只要忽略它們就行了。

就是“寧可錯殺一千,也不放過一個”。首先要保證是正確的,然後纔有資格去BB別的。

public static void main(String[] args) {

Queue queue = new Queue();

new Thread(new Producer(queue)).start();

new Thread(new Producer(queue)).start();

new Thread(new Consumer(queue)).start();

}

static class Producer implements Runnable {

Queue queue;

Producer(Queue queue) {

this.queue = queue;

}

@Override

public void run() {

try {

for (int i = 0; i < 10000; i++) {

doingLongTime();

queue.putEle(random(10000));

}

} catch (Exception e) {

e.printStackTrace();

}

}

}

static class Consumer implements Runnable {

Queue queue;

Consumer(Queue queue) {

this.queue = queue;

}

@Override

public void run() {

try {

for (int i = 0; i < 10000; i++) {

doingLongTime();

queue.takeEle();

}

} catch (Exception e) {

e.printStackTrace();

}

}

}

static class Queue {

Lock lock = new ReentrantLock();

Condition prodCond = lock.newCondition();

Condition consCond = lock.newCondition();

final int CAPACITY = 10;

Object[] container = new Object[CAPACITY];

int count = 0;

int putIndex = 0;

int takeIndex = 0;

public void putEle(Object ele) throws InterruptedException {

try {

lock.lock();

while (count == CAPACITY) {

println(“隊列已滿:%d,生產者開始睡大覺。。。”, count);

prodCond.await();

}

container[putIndex] = ele;

println(“生產元素:%d”, ele);

putIndex++;

if (putIndex >= CAPACITY) {

putIndex = 0;

}

count++;

println(“通知消費者去消費。。。”);

consCond.signalAll();

} finally {

lock.unlock();

}

}

public Object takeEle() throws InterruptedException {

try {

lock.lock();

while (count == 0) {

println(“隊列已空:%d,消費者開始睡大覺。。。”, count);

consCond.await();

}

Object ele = container[takeIndex];

println(“消費元素:%d”, ele);

takeIndex++;

if (takeIndex >= CAPACITY) {

takeIndex = 0;

}

count–;

println(“通知生產者去生產。。。”);

prodCond.signalAll();

return ele;

} finally {

lock.unlock();

}

}

}

解說:其實就是對await/signalAll的應用,幾乎面試必問。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章