JUC并發(fā)編程之Volatile關(guān)鍵字詳解
保證被volatile修飾的共享變量對所有線程總數(shù)可見的,也就是當(dāng)一個線程修改了一個被volatile修飾共享變量的值,新值總是可以被其他線程立即得知。
禁止指令重排序優(yōu)化。
@Slf4jpublic class Test01 {private static boolean initFlag = false;public static void refresh() {log.info("refresh data.......");initFlag = true;log.info("refresh data success.......");}public static void main(String[] args) {Thread threadA = new Thread(() -> {while (!initFlag) {}log.info("線程:" + Thread.currentThread().getName()+ "當(dāng)前線程嗅探到initFlag的狀態(tài)的改變");}, "threadA");threadA.start();try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}Thread threadB = new Thread(() -> {refresh();}, "threadB");threadB.start();}}

帶著這個疑惑,將代碼稍微改動一下,往 "initFlag" 變量加上"volatile",然后再來看看它的效果是如何?

先來看看這種圖,或許會更加的好理解一點

分析結(jié)論:先看到我紅色標(biāo)記的一段話,A線程內(nèi)部的循環(huán)最終都會跳出來,只不過是時間長短的問題而已。
結(jié)合上圖分析,initFlag作為成員變量,程序會將它存放在主內(nèi)存中,當(dāng)線程A和B啟動后,如果線程需要用到主內(nèi)存的initFlag,線程會從主內(nèi)存中將變量復(fù)制一份到自己內(nèi)部的工作內(nèi)存中,然后再對變量進行操作。而不是直接在線程內(nèi)部對主內(nèi)存中的變量進行操作。那么這就會有一個問題,當(dāng)線程B對工作內(nèi)存中的initFlag值進行改變后,然后將initFlag值從工作內(nèi)存中推回到主內(nèi)存,這時候線程A可能不會立即知道主內(nèi)存的值已經(jīng)發(fā)生了改變,因為A線程中的空循環(huán)它的優(yōu)先級是非常高的,它會一直占用CPU來執(zhí)行這串代碼,這就導(dǎo)致JVM無法讓CPU分點時間去主內(nèi)存中拉取最新的值。而加了volatile后,它會通知其他有用到initFlag變量的線程,強制它去拉取主內(nèi)存中最新變量的值,然后重新刷回到內(nèi)部的工作內(nèi)存中。簡單來說,加了volatile關(guān)鍵字會強制保證線程的可見性;而不加的話,JVM也會盡力的保證線程的可見性(也就是CPU空閑的時候),這也就是我前面為什么會說無論是否加了 "volatile" A線程內(nèi)部的循環(huán)最終都會退出來原因。
看到這相信對volatile的可見性有了一定的了解,接著再繼續(xù)來看看volatile它是否能夠解決并發(fā)中的原子性呢?
public class Test02 {private static int counter = 0;public static void main(String[] args) {for (int i = 0; i < 100; i++) {Thread thread = new Thread(()->{for (int j = 0; j < 1000; j++) {counter++;}});thread.start();}try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(counter);}}

下圖是在成員變量上加了volatile的效果圖

因為volatile它并不能夠解決并發(fā)中的原子性問題,看到這是不是又懵逼了?代碼中的counter++就一行代碼,為什么不是原子操作呢??其實這里是有一個坑的,其實counter++并非是一步操作,它在底層是被拆分為三個步驟進行執(zhí)行的,且看,counter++操作是counter = counter + 1的簡寫操作對吧,那么我們可以簡單的思考一下,counter的值是怎么來的呢?
根據(jù)這個思考,再來拆分一下它三個細致的步驟:
我們都知道,線程是基于時間片進行執(zhí)行的,在多線程下,假如線程內(nèi)部剛好執(zhí)行完第一步或者第二步操作,這個時候CPU發(fā)生中斷操作,它并沒有去執(zhí)行該線程內(nèi)的第三步操作(意思是暫停執(zhí)行第三步操作,等到時間片輪詢到該線程再回來繼續(xù)執(zhí)行接下來的操作),轉(zhuǎn)而去執(zhí)行另外一個線程的一個自增操作,這個時候就會出現(xiàn)問題,第一個線程執(zhí)行完第二步操作后發(fā)生暫停,轉(zhuǎn)而執(zhí)行第二個線程自增操作,回看前面所說的volatile可見性特性, 因為加了volatile的原因,第二個線程改變完值后,會通知第一個線程現(xiàn)有的counter變量已經(jīng)過期,需要重新去拉取主內(nèi)存最新的值,這個時候就會造成,我兩個線程都發(fā)生了自增操作,但是只有一個線程自增成功了,那么結(jié)果自然就不對,這也就造成了線程安全的問題。
從上面例子我們可以確定volatile是不能保證原子性的,要保證運算的原子性可以使用java.util.concurrent.atomic包下的一些原子操作類,或者使用synchronized同步塊和Lock鎖來解決該問題。
我前面有提到過線程是基于時間片執(zhí)行的,從時間的維度上來講,在線程內(nèi),上一行代碼總會比下一行代碼優(yōu)先執(zhí)行,但是在CPU里面它又不同了,它可能會將下一行的代碼放到上一行先去執(zhí)行,看到這估計有小伙伴有點懵了?啥玩意兒?這不是逗我玩嗎?代碼上中是從上往下執(zhí)行,結(jié)果到你CPU又給我亂序執(zhí)行?說著這,就不得不說到指令重排的概念了。
什么是指令重排
為什么要指令重排
下圖為從源碼到最終執(zhí)行的指令序列示意圖:

