Java多線程面試題之交替輸出問題的實現(xiàn)
交替輸出問題

一定要保證交替輸出,這就涉及到兩個線程的同步問題。
有人可能會想到,用睡眠時間差來實現(xiàn),但是只要是多線程里面,線程同步玩sleep()函數(shù)的,99.99%都是錯的。
這道題其實有100多種解法。
最簡單的解法
是這個問題的最優(yōu)解,但其實不是面試官想聽到的答案
關(guān)鍵函數(shù)
Locksupport.park():阻塞當(dāng)前線程Locksupport.unpark(""):喚醒某個線程
LockSupport
package com.mashibing.juc.c_026_00_interview.A1B2C3
import java.util.concurrent.locks.LockSupport;
public class T02_00_LockSupport {
static Thread t1 = null, t2 = null;
public static void main(String[] args) throws Exception {
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
t1 = new Thread(() -> {
for (char c : aI) {
System.out.print(c);
LockSupport.unpark(t2); // 叫醒t2
LockSupport.park(); // t1阻塞 當(dāng)前線程阻塞
}
}, "t1");
t2 = new Thread(() -> {
for (char c : aC) {
LockSupport.park(); // t2掛起
System.out.print(c);
LockSupport.unpark(t1); // 叫醒t1
}
}, "t2");
t1.start();
t2.start();
}
}

執(zhí)行程序:

