JavaEE中volatile、wait和notify詳解
一.volatile 關(guān)鍵字.
1.volatile 能保證內(nèi)存可見(jiàn)性問(wèn)題
什么是內(nèi)存可見(jiàn)性?
可見(jiàn)性指 , 一個(gè)線程對(duì)內(nèi)存的修改 , 能夠及時(shí)的被其他線程看到.
Java內(nèi)存模型(JMM):Java虛擬機(jī)規(guī)范中定義了Java內(nèi)存模型 , 目的是屏蔽一切硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異 , 以實(shí)現(xiàn)Java程序在各種平臺(tái)下都能達(dá)到一致的并發(fā)效果.
- 線程之間的共享變量存在主內(nèi)存(Main Memory)
- 每一個(gè)線程都有自己的"工作內(nèi)存"(寄存器)
- 當(dāng)線程要讀取一個(gè)共享變量時(shí) , 會(huì)把共享變量從主內(nèi)存拷貝到工作內(nèi)存, 再?gòu)墓ぷ鲀?nèi)存中讀取數(shù)據(jù).
- 當(dāng)線程要修改共享變量時(shí) , 也先修改工作內(nèi)存中的副本 , 最后同步到主內(nèi)存中.
由于每個(gè)線程都有自己的工作內(nèi)存, 這些工作內(nèi)存中的內(nèi)容相當(dāng)于同一個(gè)共享變量的副本 , 此時(shí)修改線程 t1 的工作內(nèi)存中的值 , 線程 t2 的工作內(nèi)存不一定及時(shí)發(fā)生變化.這時(shí)代碼就容易發(fā)生問(wèn)題.
此時(shí)引出兩個(gè)問(wèn)題:
- 為什么要這么多內(nèi)存
- 為什么要拷貝多次
1) 為什么要這么多內(nèi)存?
實(shí)際并沒(méi)有這么多的內(nèi)存 , 這只是Java規(guī)范中的一個(gè)術(shù)語(yǔ) , 是術(shù)語(yǔ)抽象的叫法.
所謂主內(nèi)存才是真正硬件角度的內(nèi)存 , 而所謂工作內(nèi)存 , 則是指CPU的寄存器和高速緩存(cache).至于為什么起名工作內(nèi)存 , 一方面是為了表述簡(jiǎn)單 , 另一方面也是避免涉及到硬件的細(xì)節(jié)和差異 , 例如有的CPU可能沒(méi)有cache , 有的還存在很多個(gè) , 因此Java就使用工作內(nèi)存一言蔽之了.
2) 為什么要多次拷貝?
因?yàn)镃PU訪問(wèn)寄存器和高速緩存的速度 , 比訪問(wèn)寄存器快了3-4個(gè)數(shù)量級(jí).
如果要連續(xù)10次讀取同一個(gè)數(shù)據(jù) , 不斷從內(nèi)存中訪問(wèn)就很慢 , 那么如果第一次從內(nèi)存中讀取到寄存器 , 后面9次從寄存器中讀取就會(huì)快很多.
- volatile 修飾的變量 , 能夠保證內(nèi)存可見(jiàn)性.
代碼示例:
創(chuàng)建兩個(gè)線程 t1 和 t2 , t1 線程循環(huán)重復(fù)快速讀取flag , t2 線程對(duì) flag 進(jìn)行修改.按照預(yù)期結(jié)構(gòu) , 如果我們修改 t2 線程中的 flag 變?yōu)榉? , t1 線程就會(huì)循環(huán)結(jié)束.
class MyCounter{ public int flag = 0; } public class ThreadDemo2 { public static void main(String[] args) { MyCounter myCounter = new MyCounter(); Thread t1 = new Thread(()->{ while (myCounter.flag == 0){ //循環(huán)重復(fù)快速讀取 } System.out.println("循環(huán)結(jié)束"); }); Thread t2 = new Thread(()->{ Scanner scanner = new Scanner(System.in); System.out.println("請(qǐng)輸入一個(gè)數(shù)"); myCounter.flag = scanner.nextInt(); }); t1.start(); t2.start(); } }
結(jié)果與我們預(yù)期并不相符 , 對(duì) flag 作出修改后 , t1 線程并沒(méi)有循環(huán)結(jié)束.
通過(guò) jconsole 查看 t1 線程還在執(zhí)行 , 而 t2 線程已執(zhí)行完畢.
結(jié)合內(nèi)存可見(jiàn)性問(wèn)題 , 答案顯而易見(jiàn). 一個(gè)線程讀 , 一個(gè)線程改 , 會(huì)產(chǎn)生線程不安全問(wèn)題.從匯編的角度來(lái)理解 , 執(zhí)行下面這段代碼分為兩個(gè)步驟:
- load 把內(nèi)存中的值讀到寄存器中.
- cmp 把寄存器的值和0進(jìn)行比較 , 根據(jù)比較結(jié)果決定下一步往哪執(zhí)行(條件循環(huán)指令)
上述循環(huán)操作在寄存器中 , 執(zhí)行速度極快(1秒鐘執(zhí)行百萬(wàn)次以上) , 循環(huán)這么多次 , 在 t2 真正修改前 , load 得到的執(zhí)行結(jié)果都一樣.另一方面 load 相比于 cmp 操作速度慢非常多 , 再加上反復(fù) load 的結(jié)果都一樣 , JVM 就會(huì)認(rèn)為沒(méi)有人改 flag 的值 , 從此不再?gòu)膬?nèi)存中 load flag 的值 , 直接讀取寄存器中保存的 flag , 這時(shí)JVM/編譯器的一種優(yōu)化方式 , 但由于多線程的復(fù)雜性 , 判定可能存在誤差.
解決方式:
此時(shí)為了避免上述情況 , 就需要程序員手動(dòng)干預(yù) , 可以給 flag 這個(gè)變量加上 volatile 關(guān)鍵字.意思是告訴編譯器這個(gè)變量是"易變" , 一定要每次都從內(nèi)存中重新 load 這個(gè)變量 , 不能再進(jìn)行激進(jìn)的優(yōu)化了.
class MyCounter{ public volatile int flag = 0; }
2.volatile 不能保證原子性
volatile 與 synchronized 有本質(zhì)的區(qū)別 , synchronized 保證原子性 , volatile 保證的是內(nèi)存可見(jiàn)性.
代碼示例:
這是最初演示線程安全的代碼 , 兩個(gè)線程分別對(duì) count 自增5萬(wàn)次.
- 去掉修飾 add 方法的 synchronized 關(guān)鍵字.
- 給 count 變量加上 volatile 關(guān)鍵字.
最終代碼執(zhí)行結(jié)果并不是預(yù)期的10w次.
class Counter{ public volatile int count; public void add(){ count++; } } public static void main(String[] args) { Counter counter = new Counter(); Thread t1 = new Thread(()->{ for (int i = 0; i < 50000; i++) { counter.add(); } }); Thread t2 = new Thread(()->{ for (int i = 0; i < 50000; i++) { counter.add(); } }); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("count = "+counter.count); }
二.wait和notify
由于線程的特性是搶占式執(zhí)行隨機(jī)調(diào)度 , 因此線程之間的先后執(zhí)行順序難易預(yù)知 , 但實(shí)際開(kāi)發(fā)中我們希望合理的協(xié)調(diào)多個(gè)線程之間的先后執(zhí)行順序.
完成這個(gè)協(xié)調(diào)工作主要涉及三個(gè)方法:
- wait()/wait(long timeout).讓當(dāng)前線程進(jìn)入等待狀態(tài).
- notify()/notifyAll().喚醒當(dāng)前在對(duì)象上等待的方法.
Tips:wait(),notify(),notifyAll()都是Object類的方法.
通過(guò)上述介紹可以發(fā)現(xiàn) , wait 和 notify 與 join 和 sleep 在功能上有極大的重合之處 , 那么為什么還要開(kāi)發(fā) wait 和 notify 呢?
因?yàn)?, 使用 join 就必須等待一個(gè)線程徹底執(zhí)行完才能換另一個(gè)線程. 如果我們想讓線程1執(zhí)行50% , 然后立即執(zhí)行線程2 , 顯然 join 達(dá)不到這個(gè)效果. 而且使用 sleep 必須指定休眠多長(zhǎng)時(shí)間 , 但線程1執(zhí)行完畢需要花費(fèi)多少時(shí)間并不好估計(jì).所以使用 wait 和 notify 可以更好的解決上述問(wèn)題.
1.wait方法
wait做的事情:
- 先釋放鎖
- 進(jìn)行阻塞等待
- 收到通知后 , 重新嘗試獲取獲取這個(gè)鎖 , 并且在獲取這個(gè)鎖后 , 繼續(xù)往下執(zhí)行.
代碼示例:
public static void main(String[] args) throws InterruptedException { Object object = new Object(); object.wait(); }
運(yùn)行該代碼出現(xiàn)異常 , 這是因?yàn)閳?zhí)行 wait 操作 , 需先獲取當(dāng)前線程的鎖 , 而當(dāng)前線程并沒(méi)加鎖 , 所以會(huì)出現(xiàn)非法鎖狀態(tài)異常.這就好比 , 我的一個(gè)朋友還沒(méi)收到offer就已經(jīng)開(kāi)始挑選公司.
修改后代碼:
public static void main(String[] args) throws InterruptedException { Object object = new Object(); synchronized (object) { System.out.println("wait 之前"); object.wait(); System.out.println("wait 之后"); } }
通過(guò)運(yùn)行結(jié)果可以得知 , 代碼執(zhí)行到object.wait()就進(jìn)入阻塞.實(shí)際上在阻塞狀態(tài)之前 , wait 已經(jīng)釋放了鎖 , 此時(shí)其他線程可以獲取到object對(duì)象的鎖 , 等到 wait 被喚醒后再嘗試獲取這個(gè)鎖.
舉個(gè)例子就是滑稽老鐵去ATM機(jī)取錢 , 當(dāng)他進(jìn)入銀行網(wǎng)點(diǎn)后鎖上門(mén)開(kāi)始操作ATM機(jī) , 結(jié)果發(fā)現(xiàn)ATM機(jī)沒(méi)錢 , 由于銀行外還有排隊(duì)等待辦理其他業(yè)務(wù)的人 , 他只能打開(kāi)鎖后出去(相當(dāng)于 wait 釋放鎖的操作) , 等待運(yùn)鈔車來(lái)存錢(相當(dāng)于 wait 的阻塞等待) , 當(dāng)運(yùn)鈔車把錢存進(jìn)銀行 , 站在外面排隊(duì)等待的滑稽老鐵 , 又要和其他競(jìng)爭(zhēng)進(jìn)入銀行的機(jī)會(huì).(重新嘗試獲取這個(gè)鎖) , 進(jìn)入銀行后執(zhí)行取錢操作(重新加鎖后繼續(xù)執(zhí)行其他操作).
wait結(jié)束等待的條件
- 其他線程調(diào)用該對(duì)象的 notify 方法.
- wait 等待時(shí)間超時(shí).(wait 有一個(gè)帶參方法 , 可以指定等待時(shí)間)
- 其他線程調(diào)用該等待線程的 Interrupted 方法 , 導(dǎo)致 wait 拋出InterruptException異常.
2.notify方法
notify 方法是喚醒等待的線程.
- notifty 方法同樣需要在加鎖的方法和加鎖的代碼塊中調(diào)用 , 該方法是用來(lái)喚醒那些因調(diào)用 wait方法而阻塞等待的線程 , 通知它們重新獲取對(duì)象鎖.
- 如果有多個(gè)線程調(diào)用同一對(duì)象處于等待 , 則由線程調(diào)度器 , 隨機(jī)挑選一個(gè)呈 wait 狀態(tài)的線程喚醒.
- 在 notify 方法執(zhí)行完畢后 , 當(dāng)前線程不會(huì)立即釋放該對(duì)象鎖 , 要等待執(zhí)行 notify 方法的線程徹底退出加鎖代碼塊后才會(huì)釋放鎖對(duì)象.
代碼示例:
public class ThreadDemo3 { public static void main(String[] args) throws InterruptedException { Object object = new Object(); Thread t1 = new Thread(() -> { //這個(gè)線程負(fù)責(zé)進(jìn)行等待 System.out.println("t1: wait 之前"); try { synchronized (object) { object.wait(); } } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("t1: wait 之后"); }); Thread t2 = new Thread(() -> { System.out.println("t2: notify 之前"); //notify務(wù)必獲取鎖才能通知 synchronized (object) { object.notify(); } System.out.println("t2: notify 之后"); }); t1.start(); //此時(shí)讓 wait 先執(zhí)行,防止 notify 空打一炮. Thread.sleep(100); t2.start(); } }
觀察代碼執(zhí)行結(jié)果明顯符合預(yù)期.
為什么 notify 方法也要在同步方法或同步代碼塊中?
同步方法或同步代碼塊指的是 , 加鎖的方法或加鎖的代碼塊.
代碼示例:
假設(shè)我們要實(shí)現(xiàn)一個(gè)阻塞隊(duì)列 , 如果不加同步代碼塊實(shí)現(xiàn)方法如下:
class BlockingQueen{ Queue<String> queue = new LinkedList<>(); Object lock = new Object(); public void add(String data){ queue.add(data); lock.notify(); } public String take() throws InterruptedException { while (queue.isEmpty()){ lock.wait(); } //返回隊(duì)列的頭結(jié)點(diǎn) return queue.remove(); } }
這段代碼的核心思想是 , 當(dāng)隊(duì)列為空時(shí)使用lock.wait()阻塞 , 如果調(diào)用add()方法添加元素時(shí)再采用lock.notify()喚醒.這段代碼可能產(chǎn)生以下問(wèn)題:
- 一個(gè)消費(fèi)者調(diào)用 take() 方法獲取數(shù)據(jù) , 但queue.isEmpty() , 于是反饋給生產(chǎn)者.
- 在消費(fèi)者調(diào)用 wait 之前 , 由于CPU的調(diào)度 , 消費(fèi)者線程被掛起 , 生產(chǎn)者調(diào)用add() , 然后notify().
- 之后消費(fèi)者調(diào)用wait().由于錯(cuò)誤的條件判斷導(dǎo)致 wait 調(diào)用在 notify 之后.
- 在這種情況下 , 消費(fèi)者就會(huì)一直被掛起 , 生產(chǎn)者也不再生產(chǎn) , 這個(gè)阻塞隊(duì)列就有問(wèn)題.
由此看來(lái) , 在調(diào)用 wait 和 notify 這種會(huì)掛起的操作時(shí) , 需要一種同步機(jī)制保證
3.wait和sleep的對(duì)比
理論上 wait 和 sleep 沒(méi)有可比性 , 因?yàn)?wait 常用于線程間通信 , sleep 則是讓線程阻塞一段時(shí)間 , 唯一的相同點(diǎn)是都可以讓線程放棄執(zhí)行一段時(shí)間.
- 1.wait 需要搭配 synchronized 關(guān)鍵字使用 , 而sleep則不需要.
- 2.wait 是object 方法 , sleep則是Thread類的靜態(tài)方法.
- 3.wait 被notify 喚醒屬于正常的業(yè)務(wù)范疇 , sleep 被Interrupt 喚醒需要報(bào)異常.
總結(jié)
到此這篇關(guān)于JavaEE中volatile、wait和notify的文章就介紹到這了,更多相關(guān)JavaEE volatile、wait和notify內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中使用Hutool的DsFactory操作多數(shù)據(jù)源的實(shí)現(xiàn)
在Java開(kāi)發(fā)中,管理多個(gè)數(shù)據(jù)源是一項(xiàng)常見(jiàn)需求,Hutool作為一個(gè)全能的Java工具類庫(kù),提供了DsFactory工具,幫助開(kāi)發(fā)者便捷地操作多數(shù)據(jù)源,感興趣的可以了解一下2024-09-09Java項(xiàng)目開(kāi)發(fā)中實(shí)現(xiàn)分頁(yè)的三種方式總結(jié)
這篇文章主要給大家介紹了關(guān)于Java項(xiàng)目開(kāi)發(fā)中實(shí)現(xiàn)分頁(yè)的三種方式,通過(guò)這一篇文章可以很快的學(xué)會(huì)java分頁(yè)功能,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-02-02java Scanner輸入數(shù)字、字符串過(guò)程解析
這篇文章主要介紹了java Scanner輸入數(shù)字、字符串過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-10-10java swing實(shí)現(xiàn)的掃雷游戲及改進(jìn)版完整示例
這篇文章主要介紹了java swing實(shí)現(xiàn)的掃雷游戲及改進(jìn)版,結(jié)合完整實(shí)例形式對(duì)比分析了java使用swing框架實(shí)現(xiàn)掃雷游戲功能與相關(guān)操作技巧,需要的朋友可以參考下2017-12-12舉例講解Java設(shè)計(jì)模式中的對(duì)象池模式編程
這篇文章主要介紹了Java設(shè)計(jì)模式中的對(duì)象池模式編程示例分享,對(duì)象池模式經(jīng)常在多線程開(kāi)發(fā)時(shí)被用到,需要的朋友可以參考下2016-02-02優(yōu)化spring?boot應(yīng)用后6s內(nèi)啟動(dòng)內(nèi)存減半
這篇文章主要為大家介紹了優(yōu)化spring?boot后應(yīng)用6s內(nèi)啟動(dòng)內(nèi)存減半的優(yōu)化示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-02-02