@Slf4jpublic class Test03 {private static int x = 0, y = 0;private static int a = 0, b = 0;public static void main(String[] args) throws InterruptedException {int i = 0;for (; ; ) {i++;x = 0;y = 0;a = 0;b = 0;Thread t1 = new Thread(()->{shortWait(10000);a = 1;x = b;});Thread t2 = new Thread(()->{b = 1;y = a;});t1.start();t2.start();t1.join();t2.join();String result = "第" + i + "次 (" + x + "," + y + ")";if (x == 0 && y == 0) {System.out.println(result);break;} else {log.info(result);}}}/*** 等待一段時間,時間單位納秒** @param interval*/public static void shortWait(long interval) {long start = System.nanoTime();long end;do {end = System.nanoTime();} while (start + interval >= end);}}
第一種結(jié)果:先排除指令重排,當(dāng)這段代碼以我們視覺效果的從上往下執(zhí)行,結(jié)果就是x=0,y=1(因為t1線程已經(jīng)執(zhí)行完了,t2線程才來執(zhí)行)






volatile禁止重排優(yōu)化
內(nèi)存屏障其實簡單理解的話,假如代碼中有兩行代碼,這兩行代碼在底層可能會發(fā)生指令重排,那么我不想讓他發(fā)生重排怎么辦呢?內(nèi)存屏障的作用就體現(xiàn)出來啦,我們可以將內(nèi)存屏障插在兩行代碼中間,告訴編譯器或者CPU等,不讓它進行重排,當(dāng)然內(nèi)存屏障是關(guān)于硬件層面的一些知識了,其實JVM也幫我們基于硬件層面的內(nèi)存屏障封裝好了軟件層面的內(nèi)存屏障,先來看看硬件層的內(nèi)存屏障有哪些?
硬件層的內(nèi)存屏障
看到這,針對上面的幾個表格看的是不是還有點懵圈,沒關(guān)系,我接下來會對上面的表格的內(nèi)容做一個簡單總結(jié),以及通過代碼演示。相信大家伙應(yīng)該會收獲很多。
public class ReadAndWrithe {int a = 1;int c = 0;private static volatile int d = 5;/*** 第一個操作普通讀寫 第二個操作普通讀寫 可以重排*/public void test1() {//第一個操作:普通讀寫//讀取的a變量是成員變量但是沒有被volatile所修飾,所以為普通讀操作//定義的b變量是局部變量,所以為普通寫操作int b = a + 1;//第二個操作:普通讀寫//讀取的a變量和b變量都沒有被volatile所修飾,所以為普通讀操作//定義的c變量是成員變量沒有被volatile所修飾,所以為普通寫操作c = 2;//該結(jié)論則是可以重排}/*** 第一個操作普通讀寫 第二個操作volatile讀 可以重排*/public void test2() {//第一個操作:普通讀寫//讀取的a變量是成員變量但是沒有被volatile所修飾,所以為普通讀操作//定義的b變量是局部變量,所以為普通寫操作int b = a + 1;//第二個操作:volatile讀(準確來說:volatile讀,普通寫)//讀取的d變量是成員變量且是被volatile所修飾,所以為volatile讀操作//定義的c變量是成員變量沒有被volatile所修飾,所以為普通寫操作c = d;//該結(jié)論則是可以重排}/*** 第一個操作普通讀 第二個操作volatile寫 不可以重排*/public void test3() {//第一個操作:普通讀寫//讀取的a變量是成員變量但是沒有被volatile所修飾,所以為普通讀操作//定義的b變量是局部變量,所以為普通寫操作int b = a + 1;//第二個操作:volatile寫(準確來說:volatile寫,普通讀)//讀取的c變量是成員變量但是沒有被volatile所修飾,所以為普通讀操作//賦值d變量是成員變量且是被volatile所修飾,所以為volatile寫操作d = c;//該結(jié)論則是不可以重排}}
public class ReadAndWrithe {int a = 1;int c = 0;private static volatile int d = 5;/*** 第一個操作為volatile讀,第二個操作為普通讀寫 不允許重排*/public void test1() {//第一個操作:volatile讀(準確來說:volatile讀,普通寫)//讀取的d變量是成員變量是被volatile所修飾,所以為volatile讀//定義的j變量為成員變量,所以為普通寫int j = d;//第二個操作:普通讀寫a = 5;}/*** 第一個操作為volatile讀,第二個操作為volatile讀 不允許重排*/public void test2() {//第一個操作:volatile讀//讀取的d變量是成員變量是被volatile所修飾,所以為volatile讀//定義的j變量為成員變量,所以為普通寫int j = d;//第二個操作:volatile讀//讀取的d變量是成員變量是被volatile所修飾,所以為volatile讀//定義的f變量為成員變量,所以為普通寫int f = d;}/*** 第一個操作為volatile讀,第二個操作為volatile寫 不允許重排*/public void test3() {//第一個操作:volatile讀//讀取的d變量是成員變量是被volatile所修飾,所以為volatile讀//定義的j變量為成員變量,所以為普通寫int j = d;//第二個操作:volatile寫//讀取的a變量是成員變量但不是被volatile所修飾,普通讀//賦值的d變量為volatile所修飾,所以為volatile寫d = a;}}
public class ReadAndWrithe {int a = 1;int c = 0;private static volatile int d = 5;private static volatile int d2 = 2;/*** 第一個操作為volatile寫,第二個操作為普通讀寫 可以重排*/public void test1() {//第一個操作:volatile寫(準確來說:volatile寫,普通讀)//3:普通讀//賦值的d是volatile所修飾的,所以為volatile寫d = 3;//第二個操作:普通讀寫a = 5;}/*** 第一個操作為volatile寫,第二個操作為volatile讀 不允許重排*/public void test2() {//第一個操作:volatile寫//3:普通讀//賦值的d是volatile所修飾的,所以為volatile寫d = 3;//第二個操作:volatile讀//讀取的d變量是成員變量是被volatile所修飾,所以為volatile讀//定義的f變量為成員變量,所以為普通寫int f = d;}/*** 第一個操作為volatile寫,第二個操作為volatile寫 不允許重排*/public void test3() {//第一個操作:volatile寫//3:普通讀//賦值的d是volatile所修飾的,所以為volatile寫d = 3;//第二個操作:volatile寫//讀取的a變量是成員變量但不是被volatile所修飾,普通讀//賦值的d2變量為volatile所修飾,所以為volatile寫d2 = a;}}
當(dāng)然jvm虛擬機也不會隨意將我們的代碼進行指令重排,還需要遵守以下規(guī)則
happens-before 原則
1.程序順序原則,即在一個線程內(nèi)必須保證語義串行性,也就是說按照代碼順序執(zhí)行。
2.鎖規(guī)則 解鎖(unlock)操作必然發(fā)生在后續(xù)的同一個鎖的加鎖(lock)之前,也就是說,如果對于一個鎖解鎖后,再加鎖,那么加鎖的動作必須在解鎖動作之后(同一個鎖)。
3.volatile規(guī)則 volatile變量的寫,先發(fā)生于讀,這保證了volatile變量的可見性,簡單的理解就是,volatile變量在每次被線程訪問時,都強迫從主內(nèi)存中讀該變量的值,而當(dāng)該變量發(fā)生變化時,又會強迫將最新的值刷新到主內(nèi)存,任何時刻,不同的線程總是能夠看到該變量的最新值。
4.線程啟動規(guī)則 線程的start()方法先于它的每一個動作,即如果線程A在執(zhí)行線程B的start方法之前修改了共享變量的值,那么當(dāng)線程B執(zhí)行start方法時,線程A對共享變量的修改對線程B可見
5.傳遞性 A先于B ,B先于C 那么A必然先于C
6.線程終止規(guī)則 線程的所有操作先于線程的終結(jié),Thread.join()方法的作用是等待當(dāng)前執(zhí)行的線程終止。假設(shè)在線程B終止之前,修改了共享變量,線程A從線程B的join方法成功返回后,線程B對共享變量的修改將對線程A可見。
7.線程中斷規(guī)則 對線程 interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測線程是否中斷。
8.對象終結(jié)規(guī)則對象的構(gòu)造函數(shù)執(zhí)行,結(jié)束先于finalize()方法
我是黎明大大,我知道我沒有驚世的才華,也沒有超于凡人的能力,但畢竟我還有一個不屈服,敢于選擇向命運沖鋒的靈魂,和一個就是傷痕累累也要義無反顧走下去的心。
如果您覺得本文對您有幫助,還請關(guān)注點贊一波,后期將不間斷更新更多技術(shù)文章


