Java多線程面試題之交替輸出問(wèn)題的實(shí)現(xiàn)
交替輸出問(wèn)題
一定要保證交替輸出,這就涉及到兩個(gè)線程的同步
問(wèn)題。
有人可能會(huì)想到,用睡眠時(shí)間差來(lái)實(shí)現(xiàn),但是只要是多線程里面,線程同步玩sleep()
函數(shù)的,99.99%都是錯(cuò)的。
這道題其實(shí)有100多種解法。
最簡(jiǎn)單的解法
是這個(gè)問(wèn)題的最優(yōu)解,但其實(shí)不是面試官想聽到的答案
關(guān)鍵函數(shù)
Locksupport.park()
:阻塞當(dāng)前線程Locksupport.unpark("")
:?jiǎn)拘涯硞€(gè)線程
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(); // 叫醒等待隊(duì)列里面的一個(gè)線程,對(duì)本程序來(lái)說(shuō)就是另一個(gè)線程 o.wait(); // 讓出鎖 } catch (InterruptedException e) { e.printStackTrace(); } } o.notify(); // 必須,否則無(wú)法停止程序 } }, "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(); } }
可能有人會(huì)想,代碼中的notify()
和wait()
順序是不是沒什么區(qū)別呢?那你就大錯(cuò)特錯(cuò)了,說(shuō)明你不明白notify()
和wait()
是怎么執(zhí)行的。
這道題其實(shí)是華為面試的填空題,讓你填notify()
和wait()
。
如果我們先執(zhí)行wait()
,會(huì)先讓自己直接進(jìn)入等待隊(duì)列,自己和另一個(gè)線程都在等待隊(duì)列中等待,兩個(gè)線程大??瞪小??,在那傻等,誰(shuí)也叫不醒對(duì)方,也就是根本執(zhí)行不了notify()
。
我們發(fā)現(xiàn),在程序的后面還有一個(gè)notify()
,而且還是必須有的,為什么是必須呢?我們將它注釋掉,輸出一下看看
其實(shí)這是一個(gè)小坑。
雖然程序可以正常輸出,但是程序沒有結(jié)束;我們可以根據(jù)動(dòng)圖發(fā)現(xiàn),最后一定是有一個(gè)線程是處在wait()
狀態(tài)的,沒有人叫醒它,它就會(huì)永遠(yuǎn)處在等待狀態(tài)中,從而程序無(wú)法結(jié)束,為了避免出現(xiàn)這種情況,我們要在后面加上一個(gè)notify()
。
但是還有一個(gè)大坑?。?!
玩過(guò)線程的應(yīng)該早就發(fā)現(xiàn)了這個(gè)問(wèn)題,如果第二個(gè)線程先搶到了,那么輸出的就是A1B2C3
了,怎么保證第一個(gè)永遠(yuǎn)先輸出的是數(shù)字?
我們可以使用CountDownLatch
這個(gè)類,它是JUC
新的同步工具,這個(gè)類可以想象成一個(gè)門栓,當(dāng)我們有線程執(zhí)行到門這里,它會(huì)等待門栓把門打開,線程才會(huì)執(zhí)行;如果t2
搶先一步,那么它會(huì)執(zhí)行await()
方法,因?yàn)橛虚T栓的存在,它只能在門外等待,所以t1
線程會(huì)直接執(zhí)行,執(zhí)行到countDown()
方法,使創(chuàng)建的CountDownLatch(1)
參數(shù)置為0
,即釋放門栓,所以永遠(yuǎn)都是t1
線程執(zhí)行完,t2
線程才會(huì)執(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,即只有一個(gè)門栓 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(); // 想哪個(gè)線程后執(zhí)行,await()就放在哪個(gè)線程里 } 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
包下,其中有一個(gè)專門替代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(); } }
代碼表面看起來(lái),創(chuàng)建鎖,調(diào)用方法跟synchronized
沒有區(qū)別,但是關(guān)鍵點(diǎn)在于Condition
這個(gè)類,大家應(yīng)該知道生產(chǎn)者
和消費(fèi)者
這個(gè)概念,生產(chǎn)者生產(chǎn)饅頭,生產(chǎn)滿了進(jìn)入等待隊(duì)列,消費(fèi)者吃饅頭,吃光了同樣進(jìn)入等待隊(duì)列,如果我們使用傳統(tǒng)的synchronized
,當(dāng)生產(chǎn)者生產(chǎn)滿時(shí),需要從等待隊(duì)列中叫醒消費(fèi)者,但調(diào)用notify
方法時(shí),我們能保證一定叫醒的是消費(fèi)者嗎?不能,這件事是無(wú)法做到的,那該怎么保證叫醒的一定是消費(fèi)者呢?
有兩種解決方案:
① 如果籃子已經(jīng)滿了,生產(chǎn)者會(huì)去等待隊(duì)列中叫醒一個(gè)線程
,但如果叫醒的線程還是一個(gè)生產(chǎn)者,那么新的生產(chǎn)者起來(lái)之后一定要先檢查一下籃子是否滿了,不能上來(lái)就生產(chǎn),如果是滿的,那接著去叫醒下一個(gè)線程,這樣依次重復(fù),我們一定會(huì)有一次叫醒的是消費(fèi)者。
② notifyAll()
方法:將等待隊(duì)列中的生產(chǎn)者和消費(fèi)者全喚醒,消費(fèi)者發(fā)現(xiàn)籃子是滿的,就去消費(fèi),生產(chǎn)者發(fā)現(xiàn)籃子是滿的,就繼續(xù)回到等待隊(duì)列。
但不管是這兩個(gè)哪種解決方案,我們喚醒的
線程
都是不精確的,全都存在著浪費(fèi)。這就是
synchronized
做同步的問(wèn)題。
Lock
本身就可以解決這個(gè)問(wèn)題,靠的就是Condition
,Condition
可以做到精確喚醒。
Condition
是條件的意思,但我們可以把它當(dāng)做隊(duì)列
來(lái)看待。
一個(gè)condition
就是一個(gè)等待隊(duì)列。
標(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(); // 隊(duì)列1 Condition conditionT2 = lock.newCondition(); // 隊(duì)列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(); } }
第一個(gè)線程
t1
先上來(lái)持有鎖,持有鎖之后叫醒第二隊(duì)列的內(nèi)容,然后自己進(jìn)入第一隊(duì)列等待,同理,t2
線程叫醒第一隊(duì)列的內(nèi)容,自己進(jìn)入第二隊(duì)列等待,這樣就可以做到精確喚醒
。
到此這篇關(guān)于Java多線程面試題之交替輸出問(wèn)題的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Java 交替輸出內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java使用線程池實(shí)現(xiàn)socket編程的方法詳解
這篇文章主要為大家詳細(xì)介紹了Java使用線程池實(shí)現(xiàn)socket編程的方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助2022-03-03詳解Java如何在業(yè)務(wù)代碼中優(yōu)雅的使用策略模式
這篇文章主要為大家介紹了Java如何在業(yè)務(wù)代碼中優(yōu)雅的使用策略模式,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的可以了解下2023-08-08java判斷各類型字符個(gè)數(shù)實(shí)例代碼
大家好,本篇文章主要講的是java判斷各類型字符個(gè)數(shù)實(shí)例代碼,感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話記得收藏一下,方便下次瀏覽2021-12-12Java基于Calendar類輸出指定年份和月份的日歷代碼實(shí)例
這篇文章主要介紹了Java 使用Calendar類輸出指定年份和月份的日歷,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02java字符串?dāng)?shù)組進(jìn)行大小排序的簡(jiǎn)單實(shí)現(xiàn)
下面小編就為大家?guī)?lái)一篇java字符串?dāng)?shù)組進(jìn)行大小排序的簡(jiǎn)單實(shí)現(xiàn)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-09-09Admin - SpringBoot + Maven 多啟動(dòng)環(huán)境配置實(shí)例詳解
這篇文章主要介紹了Admin - SpringBoot + Maven 多啟動(dòng)環(huán)境配置,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03Java中對(duì)象都是分配在堆上嗎?你錯(cuò)了!
這篇文章主要介紹了Java中對(duì)象都是分配在堆上嗎?你錯(cuò)了!文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,,需要的朋友可以參考下2019-06-06