volatile修飾的變量有2大特點
可以保證
- 可見性
- 有序性
為什么能實現(xiàn)這些功能,其底層原理就是內(nèi)存屏障
volatile的內(nèi)存語義
volatile關(guān)鍵字可以保證共享變量可見性,相較于普通的共享變量,使用volatile關(guān)鍵字可以保證共享變量的可見性
- 當線程讀取的是volatile關(guān)鍵字時,JMM會把該線程對應的工作內(nèi)存設置為無效,線程直接從主內(nèi)存中讀取該值到工作內(nèi)存中
- yield和sleep會導致線程讓出CPU,當線程再次調(diào)度回CPU,有可能會重新讀主存(JVM規(guī)范明確表示,yield和sleep方法不一定會強行刷新工作內(nèi)存,讀取主存,但是volatile會強行刷新內(nèi)存)
- 當線程寫的是volatile關(guān)鍵字變量,將當前修改后的變量值(工作內(nèi)存中)立即刷新到主內(nèi)存,且其他正在讀此變量的線程會等待(不是阻塞),直到寫回主內(nèi)存操作完成,保證讀的一定是刷新后的主內(nèi)存值
一句話,volatile修飾的變量在某個工作內(nèi)存修改后立刻會刷新會主內(nèi)存,并把其他工作內(nèi)存的該變量設置為無效。
內(nèi)存屏障
回憶volatile的作用
-
可見性
- 立即刷新回主內(nèi)存+失效處理。
-
有序性
- 禁止指令重排:存在數(shù)據(jù)依賴關(guān)系的禁止重排。
是什么
-
內(nèi)存屏障(也稱內(nèi)存柵欄,內(nèi)存柵障,屏障指令等,是一類同步屏障指令,是CPU或編譯器在對內(nèi)存隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執(zhí)行后才可以開始執(zhí)行此點之后的操作),避免代碼重排序。
-
內(nèi)存屏障其實就是一種JVM指令,Java內(nèi)存模型的重排規(guī)則會要求Java編譯器在生成JVM指令時插入特定的內(nèi)存屏障指令 ,通過這些內(nèi)存屏障指令,volatile實現(xiàn)了Java內(nèi)存模型中的可見性和有序性,但volatile無法保證原子性 。
-
內(nèi)存屏障之前的所有寫操作都要回寫到主內(nèi)存,
-
內(nèi)存屏障之后的所有讀操作都能獲得內(nèi)存屏障之前的所有寫操作的最新結(jié)果(實現(xiàn)了可見性)。
-
一句話:對一個 volatile 域的寫, happens-before 于任意后續(xù)對這個 volatile 域的讀,也叫寫后讀。
-
內(nèi)存屏障分類
- 上一章講解過happens-before先行發(fā)生原則,類似接口規(guī)范,落地?
- 落地靠什么?你憑什么可以保證?你管用嗎?
粗分兩種
寫屏障(Store Memory Barrier) :告訴處理器在寫屏障之前將所有存儲在緩存(store bufferes) 中的數(shù)據(jù)同步到主內(nèi)存。也就是說當看到Store屏障指令, 就必須把該指令之前所有寫入指令執(zhí)行完畢才能繼續(xù)往下執(zhí)行。
讀屏障(Load Memory Barrier) :處理器在讀屏障之后的讀操作, 都在讀屏障之后執(zhí)行。也就是說在Load屏障指令之后就能夠保證后面的讀取數(shù)據(jù)指令一定能夠讀取到最新的數(shù)據(jù)。
細分四種
什么叫保證有序性
- 禁止指令重排
- 通過內(nèi)存屏障禁止重排
-
重排序有可能影響程序的執(zhí)行和實現(xiàn), 因此, 我們有時候希望告訴JVM你別“自作聰明”給我重排序, 我這里不需要排序, 聽主人的。
-
對于編譯器的重排序, JMM會根據(jù)重排序的規(guī)則, 禁止特定類型的編譯器重排序。
-
對于處理器的重排序, Java編譯器在生成指令序列的適當位置, 插入內(nèi)存屏障指令, 來禁止特定類型的處理器排序。
volatile 的底層實現(xiàn)原理是內(nèi)存屏障,Memory Barrier(Memory Fence)
- 對 volatile 變量的寫指令后會加入寫屏障
- 寫屏障會確保指令重排序時,不會將寫屏障之前的代碼排在寫屏障之后
- 對 volatile 變量的讀指令前會加入讀屏障
- 讀屏障會確保指令重排序時,不會將讀屏障之后的代碼排在讀屏障之前
寫屏障(Store Memory Barrier) :告訴處理器在寫屏障之前將所有存儲在緩存(store bufferes) 中的數(shù)據(jù)同步到主內(nèi)存。也就是說當看到Store屏障指令, 就必須把該指令之前所有寫入指令執(zhí)行完畢才能繼續(xù)往下執(zhí)行。
讀屏障(Load Memory Barrier) :處理器在讀屏障之后的讀操作, 都在讀屏障之后執(zhí)行。也就是說在Load屏障指令之后就能夠保證后面的讀取數(shù)據(jù)指令一定能夠讀取到最新的數(shù)據(jù)。
happens-before之volatile變量規(guī)則
對一個 volatile 域的寫, happens-before 于任意后續(xù)對這個 volatile 域的讀,也叫寫后讀。
這里暫時先有個印象著就行
-
當?shù)谝粋€操作為volatile讀時,不論第二個操作是什么,都不能重排序。這個操作保證了volatile讀之后的操作不會被重排到volatile讀之前。
-
當?shù)诙€操作為volatile寫時,不論第一個操作是什么,都不能重排序。這個操作保證了volatile寫之前的操作不會被重排到volatile寫之后。
-
當?shù)谝粋€操作為volatile寫時,第二個操作為volatile讀時,不能重排。
JMM就將內(nèi)存屏障插入策略分為4種規(guī)則
讀屏障
- 在每個
volatile讀
操作的后面插入一個LoadLoad
屏障 - 在每個
volatile讀
操作的后面插入一個LoadStore
屏障
寫屏障
-
在每個
volatile寫
操作的前面插入一個StoreStore
屏障 -
在每個
volatile寫
操作的后面插入一個StoreLoad
屏障
volatile特性
如何保證可見性
volatile 的底層實現(xiàn)原理是內(nèi)存屏障,Memory Barrier(Memory Fence)
- 對 volatile 變量的寫指令后會加入寫屏障
- 保證在該屏障之前的,對共享變量的改動,都同步到主存當中
- 對 volatile 變量的讀指令前會加入讀屏障
- 保證在該屏障之后,對共享變量的讀取,加載的是主存中最新數(shù)據(jù)
寫屏障(Store Memory Barrier) :告訴處理器在寫屏障之前將所有存儲在緩存(store bufferes) 中的數(shù)據(jù)同步到主內(nèi)存。也就是說當看到Store屏障指令, 就必須把該指令之前所有寫入指令執(zhí)行完畢才能繼續(xù)往下執(zhí)行。
讀屏障(Load Memory Barrier) :處理器在讀屏障之后的讀操作, 都在讀屏障之后執(zhí)行。也就是說在Load屏障指令之后就能夠保證后面的讀取數(shù)據(jù)指令一定能夠讀取到最新的數(shù)據(jù)。
說明
- 保證不同線程對某個變量完成操作后結(jié)果及時可見,即該共享變量一旦改變所有線程立即可見。
例子
public class VolatileTest1 {
// static boolean flag = true;//不加volatile,沒有可見性
static volatile boolean flag = true;//加volatile,有可見性
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t come in");
while (flag){//默認flag是true,如果未被修改就一直循環(huán),下面那句話也打不出來
}
System.out.println(Thread.currentThread().getName()+"\t flag被修改為false,退出.....");
},"t1").start();
//暫停幾秒
TimeUnit.SECONDS.sleep(2);
flag=false;
System.out.println("main線程修改完成");
}
}
//沒有volatile時
//t1 come in
//main線程修改完成
//--------程序一直在跑(在循環(huán)里)
//有volatile時
//t1 come in
//main線程修改完成
//t1 flag被修改為false,退出.....
上述代碼原理解釋
- 線程t1中為何看不到被主線程main修改為false的flag的值?
問題可能:
-
主線程修改了flag之后沒有將其刷新到主內(nèi)存,所以t1線程看不到。
-
主線程將flag刷新到了主內(nèi)存,但是t1一直讀取的是自己工作內(nèi)存中flag的值,沒有去主內(nèi)存中更新獲取flag最新的值。
我們的訴求:
- 線程中修改了工作內(nèi)存中的副本之后,立即將其刷新到主內(nèi)存;
- 工作內(nèi)存中每次讀取共享變量時,都去主內(nèi)存中重新讀取,然后拷貝到工作內(nèi)存。
解決:
-
使用volatile修飾共享變量,就可以達到上面的效果,被volatile修改的變量有以下特點:
- 線程中讀取的時候,每次讀取都會去主內(nèi)存中讀取共享變量最新的值 ,然后將其復制到工作內(nèi)存
- 線程中修改了工作內(nèi)存中變量的副本,修改之后會立即刷新到主內(nèi)存
volatile變量的讀寫過程
Java內(nèi)存模型定義了8種每個線程工作內(nèi)存與物理主內(nèi)存之間的原子操作
- read(讀取)→load(加載)→use(使用)→assign(賦值)→store(存儲)→write(寫入)→lock(鎖定)→unlock(解鎖)
- read: 作用于主內(nèi)存,將變量的值從主內(nèi)存?zhèn)鬏數(shù)焦ぷ鲀?nèi)存,主內(nèi)存到工作內(nèi)存
- load: 作用于工作內(nèi)存,將read從主內(nèi)存?zhèn)鬏數(shù)淖兞恐捣湃牍ぷ鲀?nèi)存變量副本中,即數(shù)據(jù)加載
- use: 作用于工作內(nèi)存,將工作內(nèi)存變量副本的值傳遞給執(zhí)行引擎,每當JVM遇到需要該變量的字節(jié)碼指令時會執(zhí)行該操作
- assign: 作用于工作內(nèi)存,將從執(zhí)行引擎接收到的值賦值給工作內(nèi)存變量,每當JVM遇到一個給變量賦值字節(jié)碼指令時會執(zhí)行該操作
- store: 作用于工作內(nèi)存,將賦值完畢的工作變量的值寫回給主內(nèi)存
- write: 作用于主內(nèi)存,將store傳輸過來的變量值賦值給主內(nèi)存中的變量
由于上述6條只能保證單條指令的原子性,針對多條指令的組合性原子保證,沒有大面積加鎖,所以,JVM提供了另外兩個原子指令:
- lock: 作用于主內(nèi)存,將一個變量標記為一個線程獨占的狀態(tài),只是寫時候加鎖,就只是鎖了寫變量的過程。
- unlock: 作用于主內(nèi)存,把一個處于鎖定狀態(tài)的變量釋放,然后才能被其他線程占用
其中最核心的操作是在于我們的write操作,當我們從工作內(nèi)存寫到主內(nèi)存的時候,會進行l(wèi)ock加鎖操作,加鎖后會清空其他線程工作內(nèi)存變量的值,如果其他線程要使用該變量前必須重寫從主內(nèi)存加載值,當write完畢后,進行unlock進行解鎖 ,這樣就保證了可見性
- 這里的鎖只是鎖了些變量的過程,也就是只有寫完之后,其他線程才能到主內(nèi)存去讀數(shù)據(jù)
為何沒有原子性
- volatile變量的復合操作不具有原子性,比如number++
例子
-
synchronized
和volatile
代碼演示
class MyNumber{
//volatile int num=0;
int num=0;
public synchronized void add(){
num++;
}
}
public class VolatileNoAtomicDemo {
public static void main(String[] args) {
MyNumber myNumber = new MyNumber();
for (int i = 0; i <10; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
myNumber.add();
}
}).start();
}
//暫停幾秒鐘線程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName() + "\t" + myNumber.num);
}
}
//-------------volatile情況下
//main 941
//-----------synchronized請款下
//main 1000
讀取賦值一個普通變量的情況
當線程1對主內(nèi)存對象發(fā)起read操作到write操作第一套流程的時間里,線程2隨時都有可能對這個主內(nèi)存對象發(fā)起第二套操作
- 當線程1開始了volatile的讀寫流程的時候,線程2可以在其任何時候的流程的時候進行發(fā)起read操作,因為我們的volatile的鎖只是鎖了在wirte的寫流程
不保證原子性
- 從底層來說,i++或者number++(在執(zhí)行引擎操作時)其實是分了三步的:數(shù)據(jù)加載 、數(shù)據(jù)計算 、數(shù)據(jù)賦值 。而這三步非原子操作
- 對于volatile變量具備可見性 ,JVM只是保證從主內(nèi)存加載到線程工作內(nèi)存的值是最新的,也僅是數(shù)據(jù)加載時是最新的。
- 但是多線程環(huán)境下,“數(shù)據(jù)計算”和“數(shù)據(jù)賦值”操作可能多次出現(xiàn),若數(shù)據(jù)在加載之后,若主內(nèi)存volatile修飾變量發(fā)生修改之后,線程工作內(nèi)存中的操作將會作廢去讀主內(nèi)存最新值,操作出現(xiàn)寫丟失問題。即各線程私有內(nèi)存和主內(nèi)存公共內(nèi)存中變量不同步 ,進而導致數(shù)據(jù)不一致。由此可見volatile解決的是變量讀取時的可見性問題,但無法保證原子性,對于多線程修改主內(nèi)存共享變量的場景必須使用加鎖同步。
- 就比如這個i++操作,當你從主內(nèi)存加載了5進入,并且加載進了工作內(nèi)存,正當要進行++操作的時候,線程2進行了讀取操作,也是從主內(nèi)存讀取了5,因為線程1沒有進行write操作,所有主內(nèi)存還是最新的值,符合volatile的特性,然后線程進行了8個一套操作,然后變成6寫入主內(nèi)存,線程1的5就失效了,就需要進行重新從內(nèi)存讀取到6,但是這次++操作是丟失了,所有還是線程不安全的
結(jié)論
- volatile不適合參與到依賴當前值的運算,如i=i+1,i++之類的
- 那么依靠可見性的特點volatile可以用在哪些地方呢?通常volatile用作保存某個狀態(tài)的boolean值或or int值。 (一旦布爾值被改變迅速被看到,就可以做其他操作)
禁止指令重排
-
重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進行重新排序的一種手段,有時候會改變程序語句的先后順序
-
不存在數(shù)據(jù)依賴關(guān)系,可以重排序;
-
存在數(shù)據(jù)依賴關(guān)系 ,禁止重排序
- 數(shù)據(jù)依賴性 :若兩個操作訪問同一變量,且這兩個操作中有一個為寫操作,此時兩操作間就存在數(shù)據(jù)依賴性。
但重排后的指令絕對不能改變原有的串行語義!這點在并發(fā)設計中必須要重點考慮!
數(shù)據(jù)依賴的實例
- 不存在數(shù)據(jù)依賴關(guān)系,可以重排序 ===> 重排序OK 。
- 存在數(shù)據(jù)依賴關(guān)系,禁止重排序===> 重排序發(fā)生,會導致程序運行結(jié)果不同。
- 編譯器和處理器在重排序時,會遵守數(shù)據(jù)依賴性,不會改變存在依賴關(guān)系的兩個操作的執(zhí)行,但不同處理器和不同線程之間的數(shù)據(jù)性不會被編譯器和處理器考慮,其只會作用于單處理器和單線程環(huán)境
如何正確使用volatile(實際工作)
單一賦值可以,但是含有符合運算賦值不可以(比如i++)
-
下面這兩個單一賦值可以的
-
volatile int a = 10;
-
volatile boolean flag = false
-
狀態(tài)標志,判斷業(yè)務是否結(jié)束
//這個前面講過
public class UseVolatileDemo{
private volatile static boolean flag = true;
public static void main(String[] args){
new Thread(() -> {
while(flag) {
//do something......循環(huán)
}
},"t1").start();
//暫停幾秒鐘線程
try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
flag = false;
},"t2").start();
}
}
開銷較低的讀,寫鎖策略
當讀遠多于寫
- 最土的方法就是加兩個synchronized,但是讀用volatile,寫用synchronized可以提高性能
public class UseVolatileDemo{
//
// 使用:當讀遠多于寫,結(jié)合使用內(nèi)部鎖和 volatile 變量來減少同步的開銷
// 理由:利用volatile保證讀取操作的可見性;利用synchronized保證復合操作的原子性
public class Counter {
private volatile int value;
public int getValue(){
return value; //利用volatile保證讀取操作的可見性
}
public synchronized int increment(){
return value++; //利用synchronized保證復合操作的原子性
}
}
}
單例模式雙重鎖案例
public class SafeDoubleCheckSingleton
{
private static SafeDoubleCheckSingleton singleton; //-----這里沒加volatile
//私有化構(gòu)造方法
private SafeDoubleCheckSingleton(){
}
//雙重鎖設計
public static SafeDoubleCheckSingleton getInstance(){
if (singleton == null){
//1.多線程并發(fā)創(chuàng)建對象時,會通過加鎖保證只有一個線程能創(chuàng)建對象
synchronized (SafeDoubleCheckSingleton.class){
if (singleton == null){
//隱患:多線程環(huán)境下,由于重排序,該對象可能還未完成初始化就被其他線程讀取
singleton = new SafeDoubleCheckSingleton();
//實例化分為三步
//1.分配對象的內(nèi)存空間
//2.初始化對象
//3.設置對象指向分配的內(nèi)存地址
}
}
}
//2.對象創(chuàng)建完畢,執(zhí)行g(shù)etInstance()將不需要獲取鎖,直接返回創(chuàng)建對象
return singleton;
}
}
單線程情況下
- 單線程環(huán)境下(或者說正常情況下),在"問題代碼處",會執(zhí)行如下操作,保證能獲取到已完成初始化的實例
//三步
memory = allocate(); //1.分配對象的內(nèi)存空間
ctorInstance(memory); //2.初始化對象
instance = memory; //3.設置對象指向分配的內(nèi)存地址
多線程情況下(由于指令重排序)
隱患:多線程環(huán)境下,在"問題代碼處",會執(zhí)行如下操作,由于重排序?qū)е?,3亂序,后果就是其他線程得到的是null而不是完成初始化的對象 。(沒初始化完的就是null)
正常情況
//三步
memory = allocate(); //1.分配對象的內(nèi)存空間
ctorInstance(memory); //2.初始化對象
instance = memory; //3.設置對象指向分配的內(nèi)存地址
非正常情況
//三步
memory = allocate(); //1.分配對象的內(nèi)存空間
instance = memory; //3.設置對象指向分配的內(nèi)存地址---這里指令重排了,但是對象還沒有初始化
ctorInstance(memory); //2.初始化對象
解決
- 加volatile修飾
public class SafeDoubleCheckSingleton
{
//通過volatile聲明,實現(xiàn)線程安全的延遲初始化。
private volatile static SafeDoubleCheckSingleton singleton;
//私有化構(gòu)造方法
private SafeDoubleCheckSingleton(){
}
//雙重鎖設計
public static SafeDoubleCheckSingleton getInstance(){
if (singleton == null){
//1.多線程并發(fā)創(chuàng)建對象時,會通過加鎖保證只有一個線程能創(chuàng)建對象
synchronized (SafeDoubleCheckSingleton.class){
if (singleton == null){
//隱患:多線程環(huán)境下,由于重排序,該對象可能還未完成初始化就被其他線程讀取
//原理:利用volatile,禁止 "初始化對象"(2) 和 "設置singleton指向內(nèi)存空間"(3) 的重排序
singleton = new SafeDoubleCheckSingleton();
}
}
}
//2.對象創(chuàng)建完畢,執(zhí)行g(shù)etInstance()將不需要獲取鎖,直接返回創(chuàng)建對象
return singleton;
}
}
實例化singleton分多步執(zhí)行(分配內(nèi)存空間、初始化對象、將對象指向分配的內(nèi)存空間),某些編譯器為了性能原因,會將第二步和第三步進行重排序(java分配內(nèi)存空間、將對象指向分配的內(nèi)存空間、初始化對象)。這樣,某個線程可能會獲得一個未完全初始化的實例。
面試回答
valatile可見性
- 寫操作的話,這個變量的最新值會立即刷新到主內(nèi)存中
- 讀操作的話,總是能夠讀取這個變量的最新值,也就是這個變量最后被修改的值
- 某個線程收到通知,去讀取volatile修飾的變量的值的時候,線程私有工作內(nèi)存的數(shù)據(jù)失效,需要重新回到主內(nèi)存區(qū)讀取最新的數(shù)據(jù)
內(nèi)存屏障是什么?
內(nèi)存屏障是一種屏障指令,它使得CPU或編譯器對屏障指令的前和后所發(fā)出的內(nèi)存操作執(zhí)行一個排序的約束。也叫內(nèi)存柵欄或柵欄指令文章來源:http://www.zghlxwxcb.cn/news/detail-486716.html
內(nèi)存屏障能干嘛?
- 阻止屏障兩邊的指令重排序
- 寫數(shù)據(jù)時假如屏障,強制將線程私有工作內(nèi)存的數(shù)據(jù)刷回主物理內(nèi)存
- 讀數(shù)據(jù)時加入屏障,線程私有工作內(nèi)存的數(shù)據(jù)失效,重新到主物理內(nèi)存中獲取最新數(shù)據(jù)
內(nèi)存屏障的四大指令
- 在每一個volatile寫操作前面插入一個StoreStore屏障
- 普通寫和volatile寫禁止重排
- 在每一個volatile寫操作后面插入一個StoreLoad屏障
- volatile寫和普通讀禁止重排
- 在每一個volatile讀操作后面插入一個LoadLoad屏障
- volatile讀和普通讀禁止重排
- 在每一個volatile讀操作后面插入一個LoadStore屏障
- volatile讀和volatile寫進行重排
3句話總結(jié)文章來源地址http://www.zghlxwxcb.cn/news/detail-486716.html
- volatile寫之前的的操作,都禁止重排到volatile之后
- volatile讀之后的操作,都禁止重排到volatile之前
- volatile寫之后volatile讀,禁止重排序
到了這里,關(guān)于第六章volatile詳解的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!