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