Java線程通信及線程虛假喚醒知識(shí)總結(jié)
線程通信
線程在內(nèi)部運(yùn)行時(shí),線程調(diào)度具有一定的透明性,程序通常無(wú)法控制線程的輪換執(zhí)行。但Java本身提供了一些機(jī)制來(lái)保證線程協(xié)調(diào)運(yùn)行。
假設(shè)目前系統(tǒng)中有兩個(gè)線程,分別代表存款和取錢。當(dāng)錢存進(jìn)去,立馬就取出來(lái)挪入指定賬戶。這涉及到線程間的協(xié)作,使用到Object類提供的wait()、notify()、notifyAll()三個(gè)方法,其不屬于Thread類,而屬于Object,而這三個(gè)方法必須由監(jiān)視器對(duì)象來(lái)調(diào)用:
- synchronized修飾的方法,因?yàn)樵擃惖哪J(rèn)實(shí)例(this)就是同步監(jiān)視器,因此可以在同步方法中直接調(diào)用
- synchronized修飾的同步代碼塊,同步監(jiān)視器是synchronized括號(hào)里的對(duì)象,因此必須使用該對(duì)象來(lái)調(diào)用
三個(gè)方法解釋如下:
- wait():當(dāng)前線程等待,釋放當(dāng)前對(duì)象鎖,讓出CPU,直到其他線程使用notify或者notifyAll喚醒該線程
- notify():?jiǎn)拘言诖送奖O(jiān)視器上等待的單個(gè)線程,若存在多個(gè)線程,則隨機(jī)喚醒一個(gè)。執(zhí)行了notify不會(huì)馬上釋放鎖,只有完全退出synchronized代碼塊或者中途遇到wait,呈wait狀態(tài)的線程才可以去爭(zhēng)取該對(duì)象鎖
- notifyAll():?jiǎn)拘言诖送奖O(jiān)視器上的所有線程,同上。
現(xiàn)在用兩個(gè)同步方法分別代表存錢取錢
- 當(dāng)余額為0時(shí),進(jìn)入存錢流程,執(zhí)行存錢操作后,喚醒取錢線程
- 當(dāng)余額為0時(shí),進(jìn)入取錢流程,發(fā)現(xiàn)num==0,進(jìn)入阻塞狀態(tài),等待被喚醒
/** * 存一塊錢 * * @throws InterruptedException */ public synchronized void increase() throws InterruptedException { // 當(dāng)余額為1,說(shuō)明已經(jīng)存過錢,等待取錢。存錢方法阻塞 if (num == 1) { this.wait(); } // 執(zhí)行存錢操作 num++; System.out.println(Thread.currentThread().getName() + ":num=" + num); // 喚醒其他線程 this.notifyAll(); } /** * 取一塊錢 * * @throws InterruptedException */ public synchronized void decrease() throws InterruptedException { // 當(dāng)余額為0,說(shuō)明已經(jīng)取過錢,等待存錢。取錢方法阻塞 if (num == 0) { this.wait(); } num--; System.out.println(Thread.currentThread().getName() + ":num=" + num); this.notifyAll(); }
調(diào)用方法:
private int num = 0; public static void main(String[] args) { Test test = new Test(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { test.increase(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "存錢").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { test.decrease(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "取錢").start(); }
結(jié)果沒有什么問題
線程虛假喚醒
上述線程通信看起來(lái)似乎沒有什么問題,但若此時(shí)將存錢和取錢的人數(shù)各增加1,再看運(yùn)行結(jié)果
private int num = 0; public static void main(String[] args) { Test test = new Test(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { test.increase(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "存錢1").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { test.decrease(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "取錢1").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { test.increase(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "存錢2").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { test.decrease(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "取錢2").start(); }
產(chǎn)生的結(jié)果已經(jīng)不是最初的只有0和1
造成這個(gè)結(jié)果的原因就是線程間的虛假喚醒
由于目前分別有多個(gè)取款和存款線程。假設(shè)其中一個(gè)存款線程執(zhí)行完畢,并使用wait釋放同步監(jiān)視器鎖定,那其余多個(gè)取款線程將同時(shí)被喚醒,此時(shí)余額為1,如果有10個(gè)同時(shí)取錢,那余額會(huì)變?yōu)?9,造成結(jié)果錯(cuò)誤。
因此,每次線程從wait中被喚醒,都必須再次測(cè)試是否符合喚醒條件,如果不符合那就繼續(xù)等待。
由于多個(gè)線程被同時(shí)喚醒,在if(xxxx){wait();}處 if判斷只會(huì)執(zhí)行一次,當(dāng)下一個(gè)被喚醒的線程過來(lái)時(shí),由于if已經(jīng)判斷過,則直接從wait后面的語(yǔ)句繼續(xù)執(zhí)行,因此將if換成while可解決該問題,下次被喚醒的線程過來(lái),while重新判斷一下,發(fā)現(xiàn)上一個(gè)被喚醒的線程已經(jīng)拿到鎖,因此這個(gè)被虛假喚醒的線程將繼續(xù)等待鎖。
/** * 存一塊錢 * * @throws InterruptedException */ public synchronized void increase() throws InterruptedException { while (num == 1) {// 防止每次進(jìn)來(lái)的喚醒線程只判斷一次造成虛假喚醒,替換成while this.wait(); } num++; System.out.println(Thread.currentThread().getName() + ":num=" + num); this.notifyAll(); } /** * 取一塊錢 * * @throws InterruptedException */ public synchronized void decrease() throws InterruptedException { while (num == 0) { this.wait(); } num--; System.out.println(Thread.currentThread().getName() + ":num=" + num); this.notifyAll(); }
再次運(yùn)行,結(jié)果正常:
到此這篇關(guān)于Java線程通信及線程虛假喚醒知識(shí)總結(jié)的文章就介紹到這了,更多相關(guān)Java線程通信及線程虛假喚醒內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java創(chuàng)建線程池的7種實(shí)現(xiàn)方法
在Java中線程池是一種管理線程的機(jī)制,它可以創(chuàng)建一組線程并重復(fù)使用它們,避免了創(chuàng)建和銷毀線程的開銷,這篇文章主要給大家介紹了關(guān)于java創(chuàng)建線程池的7種實(shí)現(xiàn)方法,需要的朋友可以參考下2023-10-10springboot整合websocket最基礎(chǔ)入門使用教程詳解
這篇文章主要介紹了springboot整合websocket最基礎(chǔ)入門使用教程詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03解決SpringBoot運(yùn)行Test時(shí)報(bào)錯(cuò):SpringBoot Unable to find
這篇文章主要介紹了SpringBoot運(yùn)行Test時(shí)報(bào)錯(cuò):SpringBoot Unable to find a @SpringBootConfiguration,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10Springboot整合mybatis開啟二級(jí)緩存的實(shí)現(xiàn)示例
在一級(jí)緩存中,是查詢兩次數(shù)據(jù)庫(kù)的,顯然這是一種浪費(fèi),既然SQL查詢相同,就沒有必要再次查庫(kù)了,直接利用緩存數(shù)據(jù)即可,這種思想就是MyBatis二級(jí)緩存的初衷,本文就詳細(xì)的介紹了Springboot整合mybatis開啟二級(jí)緩存,感興趣的可以了解一下2022-05-05java簡(jiǎn)單實(shí)現(xiàn)計(jì)算器
這篇文章主要為大家詳細(xì)介紹了java簡(jiǎn)單實(shí)現(xiàn)計(jì)算器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-12-12ArrayList和HashMap如何自己實(shí)現(xiàn)實(shí)例詳解
這篇文章主要介紹了 ArrayList和HashMap如何自己實(shí)現(xiàn)的相關(guān)資料,需要的朋友可以參考下2016-12-12SpringBoot 關(guān)于Feign的超時(shí)時(shí)間配置操作
這篇文章主要介紹了SpringBoot 關(guān)于Feign的超時(shí)時(shí)間配置操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09Redis結(jié)合AOP與自定義注解實(shí)現(xiàn)分布式緩存流程詳解
項(xiàng)目中如果查詢數(shù)據(jù)是直接到MySQL數(shù)據(jù)庫(kù)中查詢的話,會(huì)查磁盤走IO,效率會(huì)比較低,所以現(xiàn)在一般項(xiàng)目中都會(huì)使用緩存,目的就是提高查詢數(shù)據(jù)的速度,將數(shù)據(jù)存入緩存中,也就是內(nèi)存中,這樣查詢效率大大提高2022-11-11