給網(wǎng)站劃分欄目怎么做百度關(guān)鍵詞排名
volatile 關(guān)鍵字
volatile 能保證內(nèi)存可見性
volatile 修飾的變量, 能夠保證 “內(nèi)存可見性”
示例代碼:
運行結(jié)果:
當輸入1(1是非O)的時候,但是t1這個線程并沿有結(jié)束循環(huán),
同時可以看到,t2這個線程已經(jīng)執(zhí)行完了,而t1線程還在繼續(xù)循環(huán).
這個情況,就叫做內(nèi)存可見性問題 ~~ 這也是一個線程不安全問題(一個線程讀,一個線程改)
while (myCounter.flag == 0) { // 循環(huán)體空著,什么也不做 }
這里使用匯編來理解,大概就是兩步操作:
- load, 把內(nèi)存中 flag 的值,讀取到寄存器里.
- cmp, 把寄存器的值,和0進行比較,根據(jù)比較結(jié)果,決定下一步往哪個地方執(zhí)行(條件跳轉(zhuǎn)指令).
上述是個循環(huán),這個循環(huán)執(zhí)行速度極快,一秒鐘執(zhí)行百萬次以上…
循環(huán)執(zhí)行這么多次,在線程 t2 真正修改之前, load得到的結(jié)果都是一樣的;
另一方面, load操作和cmp操作相比,速度慢非常非常多!!!
注:
CPU針對寄存器的操作,要比內(nèi)存操作快很多,快3-4數(shù)量級;
計算機對于內(nèi)存的操作,比硬盤快3-4個數(shù)量級.
由于 load 執(zhí)行速度太慢(相比于cmp來說),再加上反復 load 到的結(jié)果都一樣, JVM 就做出了一個非常大膽的決定 ~~ 判定好像沒人改 flag 值,不再真正的重復 load 了,干脆就只讀取一次就好了 => 編譯器優(yōu)化的一種方式.
實際上是有人在修改的,但是 JVM/編譯器 對于這種多線程的情況,判定可能存在誤差.
此時,就需要我們手動干預了,可以給 flag 這個變量加上 volatile 關(guān)鍵字,意思就是告訴編譯器,這個變量是"易變"的,要每次都重新讀取這個變量的內(nèi)存內(nèi)容,不能再進行激進的優(yōu)化了.
博主感慨: 快和準之間往往不可兼得
內(nèi)存可見性問題
一個線程針對一個變量進行讀取操作,同時另一個線程針對這個變量進行修改,此時讀到的值,不一定是修改之后的值;這個讀線程沒有感知到變量的變化;
歸根結(jié)底是 編譯器/JVM 在多線程環(huán)境下優(yōu)化時產(chǎn)生了誤判了.
備注:
(1)上述說的內(nèi)存可見性編譯器優(yōu)化的問題,也不是始終會出現(xiàn)的(編譯器可能存在誤判,也不是100%就誤判!);
(2)編譯器的優(yōu)化,很多時候是“玄學問題”,應用程序這個角度是無法感知的.編譯器的優(yōu)化,很多時候是“玄學問題”,應用程序這個角度是無法感知的.
代碼改正:
class MyCounter {volatile public int flag = 0;
}
運行結(jié)果:
注意事項:
(1) volatile 只能修飾變量;
(2) volatile 不能修飾方法的局部變量,局部變量只能在你當前線程里面用,不能多線程之間同時讀取/修改(天然就規(guī)避了線程安全問題);
(1)局部變量只能在當前方法里使用的,出了方法變量就沒了,方法內(nèi)部的變量在"棧”這樣的內(nèi)存空間上;
(2)每個線程都有自己的??臻g,即使是同一個方法,在多個線程中被調(diào)用,這里的局部變量也會處在不同的??臻g中,本質(zhì)上是不同變量,也就涉及不到修改/讀取同一個變量的操作;
(3)棧記錄了方法之間的調(diào)用關(guān)系;
個人理解: 局部變量只對當前線程可見,其他線程看不了.
(3) 如果一個變量在兩個線程中,一個讀,一個寫,就需要考慮volatile 了;
(4) volatile 不保證原子性,原子性是靠 synchronized 來保證的. synchronized 和 volatile 都能保證線程安全 => 不能使用 volatile 處理兩個線程并發(fā)++這樣的問題;
(5) 如果涉及到某個代碼,既需要考慮原子性,有需要考慮內(nèi)存可見性,就把 synchronized 和 volatile 都用上就行了.
從 JMM 的角度重新表述內(nèi)存可見性問題
內(nèi)存可見性問題,其他的一些資料,談到了JMM(Java Memory Mode ~~ Java內(nèi)存模型)
從 JMM 的角度重新表述內(nèi)存可見性問題(Java的官方文檔的大概表述):
Java 程序里,主內(nèi)存,每個線程還有自己的工作內(nèi)存(線程 t1 的和線程 t2 的工作內(nèi)存不是同一個東西);
線程 t1 進行讀取的時候,只是讀取了工作內(nèi)存的值;
線程 t2進行修改的時候,先修改的工作內(nèi)存的值,然后再把工作內(nèi)存的內(nèi)容同步到主內(nèi)存中,但是由于編譯器優(yōu)化,導致線程 t1沒有重新的從主內(nèi)存同步數(shù)據(jù)到工作內(nèi)存,讀到的結(jié)果就是“修改之前"的結(jié)果.
如果把"主內(nèi)存”代替成"內(nèi)存",把“工作內(nèi)存"代替成"CPU寄存器",就容易理解.
注: 之所以上面這段話這么晦澀,是翻譯不行,翻譯官得背鍋 ~~ 翻譯的結(jié)果讓人誤會了!!!
主內(nèi)存: main memory => 主存,也就是平時所說的內(nèi)存
工作內(nèi)存: work memory =>工作存儲區(qū),并非是所說的內(nèi)存,而是CPU上存儲數(shù)據(jù)的單元(寄存器)
為什么Java這里,不直接叫做“CPU寄存器",而是專門搞了"工作內(nèi)存”說法呢?
這里的工作內(nèi)存,不一定只是CPU的寄存器,還可能包括CPU的緩存cache.
當CPU要讀取一個內(nèi)存數(shù)據(jù)的時候,可能是直接讀內(nèi)存也可能是讀cache還能是讀寄存器…
引入cache之后,硬件結(jié)構(gòu)就更復雜了,工作內(nèi)存(工作存儲區(qū)): CPU寄存器 + CPU的cache;
一方面是為了表述簡單,另一方面也是為了避免涉及到硬件的細節(jié)和差異,Java里就使用"工作內(nèi)存"這個詞來統(tǒng)稱(泛指)了;畢竟,現(xiàn)實中有的 CPU 可能沒有 cache, 有的 CPU 有;有的 CPU 可能有一個cache,還可能有多個;現(xiàn)代的 CPU 普遍是3級cache, L1, L2, L3,總之,情況多樣.
注: 學校的"計算機系統(tǒng)結(jié)構(gòu)”會講解CPU內(nèi)部的結(jié)構(gòu),尤其是寄存器, cache,指令等等,上這門課的時候要好好聽講.
wait 和 notify
線程最大的問題,是搶占式執(zhí)行,隨機調(diào)度~~
程序猿寫代碼,不喜歡隨機,喜歡確定的東西,于是發(fā)明了一些辦法,來控制線程之間的執(zhí)行順序,雖然線程在內(nèi)核里的調(diào)度是隨機的,但是可以通過一些 API 讓線程主動塞,主動放棄CPU(給別的線程讓路).
比如,t1,t2 兩個線程,希望t1先干活,干的差不多了,再讓t2來干.就可以讓t2先wait(阻塞,主動放棄CPU)等t1干的差不多了,再通過notify 通知t2,把t2喚醒,讓t2接著干.
那么上述場景,使用 join 或者 sleep行不行呢?
使用join,則必須要t1徹底執(zhí)行完,t2才能運行.如果是希望t1先干50%的活,就讓t2開始行動,join無能為力.
使用sleep,指定一個休眠時間的,但是t1執(zhí)行的這些活,到底花了多少時間,不好估計.
使用wait和notify可以更好的解決上述的問題.
注: wait, notify, notifyAll 這幾個類,都是Object類(Java里所有類的祖宗)的方法.Java里隨便new個對象,都可以有這三個方法!!
wait
wait 進行阻塞.某個線程調(diào)用wait方法,就會進入阻塞(無論是通過哪個對象 wait的),此時就處在WAITING狀態(tài).
wait 不加任何參數(shù),就是一個"死等"一直等待,直到有其它線程喚醒它.
示例代碼:
public class ThreadDemo16 {public static void main(String[] args) throws InterruptedException {Object object = new Object();object.wait();}
}
throws InterruptedException
: 這個異常,很多帶有阻塞功能的方法都帶.這些方法都是可以 interrupt 方法通過這個異常給喚醒的.
運行結(jié)果:
IllegalMonitorStateException
~~ 非法的鎖狀態(tài)異常
~~ 鎖的狀態(tài),無非就是被加鎖的狀態(tài)和和被解鎖的狀態(tài).
為什么有這個異常,要先理解 wait 的操作是干什么了.
1.先釋放鎖
2.進行阻塞等待
3.收到通知之后,重新嘗試獲取鎖,并且在獲取鎖后,繼續(xù)往下執(zhí)行.
這里鎖狀態(tà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 之后");}}
雖然這里wait是阻塞了,阻塞在 synchronized 代碼塊里,實際上,這里的阻塞是釋放了鎖的,此時其他線程是可以獲取到object這個對象的鎖的,此時這里的阻塞,就處在WAITING狀態(tài).
t1.start();
t2.start();
如果代碼這里寫作 t1.start 和 t2.start 由于線程調(diào)度的不確定性,此時不能保證一定是先執(zhí)行 wait ,后執(zhí)行notify. 如果調(diào)用notify,此時沒有線程wait,此處的wait是無法被喚醒的!!!(這種通知就是無效通知).
因此此處的代碼還是要盡量保證先執(zhí)行wait后執(zhí)行notify才是有意義的.
改正的代碼:
public static void main(String[] args) throws InterruptedException {Object object = new Object();Thread t1 = new Thread(() -> {// 這個線程負責進行等待System.out.println("t1: wait 之前");try {synchronized (object) {object.wait();}} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t2: wait 之后");});Thread t2 = new Thread(() -> {System.out.println("t2: notify 之前");synchronized (object) {// notify 務必要獲取到鎖,才能進行通知object.notify();}System.out.println("t2: notify 之后");});t1.start();// 此處寫的 sleep 500 是大概率會讓 t1 先執(zhí)行 wait 的// 極端情況下,電腦特別卡的時候, 可能線程的調(diào)度時間就超過了 500 ms// 還是可能 t2 先執(zhí)行 notifyThread.sleep(500);t2.start();}
運行結(jié)果:
此處,先執(zhí)行了wait,很明顯wait操作阻塞了,沒有看到wait之后的打印;
接下來執(zhí)行到了t2, t2進行了notify的時候,才會把t1的wait喚醒.t1才能繼續(xù)執(zhí)行.
只要t2不進行notify,此時t1就會始終wait下去(死等).
wait無參數(shù)版本,就是死等的.
wait帶參數(shù)版本,指定了等待的最大時間.
wait的帶有等待時間的版本,看起來就和sleep有點像.其實還是有本質(zhì)差別的:
雖然都是能指定等待時間,也都能被提前喚醒(wait是使用notify 喚醒, sleep使用interrupt喚醒)但是這里表示的含義截然不同.
notify喚醒wait,這是不會有任何異常的.(正常的業(yè)務邏輯),interrupt喚醒sleep 則是出異常了(表示一個出問題了的邏輯).
如果當前有多個線程在等待object對象,此時有一個線程 object.notify(),此時是隨機喚醒一個等待的線程.(不知道具體是哪個),但是,可以用多組不同的對象來控制線程的執(zhí)行順序.
比如,有三個線程,希望先執(zhí)行線程1,再執(zhí)行線程2,再執(zhí)行線程3,
創(chuàng)建obj1,供線程1,2使用創(chuàng)建obj2,供線程2,3使用線程3, obj2.wait
線程2.obj1.wait(),喚醒之后執(zhí)行obj2.notify()
線程1執(zhí)行自己的任務,執(zhí)行完了之后,obj1.notify即可.
notifyAll和notify非常相似.
多個線程 wait 的時候, notify隨機喚醒一個, notifyAll 所有線程都喚醒,這些線程再一起競爭鎖…