volatile可見性的一些認(rèn)識和論證
一、前言
volatile的關(guān)鍵詞的使用在JVM內(nèi)存模型中已是老生常談了,這篇文章主要結(jié)合自己對可見性的一些認(rèn)識和一些直觀的例子來談?wù)剉olatile。文章正文大致分為三部分,首先會介紹一下happen-before,接著講解volatile的一些使用場景,最后會附上一些例子來論證使用與不使用volatile的區(qū)別。
二、happen-before
對操作系統(tǒng)有認(rèn)識的同學(xué)一定知道,CPU一般有三級緩存,在與內(nèi)存交互的時候,存在緩存與內(nèi)存的更新問題,其次CPU在讀取指令的時候,會做一些指令重排序的工作,提高程序運行效率。類比JVM內(nèi)存模型(見下圖),每個線程擁有自己的工作內(nèi)存,同時存在一個主存,線程間通過主存來進(jìn)行通信,同樣的,JVM也存在指令重排序,可見JVM內(nèi)存模型與實際物理內(nèi)存模型十分相似。(這里順便提一下,編譯器其實也會作一定重排序優(yōu)化)。
作為開發(fā)人員,你不可能了解到每個JVM優(yōu)化細(xì)節(jié),更不可能了解到CPU何時會進(jìn)行指令重排序,所以java語言定義了更上層的一個概念,就是"happen-before"。起初,我看到這個單詞的時候,誤以為這是一個指令執(zhí)行順序的規(guī)則,后來仔細(xì)想想又發(fā)覺不對勁。如果”happen-before“僅僅是抽象了指令執(zhí)行順序的概念,那么它就把握不了“工作內(nèi)存將值寫回主存”和“工作內(nèi)存從主存中刷新自己的值”這個兩個action的時機(jī)。那么這個概念也就變得沒什么意義了。所以!所以!所以!”happen-before“是一個可見性的原則?。?!
下面給出happen-before的具體規(guī)則:
程序次序規(guī)則:一個線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作;
鎖定規(guī)則:一個unLock操作先行發(fā)生于后面對同一個鎖額lock操作;
volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作;
傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C;
線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每個一個動作;
線程中斷規(guī)則:對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生;
線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測到線程已經(jīng)終止執(zhí)行;
對象終結(jié)規(guī)則:一個對象的初始化完成先行發(fā)生于他的finalize()方法的開始;
三、volatile的使用場景
happen-before的第三條規(guī)則提到“volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作”,也就是說;一個volatile變量的寫操作對后續(xù)對讀操作可見。說白了就是每次寫完volatile變量,都會將值從工作內(nèi)存寫回到主存中去,每次讀取volatile變量,工作內(nèi)存必須從主存中刷新下自己的值。如此的話,volatile就是為了解決多個線程共享數(shù)據(jù)的可見性問題。但是不是任何數(shù)據(jù)共享場景都可以使用volatile,必須滿足以下兩種情景才行。
應(yīng)用場景:
1.多個線程不依賴原值的情況下進(jìn)行讀寫操作
2.一個線程依賴原值進(jìn)行寫操作,多個線程進(jìn)行讀操作
在我看來,除了這兩種情況外,無非是多個線程依賴原值進(jìn)行運算,這樣子倒不是說volatile可見性不起作用了,而是無法保證讀取原值和運算是一個原子操作!舉個簡單的例子,多個線程執(zhí)行i++;i是一個共享變量,由于讀取i的值和i自增不是一個原子操作,所以i最終會丟失掉一部分自增過程。代碼如下,最終i輸出的結(jié)果是一個小于1000的整數(shù)。
/** * Created by chenqimiao on 17/8/23. */ public class Testv { public static volatile int i = 0; public static void main(String args[]){ for (int i =0;i<1000;i++){ new Thread(){ public void run(){ Thread.yield(); Testv.i++; } }.start(); } System.out.println(Testv.i); } }
要滿足以上這種需求,我們還必須賦予代碼原子性,最常用的肯定是鎖操作了,一個字穩(wěn),性能可觀,同時保證原子性和可見性。如果想操作一波的話,還可以考慮使用一些無鎖操作,如CAS,象java.util.concurrent包下的一些原子類就是利用了CAS來做到原子性,但原子性并不能保證可見性,這個時候,還需要配合volatile。
以上種種都是對volatile使用場景的概括,想了解具體的使用場景可以參考博文:https://www.ibm.com/developerworks/cn/java/j-jtp06197.html
四、volatile可見性的證明
先上段代碼好了,不知道從何說起了。
package com.example.demo.netty; /** * Created with IntelliJ IDEA. * User: chenqimiao * Date: 2017/8/23 * Time: 9:16 * To change this template use File | Settings | File Templates. */ public class VolatileTest { boolean isStop = false; public void test(){ Thread t1 = new Thread(){ public void run() { isStop=true; } }; Thread t2 = new Thread(){ public void run() { while (!isStop); } }; t2.start(); t1.start(); } public static void main(String args[]) throws InterruptedException { for (int i =0;i<25;i++){ new VolatileTest().test(); } } }
上面這段代碼可能永遠(yuǎn)也不會結(jié)束,因為線程一對isStop的賦值,線程二可能對此并不可見。當(dāng)然只是可能,所以為了放大可見性問題,我這里作了25次循環(huán)。只要有一組線程,“線程一對isStop的賦值,線程二對此不可見”的情況發(fā)生,就不會退出程序。
now,假如你給 isStop 添加一個 volatile 關(guān)鍵字,那么你會發(fā)現(xiàn)程序立馬就會退出。
總結(jié)
以上所述是小編給大家介紹的volatile可見性的一些認(rèn)識和論證,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
相關(guān)文章
解決idea刪除模塊后重新創(chuàng)建顯示該模塊已經(jīng)被注冊的問題
這篇文章主要介紹了解決idea刪除模塊后重新創(chuàng)建顯示該模塊已經(jīng)被注冊的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02jpa使用manyToOne(opntional=true)踩過的坑及解決
這篇文章主要介紹了jpa使用manyToOne(opntional=true)踩過的坑及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10不使用myeclipse注冊機(jī)得到myeclipse注冊碼的方法(myeclipse序列號)
本文為大家介紹不使用myeclipse注冊機(jī)就能得到myeclipse注冊碼(序列號)的方法, 運行下面的JAVA代碼就可以了2014-01-01java 遞歸查詢所有子節(jié)點id的方法實現(xiàn)
在多層次的數(shù)據(jù)結(jié)構(gòu)中,經(jīng)常需要查詢一個節(jié)點下的所有子節(jié)點,本文主要介紹了java 遞歸查詢所有子節(jié)點id的方法實現(xiàn),具有一定的參考價值,感興趣的可以了解一下2024-03-03