volatile
參考:
Java併發:volatile內存可見性和指令重排
你真的瞭解volatile嗎,關於volatile的那些事
java中volatile關鍵字的含義
volatile 作用
- 保證內存可見性
- 防止指令重排
- 不能解決原子性
volatile 理解
java中多線程共享的變量存儲在主內存中,每個線程都有自己的工作內存,工作內存保存了主內存的副本,線程要操作共享變量,實際操作的是線程工作內存的副本,操作完畢後再同步寫入主內存,各個線程線程只能訪問自己的工作內存,不可以訪問其它線程的工作內存。
java中線程工作內存跟主內存的交互
- lock:將主內存中的變量鎖定,爲一個線程所獨佔
- unclock:將lock加的鎖定解除,此時其它的線程可以有機會訪問此變量
- read:將主內存中的變量值讀到工作內存當中
- load:將read讀取的值保存到工作內存中的變量副本中。
- use:將值傳遞給線程的代碼執行引擎(多次)
- assign:將執行引擎處理返回的值重新賦值給變量副本(多次)
- store:將變量副本的值存儲到主內存中。
- write:將store存儲的值寫入到主內存的共享變量當中。
可見性:保證線程使用共享變量時每次都去主內存獲取最新的,保證了read-load的一致性
原子性:保證線程在read-load-use-assign-store-write共享變量過程中,其它線程不能對共享變量進行修改
共享變量使用volatile修飾後,保證線程每次訪問共享變量都去主內存獲取,保證每次獲取到的是主內存中最新的值,即保證了read-load是最新的,這樣就實現了可見性,但是在後續的use-assign-store-write過程中,其它線程可能會對共享變量進行操作更改,這樣無法保證原子性
代碼解讀可見性
如果不使用volatile修飾共享變量,線程只會在第一次使用共享變量的時候去主內存加載建立副本,這樣子線程永遠不會停止
使用volatile修飾修飾共享變量,在while循環的判斷running值的時候,每次都去主內存獲取最新的值,當主線程將running設置爲false的時候,停止子線程,在while循環中使用了count變量,如果只將count用volatile修飾,也能停止子線程,由此可見,線程去主內存讀取共享變量的時候,會把所有用到的共享變量都在工作內存建立副本
public class Task implements Runnable{
//將count用volatile修飾,保證每次去主存讀取count值,
//讀取的同時會將running也從主存讀取,不管running是否用volatile修飾
private volatile int count = 0;
private boolean running = true;
@Override
public void run() {
while(running){
//
count++;
}
System.out.println("子線程"+Thread.currentThread().getName()+"停止");
}
public static void main(String[] args) throws InterruptedException {
Task task = new Task();
//啓動子線程
new Thread(task).start();
Thread.currentThread().sleep(3000);
task.setRunning(false);
System.out.println("主線程停止");
}
public void setRunning(boolean running) {
this.running = running;
}
public int getCount() {
return count;
}
}
代碼解讀無法實現原子性
下面這段程序執行完畢後無法保證count的數量最終爲1000,這是因爲volatile只能保證使用count的時候去主內存讀取到最新的值,但是在對count進行+1操作的時候,其它線程可能會對count進行修改+1然後寫會主內存,造成最後的結果不是1000,如果要保證1000,還是要對整個read到write回主內存保證一致性,這就需要使用synchronized或者lock去實現了。
public class Counter {
//使用volatile修飾共享變量
public volatile static int count = 0;
public static void inc() {
// 這裏延遲1毫秒,使得結果明顯
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
//無法保證是1000
count++;
}
public static void main(String[] args) {
// 同時啓動1000個線程,去進行i++計算
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Counter.inc();
}
}).start();
}
// 無法保證count值爲1000
System.out.println("運行結果:Counter.count=" + Counter.count);
}
}
volatile修飾避免代碼重排
指令重排序是JVM爲了優化指令,提高程序運行效率,在不影響單線程程序執行結果的前提下,儘可能地提高並行度。編譯器、處理器也遵循這樣一個目標。注意是單線程。多線程的情況下指令重排序就會給程序員帶來問題。
內存屏障:在使用volatile修飾的變量前後插入一個內存柵欄,告訴JVM該條指令不能跟前後語句進行重排。
指令重排在多線程操作的時候,如果變量沒有使用volatile修飾,可能會出現問題
//線程1初始化User User user; user = new User(); //線程2讀取user if(user!=null){ user.getName(); }
具體來看User user = new User的語義:
1:分配對象的內存空間
2:初始化對線
3:設置user指向剛分配的內存地址操作2依賴於操作1,但是操作3並不依賴於操作2,所以JVM是可以針對它們進行指令的優化重排序的,優化後變爲 1->3->2
這些線程1在執行完第3步而還沒來得及執行完第2步的時候,如果內存刷新到了主存,那麼線程2將得到一個未初始化完成的對象。
//在線程A中: context = loadContext(); inited = true; //在線程B中: while(!inited ){ //根據線程A中對inited變量的修改決定是否使用context變量 sleep(100); } doSomethingwithconfig(context); //假設線程A中發生了指令重排序: inited = true; context = loadContext(); //那麼B中很可能就會拿到一個尚未初始化或尚未初始化完成的context,從而引發程序錯誤。
volatile關鍵字通過提供“內存屏障”的方式來防止指令被重排序,爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。懶漢式單例模式就是使用volatile防止創建多個實例對象
總結
- volatile無法實現原子性,只能實現可見性
- 當要訪問的變量已在synchronized代碼塊中,或者爲常量時,沒必要使用volatile。
- 由於使用volatile屏蔽掉了JVM中必要的代碼優化,所以在效率上比較低,因此一定在必要時才使用此關鍵字。
- 在需要同步的時候,第一選擇應該是synchronized關鍵字,這是最安全的方式,嘗試其他任何方式都是有風險的。尤其在、jdK1.5之後,對synchronized同步機制做了很多優化,如:自適應的自旋鎖、鎖粗化、鎖消除、輕量級鎖等,使得它的性能明顯有了很大的提升。
- 當且僅當滿足以下所有條件時,才應該使用volatile變量:
- 對變量的寫入操作不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。
- 該變量沒有包含在具有其他變量的不變式中,防止影響其他變量??
- 防止代碼重排