一、volatile的實作原理
synchronized是阻塞式同步,在執行緒競爭激烈的情況下會升級為重量級鎖,而volatile就可以說是JVM提供的最輕量級的同步機制,JMM告訴我們,各個執行緒會將共享變數從主記憶體中拷貝到作業記憶體,然后執行引擎會基于作業記憶體中的資料進行操作處理,執行緒在作業記憶體進行操作后何時會寫入主記憶體中?這個實際對普通變數沒有規定的,而針對volatile修飾的變數給Java虛擬機特殊的約定,執行緒對volatile變數的修改會立刻被其他執行緒所感知,即不會出現資料臟讀,從而保證資料的可見性,
被volatile修飾的變數能夠保證每個執行緒能夠獲取該變數的最新值,從而避免出現資料臟讀現象
在生成匯編代碼時會在volatile修飾的共享變數進行寫操作的時候回多出Lock前綴的指令,主要有兩個方面的影響:
將當前處理器快取行的資料寫回系統記憶體;
這個寫回記憶體的操作會使得其他CPU 里快取了該記憶體地址的資料無效,當處理器發現本地快取失效后,就會從記憶體中重讀該變數資料,即可以獲取當前最新值,
這樣volatile變數通過這樣的機制就是的每個執行緒都能獲得該變數的最新值
二、volatile能保證執行緒安全嗎--原子操作(i++)
volatile并不能保證執行緒安全,volatile關鍵字保證可見性、有序性,單不保證原子性,
可見性
對一個volatile變數的讀,總能看到(任意執行緒)對這個volatile變數的寫入,
原子性
對任意單個volatile變數的讀/寫具有原子性,單類似于volatile++這種復合操作不具有原子性
多執行緒下自增
很多人認為,多執行緒下i++這個是多執行緒并發問題,在變數count之前加上volatile就可以避免這個問題,看看結果是不是復合我們的預期,
package passtra;
public class Conter{
public volatile static int count=0;
public static void inc(){
try {
Thread.sleep(1);
} catch (Exception e) {
// TODO: handle exception
}
count++;
}
public static void main(String[] args) {
for(int i=0;i<1000;i++){
new Thread(new Runnable() {
@Override
public void run() {
Conter.inc();
}
}).start();
}
System.err.println("運行結果:Counter.cont="+Conter.count);
}
}
運行結果:Counter.cont=980
運行結果不是我們期望的1000,每次運行的結果也不相同
原因分析
這是因為雖然volatile保證了記憶體可見性,每個執行緒拿到的值都是最新值,但是count++這個操作并不是原子的,這里面涉及到獲取值、自增、賦值的操作并不能同時完成,所以每個執行緒最終賦值是會進行重復賦值
1、JVM運行時記憶體區域,其中有一個記憶體區域是JVM虛擬機堆疊,每個執行緒運行時都有一個執行緒堆疊(執行緒私有)
執行緒堆疊保證了執行緒運行時變數值資訊
2、當執行緒訪問某一個物件值得時候,首先通過物件的參考找到對應在堆記憶體的變數的值,然后把堆記憶體變數的具體指load到執行緒本地記憶體中,建立一個變數副本,
之后執行緒就不在和物件在堆記憶體中的變數值有任何關系了,而是直接修改副本變數的值
3、在修改完之后的某一個時刻(執行緒退出之前),自動把執行緒變數副本的值寫回到物件在堆中變數
這樣在堆中的變數的值就發生了變化
4、互動圖如下:

- read and load:從主存復制3變數到當前作業記憶體
- use and assign:執行代碼,改變共享變數值
- store and write:執行緒本地作業記憶體資料重繪主存相關內容
其中:
1、use and assgin可以多次出現,但是這些操作并不是原子性的,也就是在read load之后,如果主記憶體count變數發生修改后,執行緒作業記憶體中的值由于已經加載,不會產生對應的變化,所以計算出來的結果和預期不會一樣,
2、對于volatile修飾的變數,jvm只是保證從主記憶體加載到執行緒作業記憶體的值是最新的,例如:
執行緒A,執行緒B在進行read,load操作中,發現記憶體中count的值都是5,那么都會加載這個最新的值;
在執行緒A對count進行修改之后,會write到主記憶體中,主記憶體中的count變數就會變為6;
執行緒B對由于已經進行read load操作,在進行運算之后,也會更新主記憶體count的變數值為6;
導致兩個執行緒及時使用volatile關鍵字修改之后,還會存在并發的情況
解決辦法
1、可以使執行緒串行執行(其實就是單執行緒,沒有發揮多執行緒的優勢)
2、可以使用synchronized或者鎖的范式保證原子性
3、使用Atomic包中的AtomicInteger來替換int,它利用CAS演算法保證了原子性
三、volatile的防止指令重排應用--雙重懶加載單利模式
package passtra;
public class Singleton{
private static volatile Singleton singleton;
private Singleton(){}
public static Singleton getsingleton(){
if(singleton==null){
synchronized (Singleton.class) {
if(singleton==null){
singleton=new Singleton();
}
}
}
return singleton;
}
}
這里的volatile關鍵字就是為了防止指令重排,
如果不用volatile,singleton=new Singleton();這段代碼其實分了三步:
分配記憶體空間(1)
初始化物件(2)
將singleton物件指向分配的記憶體地址(3)
加上volatile是為了讓這三步操作順序執行,反之有可能第二部在第三部之前執行,就有可能某個執行緒拿到的單利物件是還沒有初始化的,以至于報錯,
四、volatile在Java并發中的應用
volatile在Java并發中用的很多,比如:Atomic包中的value,以及AbstractQueuedLongSynchronizer中的state都是被定義為volatile來保證記憶體可見性
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/73389.html
標籤:Java
上一篇:阿里的秒殺系統是怎么設計的?
下一篇:php鏈式操作的實作