是我們想要的結(jié)果。
面試官想聽到的解法
synchronized wait notify
package com.mashibing.juc.c_026_00_interview.A1B2C3
public class T06_00_sync_wait_notify {
public static void main(String[] args) {
final Object o = new Object();
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
new Thread(() -> {
// 首先創(chuàng)建一把鎖
synchronized (o) {
for (char c : aI) {
System.out.print(c);
try {
o.notify(); // 叫醒等待隊列里面的一個線程,對本程序來說就是另一個線程
o.wait(); // 讓出鎖
} catch (InterruptedException e) {
e.printStackTrace();
}
}
o.notify(); // 必須,否則無法停止程序
}
}, "t1").start();
new Thread(() -> {
synchronized (o) {
for (char c : aC) {
System.out.print(c);
try {
o.notify();
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
o.notify();
}
}, "t2").start();
}
}

可能有人會想,代碼中的notify()和wait()順序是不是沒什么區(qū)別呢?那你就大錯特錯了,說明你不明白notify()和wait()是怎么執(zhí)行的。
這道題其實是華為面試的填空題,讓你填notify()和wait()。

如果我們先執(zhí)行wait(),會先讓自己直接進(jìn)入等待隊列,自己和另一個線程都在等待隊列中等待,兩個線程大??瞪小??,在那傻等,誰也叫不醒對方,也就是根本執(zhí)行不了notify()。
我們發(fā)現(xiàn),在程序的后面還有一個notify(),而且還是必須有的,為什么是必須呢?我們將它注釋掉,輸出一下看看

其實這是一個小坑。
雖然程序可以正常輸出,但是程序沒有結(jié)束;我們可以根據(jù)動圖發(fā)現(xiàn),最后一定是有一個線程是處在wait()狀態(tài)的,沒有人叫醒它,它就會永遠(yuǎn)處在等待狀態(tài)中,從而程序無法結(jié)束,為了避免出現(xiàn)這種情況,我們要在后面加上一個notify()。
但是還有一個大坑?。。?/strong>
玩過線程的應(yīng)該早就發(fā)現(xiàn)了這個問題,如果第二個線程先搶到了,那么輸出的就是A1B2C3了,怎么保證第一個永遠(yuǎn)先輸出的是數(shù)字?
我們可以使用CountDownLatch這個類,它是JUC新的同步工具,這個類可以想象成一個門栓,當(dāng)我們有線程執(zhí)行到門這里,它會等待門栓把門打開,線程才會執(zhí)行;如果t2搶先一步,那么它會執(zhí)行await()方法,因為有門栓的存在,它只能在門外等待,所以t1線程會直接執(zhí)行,執(zhí)行到countDown()方法,使創(chuàng)建的CountDownLatch(1)參數(shù)置為0,即釋放門栓,所以永遠(yuǎn)都是t1線程執(zhí)行完,t2線程才會執(zhí)行。
完整代碼
package com.mashibing.juc.c_026_00_interview.A1B2C3
import java.util.concurrent.CountDownLatch;
public class T07_00_sync_wait_notify {
private static CountDownLatch latch = new CountDownLatch(1); // 設(shè)置門栓的參數(shù)為1,即只有一個門栓
public static void main(String[] args) {
final Object o = new Object();
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
new Thread(() -> {
synchronized (o) {
for (char c : aI) {
System.out.print(c);
latch.countDown(); // 門栓的數(shù)值-1,即打開門
try {
o.notify();
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
o.notify();
}
}, "t1").start();
new Thread(() -> {
try {
latch.await(); // 想哪個線程后執(zhí)行,await()就放在哪個線程里
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o) {
for (char c : aC) {
System.out.print(c);
try {
o.notify();
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
o.notify();
}
}, "t2").start();
}
}
這樣就解決了我們的擔(dān)憂。
更靈活,更精細(xì)的解法
JDK提供了很多新的同步工具,在JUC包下,其中有一個專門替代synchronized的鎖:Lock。
Lock ReentrantLock await signal
package com.mashibing.juc.c_026_00_interview.A1B2C3
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class T08_00_lock_condition {
public static void main(String[] args) {
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try {
for (char c : aI) {
System.out.print(c);
condition.signal(); // notify()
condition.await(); // wait()
}
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1").start();
new Thread(() -> {
lock.lock(); // synchronized
try {
for (char c : aC) {
System.out.print(c);
condition.signal(); // o.notify
condition.await(); // o.wait
}
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t2").start();
}
}
代碼表面看起來,創(chuàng)建鎖,調(diào)用方法跟synchronized沒有區(qū)別,但是關(guān)鍵點在于Condition這個類,大家應(yīng)該知道生產(chǎn)者和消費者這個概念,生產(chǎn)者生產(chǎn)饅頭,生產(chǎn)滿了進(jìn)入等待隊列,消費者吃饅頭,吃光了同樣進(jìn)入等待隊列,如果我們使用傳統(tǒng)的synchronized,當(dāng)生產(chǎn)者生產(chǎn)滿時,需要從等待隊列中叫醒消費者,但調(diào)用notify方法時,我們能保證一定叫醒的是消費者嗎?不能,這件事是無法做到的,那該怎么保證叫醒的一定是消費者呢?
有兩種解決方案:
① 如果籃子已經(jīng)滿了,生產(chǎn)者會去等待隊列中叫醒一個線程,但如果叫醒的線程還是一個生產(chǎn)者,那么新的生產(chǎn)者起來之后一定要先檢查一下籃子是否滿了,不能上來就生產(chǎn),如果是滿的,那接著去叫醒下一個線程,這樣依次重復(fù),我們一定會有一次叫醒的是消費者。
② notifyAll()方法:將等待隊列中的生產(chǎn)者和消費者全喚醒,消費者發(fā)現(xiàn)籃子是滿的,就去消費,生產(chǎn)者發(fā)現(xiàn)籃子是滿的,就繼續(xù)回到等待隊列。
但不管是這兩個哪種解決方案,我們喚醒的
線程都是不精確的,全都存在著浪費。這就是
synchronized做同步的問題。

Lock本身就可以解決這個問題,靠的就是Condition,Condition可以做到精確喚醒。
Condition是條件的意思,但我們可以把它當(dāng)做隊列來看待。
一個condition就是一個等待隊列。
標(biāo)準(zhǔn)代碼
package com.mashibing.juc.c_026_00_interview.A1B2C3
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class T08_00_lock_condition {
public static void main(String[] args) {
char[] aI = "1234567".toCharArray();
char[] aC = "ABCDEFG".toCharArray();
Lock lock = new ReentrantLock();
Condition conditionT1 = lock.newCondition(); // 隊列1
Condition conditionT2 = lock.newCondition(); // 隊列2
CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
lock.lock(); // synchronized
try {
for (char c : aI) {
System.out.print(c);
latch.countDown();
conditionT2.signal(); // o.notify()
conditionT1.await(); // o.wait()
}
conditionT2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1").start();
new Thread(() -> {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock(); // synchronized
try {
for (char c : aC) {
System.out.print(c);
conditionT1.signal(); // o.notify
conditionT2.await(); // o.wait
}
conditionT1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t2").start();
}
}

第一個線程
t1先上來持有鎖,持有鎖之后叫醒第二隊列的內(nèi)容,然后自己進(jìn)入第一隊列等待,同理,t2線程叫醒第一隊列的內(nèi)容,自己進(jìn)入第二隊列等待,這樣就可以做到精確喚醒。
到此這篇關(guān)于Java多線程面試題之交替輸出問題的實現(xiàn)的文章就介紹到這了,更多相關(guān)Java 交替輸出內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java ArrayList.remove()的三種錯誤用法以及六種正確用法詳解
這篇文章主要介紹了java ArrayList.remove()的三種錯誤用法以及六種正確用法詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01
Java函數(shù)式開發(fā) Optional空指針處理
本文主要介紹Java函數(shù)式開發(fā) Optional空指針處理,這里整理了相關(guān)資料,及示例代碼,有興趣的小伙伴可以參考下2016-09-09
IDEA工程運行時總是報xx程序包不存在實際上包已導(dǎo)入(問題分析及解決方案)
這篇文章主要介紹了IDEA工程運行時,總是報xx程序包不存在,實際上包已導(dǎo)入,本文給大家分享問題分析及解決方案,通過實例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2020-08-08
Java 高并發(fā)二:多線程基礎(chǔ)詳細(xì)介紹
本文主要介紹Java 高并發(fā)多線程的知識,這里整理詳細(xì)的資料來解釋線程的知識,有需要的學(xué)習(xí)高并發(fā)的朋友可以參考下2016-09-09

