Java中Volatile關鍵字詳解
一饭豹、基本概念
先補充一下概念:Java 內(nèi)存模型中的可見性啦逆、原子性和有序性臀玄。
可見性:
可見性是一種復雜的屬性,因為可見性中的錯誤總是會違背我們的直覺况木。通常垒拢,我們無法確保執(zhí)行讀操作的線程能適時地看到其他線程寫入的值旬迹,有時甚至是根本不可能的事情。為了確保多個線程之間對內(nèi)存寫入操作的可見性求类,必須使用同步機制奔垦。
可見性,是指線程之間的可見性尸疆,一個線程修改的狀態(tài)對另一個線程是可見的椿猎。也就是一個線程修改的結(jié)果。另一個線程馬上就能看到寿弱。比如:用volatile修飾的變量鸵贬,就會具有可見性。volatile修飾的變量不允許線程內(nèi)部緩存和重排序脖捻,即直接修改內(nèi)存阔逼。所以對其他線程是可見的。但是這里需要注意一個問題地沮,volatile只能讓被他修飾內(nèi)容具有可見性嗜浮,但不能保證它具有原子性。比如 volatile int a = 0摩疑;之后有一個操作 a++危融;這個變量a具有可見性,但是a++ 依然是一個非原子操作雷袋,也就是這個操作同樣存在線程安全問題吉殃。
在 Java 中 volatile、synchronized 和 final 實現(xiàn)可見性楷怒。
原子性:
原子是世界上的最小單位蛋勺,具有不可分割性。比如 a=0鸠删;(a非long和double類型) 這個操作是不可分割的抱完,那么我們說這個操作時原子操作。再比如:a++刃泡; 這個操作實際是a = a + 1巧娱;是可分割的,所以他不是一個原子操作烘贴。非原子操作都會存在線程安全問題禁添,需要我們使用同步技術(sychronized)來讓它變成一個原子操作。一個操作是原子操作桨踪,那么我們稱它具有原子性老翘。java的concurrent包下提供了一些原子類,我們可以通過閱讀API來了解這些原子類的用法。比如:AtomicInteger酪捡、AtomicLong叁征、AtomicReference等。
在 Java 中 synchronized 和在 lock逛薇、unlock 中操作保證原子性捺疼。
有序性:
Java 語言提供了 volatile 和 synchronized 兩個關鍵字來保證線程之間操作的有序性,volatile 是因為其本身包含“禁止指令重排序”的語義永罚,synchronized 是由“一個變量在同一個時刻只允許一條線程對其進行 lock 操作”這條規(guī)則獲得的啤呼,此規(guī)則決定了持有同一個對象鎖的兩個同步塊只能串行執(zhí)行。
下面內(nèi)容摘錄自《Java Concurrency in Practice》:
下面一段代碼在多線程環(huán)境下呢袱,將存在問題官扣。
+ View code
/**
* @author zhengbinMac
*/
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
@Override
public void run() {
while(!ready) {
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
NoVisibility可能會持續(xù)循環(huán)下去,因為讀線程可能永遠都看不到ready的值羞福。甚至NoVisibility可能會輸出0惕蹄,因為讀線程可能看到了寫入ready的值,但卻沒有看到之后寫入number的值治专,這種現(xiàn)象被稱為“重排序”卖陵。只要在某個線程中無法檢測到重排序情況(即使在其他線程中可以明顯地看到該線程中的重排序),那么就無法確保線程中的操作將按照程序中指定的順序來執(zhí)行张峰。當主線程首先寫入number泪蔫,然后在沒有同步的情況下寫入ready,那么讀線程看到的順序可能與寫入的順序完全相反喘批。
在沒有同步的情況下撩荣,編譯器、處理器以及運行時等都可能對操作的執(zhí)行順序進行一些意想不到的調(diào)整饶深。在缺乏足夠同步的多線程程序中餐曹,要想對內(nèi)存操作的執(zhí)行春旭進行判斷,無法得到正確的結(jié)論粥喜。
這個看上去像是一個失敗的設計凸主,但卻能使JVM充分地利用現(xiàn)代多核處理器的強大性能。例如额湘,在缺少同步的情況下,Java內(nèi)存模型允許編譯器對操作順序進行重排序旁舰,并將數(shù)值緩存在寄存器中锋华。此外,它還允許CPU對操作順序進行重排序箭窜,并將數(shù)值緩存在處理器特定的緩存中毯焕。
二、Volatile原理
Java語言提供了一種稍弱的同步機制,即volatile變量纳猫,用來確保將變量的更新操作通知到其他線程婆咸。當把變量聲明為volatile類型后,編譯器與運行時都會注意到這個變量是共享的芜辕,因此不會將該變量上的操作與其他內(nèi)存操作一起重排序尚骄。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值侵续。
在訪問volatile變量時不會執(zhí)行加鎖操作倔丈,因此也就不會使執(zhí)行線程阻塞,因此volatile變量是一種比sychronized關鍵字更輕量級的同步機制状蜗。
當對非 volatile 變量進行讀寫的時候需五,每個線程先從內(nèi)存拷貝變量到CPU緩存中。如果計算機有多個CPU轧坎,每個線程可能在不同的CPU上被處理宏邮,這意味著每個線程可以拷貝到不同的 CPU cache 中。
而聲明變量是 volatile 的缸血,JVM 保證了每次讀變量都從內(nèi)存中讀蜜氨,跳過 CPU cache 這一步。
當一個變量定義為 volatile 之后属百,將具備兩種特性:
1.保證此變量對所有的線程的可見性记劝,這里的“可見性”,如本文開頭所述族扰,當一個線程修改了這個變量的值厌丑,volatile 保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新渔呵。但普通變量做不到這點怒竿,普通變量的值在線程間傳遞均需要通過主內(nèi)存(詳見:Java內(nèi)存模型)來完成。
2.禁止指令重排序優(yōu)化扩氢。有volatile修飾的變量耕驰,賦值后多執(zhí)行了一個“l(fā)oad addl $0x0, (%esp)”操作,這個操作相當于一個內(nèi)存屏障(指令重排序時不能把后面的指令重排序到內(nèi)存屏障之前的位置)录豺,只有一個CPU訪問內(nèi)存時朦肘,并不需要內(nèi)存屏障;(什么是指令重排序:是指CPU采用了允許將多條指令不按程序規(guī)定的順序分開發(fā)送給各相應電路單元處理)双饥。
volatile 性能:
volatile 的讀性能消耗與普通變量幾乎相同媒抠,但是寫操作稍慢,因為它需要在本地代碼中插入許多內(nèi)存屏障指令來保證處理器不發(fā)生亂序執(zhí)行咏花。