談?wù)勀銓?volatile 的理解?
你知道 volatile 底層的實(shí)現(xiàn)機(jī)制嗎滑燃?
volatile 變量和 atomic 變量有什么不同役听?
volatile 的使用場景,你能舉兩個(gè)例子嗎表窘?
文章收錄在 GitHub JavaKeeper 典予,包含 N 線互聯(lián)網(wǎng)開發(fā)必備技能兵器譜
之前算是比較詳細(xì)的介紹了 Java 內(nèi)存模型——JMM, JMM是圍繞著并發(fā)過程中如何處理可見性乐严、原子性和有序性這 3 個(gè) 特征建立起來的瘤袖,而 volatile 可以保證其中的兩個(gè)特性,下面具體探討下這個(gè)面試必問的關(guān)鍵字昂验。
1. 概念
volatile 是 Java 中的關(guān)鍵字捂敌,是一個(gè)變量修飾符,用來修飾會(huì)被不同線程訪問和修改的變量既琴。
2. Java 內(nèi)存模型 3 個(gè)特性
2.1 可見性
可見性是一種復(fù)雜的屬性占婉,因?yàn)榭梢娦灾械腻e(cuò)誤總是會(huì)違背我們的直覺。通常甫恩,我們無法確保執(zhí)行讀操作的線程能適時(shí)地看到其他線程寫入的值逆济,有時(shí)甚至是根本不可能的事情。為了確保多個(gè)線程之間對內(nèi)存寫入操作的可見性,必須使用同步機(jī)制奖慌。
可見性抛虫,是指線程之間的可見性,一個(gè)線程修改的狀態(tài)對另一個(gè)線程是可見的升薯。也就是一個(gè)線程修改的結(jié)果莱褒。另一個(gè)線程馬上就能看到。
在 Java 中 volatile涎劈、synchronized 和 final 都可以實(shí)現(xiàn)可見性广凸。
2.2 原子性
原子性指的是某個(gè)線程正在執(zhí)行某個(gè)操作時(shí),中間不可以被加塞或分割蛛枚,要么整體成功谅海,要么整體失敗。比如 a=0蹦浦;(a非long和double類型) 這個(gè)操作是不可分割的扭吁,那么我們說這個(gè)操作是原子操作。再比如:a++盲镶; 這個(gè)操作實(shí)際是a = a + 1侥袜;是可分割的,所以他不是一個(gè)原子操作溉贿。非原子操作都會(huì)存在線程安全問題枫吧,需要我們使用同步技術(shù)(sychronized)來讓它變成一個(gè)原子操作。一個(gè)操作是原子操作宇色,那么我們稱它具有原子性九杂。Java的 concurrent 包下提供了一些原子類,AtomicInteger宣蠕、AtomicLong例隆、AtomicReference等。
在 Java 中 synchronized 和在 lock抢蚀、unlock 中操作保證原子性镀层。
2.3 有序性
Java 語言提供了 volatile 和 synchronized 兩個(gè)關(guān)鍵字來保證線程之間操作的有序性,volatile 是因?yàn)槠浔旧戆敖怪噶钪嘏判颉钡恼Z義思币,synchronized 是由“一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對其進(jìn)行 lock 操作”這條規(guī)則獲得的鹿响,此規(guī)則決定了持有同一個(gè)對象鎖的兩個(gè)同步塊只能串行執(zhí)行。
3. volatile 是 Java 虛擬機(jī)提供的輕量級(jí)的同步機(jī)制
- 保證可見性
- 不保證原子性
- 禁止指令重排(保證有序性)
3.1 空說無憑谷饿,代碼驗(yàn)證
3.1.1 可見性驗(yàn)證
class MyData {
int number = 0;
public void add() {
this.number = number + 1;
}
}
// 啟動(dòng)兩個(gè)線程惶我,一個(gè)work線程,一個(gè)main線程博投,work線程修改number值后绸贡,查看main線程的number
private static void testVolatile() {
MyData myData = new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t come in");
try {
TimeUnit.SECONDS.sleep(2);
myData.add();
System.out.println(Thread.currentThread().getName()+"\t update number value :"+myData.number);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "workThread").start();
//第2個(gè)線程,main線程
while (myData.number == 0){
//main線程還在找0
}
System.out.println(Thread.currentThread().getName()+"\t mission is over");
System.out.println(Thread.currentThread().getName()+"\t mission is over,main get number is:"+myData.number);
}
}
運(yùn)行 testVolatile()
方法听怕,輸出如下捧挺,會(huì)發(fā)現(xiàn)在 main 線程死循環(huán),說明 main 線程的值一直是 0
workThread execute
workThread update number value :1
修改 volatile int number = 0
,尿瞭,在 number 前加關(guān)鍵字 volatile,重新運(yùn)行闽烙,main 線程獲取結(jié)果為 1
workThread execute
workThread update number value :1
main execute over,main get number is:1
3.1.2 不保證原子性驗(yàn)證
class MyData {
volatile int number = 0;
public void add() {
this.number = number + 1;
}
}
private static void testAtomic() throws InterruptedException {
MyData myData = new MyData();
for (int i = 0; i < 10; i++) {
new Thread(() ->{
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
}
},"addPlusThread:"+ i).start();
}
//等待上邊20個(gè)線程結(jié)束后(預(yù)計(jì)5秒肯定結(jié)束了)声搁,在main線程中獲取最后的number
TimeUnit.SECONDS.sleep(5);
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println("final value:"+myData.number);
}
運(yùn)行 testAtomic
發(fā)現(xiàn)最后的輸出值黑竞,并不一定是期望的值 10000,往往是比 10000 小的數(shù)值疏旨。
final value:9856
為什么會(huì)這樣呢很魂,因?yàn)?i++
在轉(zhuǎn)化為字節(jié)碼指令的時(shí)候是4條指令
-
getfield
獲取原始值 -
iconst_1
將值入棧 -
iadd
進(jìn)行加 1 操作 -
putfield
把iadd
后的操作寫回主內(nèi)存
這樣在運(yùn)行時(shí)候就會(huì)存在多線程競爭問題,可能會(huì)出現(xiàn)了丟失寫值的情況檐涝。
如何解決原子性問題呢遏匆?
加 synchronized
或者直接使用 Automic
原子類。
3.1.3 禁止指令重排驗(yàn)證
計(jì)算機(jī)在執(zhí)行程序時(shí)谁榜,為了提高性能幅聘,編譯器和處理器常常會(huì)對指令做重排,一般分為以下 3 種
處理器在進(jìn)行重排序時(shí)必須要考慮指令之間的數(shù)據(jù)依賴性窃植,我們叫做 as-if-serial
語義
單線程環(huán)境里確保程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果一致喊暖;但是多線程環(huán)境中線程交替執(zhí)行,由于編譯器優(yōu)化重排的存在撕瞧,兩個(gè)線程中使用的變量能否保證一致性是無法確定的,結(jié)果無法預(yù)測狞尔。
我們往往用下面的代碼驗(yàn)證 volatile 禁止指令重排丛版,如果多線程環(huán)境下,`最后的輸出結(jié)果不一定是我們想象到的 2偏序,這時(shí)就要把兩個(gè)變量都設(shè)置為 volatile页畦。
public class ReSortSeqDemo {
int a = 0;
boolean flag = false;
public void mehtod1(){
a = 1;
flag = true;
}
public void method2(){
if(flag){
a = a +1;
System.out.println("reorder value: "+a);
}
}
}
volatile
實(shí)現(xiàn)禁止指令重排優(yōu)化,從而避免了多線程環(huán)境下程序出現(xiàn)亂序執(zhí)行的現(xiàn)象研儒。
還有一個(gè)我們最常見的多線程環(huán)境中 DCL(double-checked locking)
版本的單例模式中豫缨,就是使用了 volatile 禁止指令重排的特性。
public class Singleton {
private static volatile Singleton instance;
private Singleton(){}
// DCL
public static Singleton getInstance(){
if(instance ==null){ //第一次檢查
synchronized (Singleton.class){
if(instance == null){ //第二次檢查
instance = new Singleton();
}
}
}
return instance;
}
}
因?yàn)橛兄噶钪嘏判虻拇嬖诙硕洌p端檢索機(jī)制也不一定是線程安全的好芭。
why ?
Because: instance = new Singleton();
初始化對象的過程其實(shí)并不是一個(gè)原子的操作,它會(huì)分為三部分執(zhí)行冲呢,
- 給 instance 分配內(nèi)存
- 調(diào)用 instance 的構(gòu)造函數(shù)來初始化對象
- 將 instance 對象指向分配的內(nèi)存空間(執(zhí)行完這步 instance 就為非 null 了)
步驟 2 和 3 不存在數(shù)據(jù)依賴關(guān)系舍败,如果虛擬機(jī)存在指令重排序優(yōu)化,則步驟 2和 3 的順序是無法確定的。如果A線程率先進(jìn)入同步代碼塊并先執(zhí)行了 3 而沒有執(zhí)行 2邻薯,此時(shí)因?yàn)?instance 已經(jīng)非 null裙戏。這時(shí)候線程 B 在第一次檢查的時(shí)候,會(huì)發(fā)現(xiàn) instance 已經(jīng)是 非null 了厕诡,就將其返回使用累榜,但是此時(shí) instance 實(shí)際上還未初始化,自然就會(huì)出錯(cuò)灵嫌。所以我們要限制實(shí)例對象的指令重排壹罚,用 volatile 修飾(JDK 5 之前使用了 volatile 的雙檢鎖是有問題的)。
4. 原理
volatile 可以保證線程可見性且提供了一定的有序性醒第,但是無法保證原子性渔嚷。在 JVM 底層是基于內(nèi)存屏障實(shí)現(xiàn)的。
- 當(dāng)對非 volatile 變量進(jìn)行讀寫的時(shí)候稠曼,每個(gè)線程先從內(nèi)存拷貝變量到 CPU 緩存中形病。如果計(jì)算機(jī)有多個(gè)CPU,每個(gè)線程可能在不同的 CPU 上被處理霞幅,這意味著每個(gè)線程可以拷貝到不同的 CPU cache 中
- 而聲明變量是 volatile 的漠吻,JVM 保證了每次讀變量都從內(nèi)存中讀,跳過 CPU cache 這一步司恳,所以就不會(huì)有可見性問題
- 對 volatile 變量進(jìn)行寫操作時(shí)途乃,會(huì)在寫操作后加一條 store 屏障指令,將工作內(nèi)存中的共享變量刷新回主內(nèi)存扔傅;
- 對 volatile 變量進(jìn)行讀操作時(shí)耍共,會(huì)在寫操作后加一條 load 屏障指令,從主內(nèi)存中讀取共享變量猎塞;
通過 hsdis 工具獲取 JIT 編譯器生成的匯編指令來看看對 volatile 進(jìn)行寫操作CPU會(huì)做什么事情试读,還是用上邊的單例模式,可以看到
(PS:具體的匯編指令對我這個(gè) Javaer 太南了荠耽,但是 JVM 字節(jié)碼我們可以認(rèn)識(shí)钩骇,putstatic
的含義是給一個(gè)靜態(tài)變量設(shè)置值,那這里的 putstatic instance
,而且是第 17 行代碼铝量,更加確定是給 instance 賦值了倘屹。果然像各種資料里說的,找到了 lock add1
據(jù)說還得翻閱慢叨。這里可以看下這兩篇 http://www.reibang.com/p/6ab7c3db13c3 纽匙、 https://www.cnblogs.com/xrq730/p/7048693.html )
有 volatile 修飾的共享變量進(jìn)行寫操作時(shí)會(huì)多出第二行匯編代碼,該句代碼的意思是對原值加零插爹,其中相加指令addl前有 lock 修飾哄辣。通過查IA-32架構(gòu)軟件開發(fā)者手冊可知请梢,lock前綴的指令在多核處理器下會(huì)引發(fā)兩件事情:
- 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存
- 這個(gè)寫回內(nèi)存的操作會(huì)引起在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效
正是 lock 實(shí)現(xiàn)了 volatile 的「防止指令重排」「內(nèi)存可見」的特性
5. 使用場景
您只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全力穗,必須同時(shí)滿足下面兩個(gè)條件:
- 對變量的寫操作不依賴于當(dāng)前值
- 該變量沒有包含在具有其他變量的不變式中
其實(shí)就是在需要保證原子性的場景毅弧,不要使用 volatile。
5. volatile 性能
volatile 的讀性能消耗與普通變量幾乎相同当窗,但是寫操作稍慢够坐,因?yàn)樗枰诒镜卮a中插入許多內(nèi)存屏障指令來保證處理器不發(fā)生亂序執(zhí)行。
引用《正確使用 volaitle 變量》一文中的話:
很難做出準(zhǔn)確崖面、全面的評(píng)價(jià)元咙,例如 “X 總是比 Y 快”,尤其是對 JVM 內(nèi)在的操作而言巫员。(例如庶香,某些情況下 JVM 也許能夠完全刪除鎖機(jī)制,這使得我們難以抽象地比較 volatile
和 synchronized
的開銷简识。)就是說赶掖,在目前大多數(shù)的處理器架構(gòu)上,volatile 讀操作開銷非常低 —— 幾乎和非 volatile 讀操作一樣七扰。而 volatile 寫操作的開銷要比非 volatile 寫操作多很多奢赂,因?yàn)橐WC可見性需要實(shí)現(xiàn)內(nèi)存界定(Memory Fence),即便如此颈走,volatile 的總開銷仍然要比鎖獲取低膳灶。
volatile 操作不會(huì)像鎖一樣造成阻塞,因此立由,在能夠安全使用 volatile 的情況下轧钓,volatile 可以提供一些優(yōu)于鎖的可伸縮特性。如果讀操作的次數(shù)要遠(yuǎn)遠(yuǎn)超過寫操作锐膜,與鎖相比聋迎,volatile 變量通常能夠減少同步的性能開銷。
參考
《深入理解Java虛擬機(jī)》
http://tutorials.jenkov.com/java-concurrency/java-memory-model.html
https://juejin.im/post/5dbfa0aa51882538ce1a4ebc
《正確使用 Volatile 變量》https://www.ibm.com/developerworks/cn/java/j-jtp06197.html