摘要:所以多線程條件下使用關鍵字的前提是對變量的寫操作不依賴于變量的當前值,而賦值操作很明顯滿足這一前提。在多線程環境下,正確使用關鍵字可以比直接使用更加高效而且代碼簡潔,但是使用關鍵字也更容易出錯。
volatile 作為 Java 語言的一個關鍵字,被看作是輕量級的 synchronized(鎖)。雖然 volatile 只具有synchronized 的部分功能,但是一般使用 volatile 會比使用 synchronized 更有效率。在編寫多線程程序的時候,volatile 修飾的變量能夠:
保證內存 可見性
防止指令 重排序
保證對 64 位變量 讀寫的原子性
一. 保證內存可見性JVM 中,每個線程都擁有自己棧內存,用來保存當前線程運行過程中的變量數據;然后多個線程之間共享堆內存(也稱主存)。當線程需要訪問一個變量時,首先將其從堆內存中復制到自己的棧內存作為副本,然后線程每次對該變量的操作,都將是對棧中的副本進行操作 —— 在某些時刻(比如退出 synchronized 塊或線程結束),線程會將棧中副本的值寫回到主存,此時主存中的變量才會被替換為副本的值。這樣自然就帶來一個問題,即如果兩個線程共享一個變量,線程A 改變了變量的值,但是 線程B 可能無法立即發現。比如下面這個經典的例子:
public class ConcurrentTest { private static boolean running = true; public static class AnotherThread extends Thread { @Override public void run() { System.out.println("AnotherThread is running"); while (running) { } System.out.println("AnotherThread is stoped"); } } public static void main(String[] args) throws Exception { new AnotherThread ().start(); Thread.sleep(1000); running = false; // 1 秒之后想停止 AnotherThread } }
上面這段代碼一般情況下都會死鎖,就是因為在 main 方法(主線程)中對 running 做的修改,并不能立馬對 AnotherThread 可見。
如果將 running 加上修飾符 volatile,那么便可以獲取實際希望的結果,因為此時主線程中設置 running 為 false 之后,AnotherThread 可以立馬發現 running 的值發生了改變:
二. 防止指令重排序對于 volatile 修飾的變量,JVM 可以保證:
每次對該變量的寫操作,都將立即同步到主存;
每次對該變量的讀操作,都將從主存讀取,而不是線程棧
如果一個操作不是原子操作,那么 JVM 便可能會對該操作涉及的指令進行 重排序。重排序即在不改變程序語義的前提下,通過調整指令的執行順序,盡可能達到提高運行效率的目的。
對于單例模式,為了達到延時初始化,并且可以在多線程環境下使用,我們可以直接使用 synchronized 關鍵字:
public class Singleton { public static Singleton instance = null; private Singleton() { } public synchronized static Singleton getSingleton() { if (instance == null) { instance = new Singleton(); } return instance; } }
這樣做的缺陷也很明顯,那就是 instance 初始化完畢之后,以后每次獲取 instance 仍然需要進行加鎖操作,是個很大的效率浪費。
于是出現了一種經典寫法叫 “雙重檢測鎖”:
public class Singleton { public static Singleton instance = null; private Singleton() { } public static Singleton getSingleton() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
但是這樣的寫法同樣會存在問題,因為 instance = new Singleton() 并非原子操作,其大概可以等同于執行:
分配一個 Singleton 對應的內存
初始化這個 Singleton 對應的內存
將 instance 指向對應的內存的地址
其中,2 依賴于 1,但是 3 并不依賴于 2 —— 所以,存在 JVM 將這三條語句重排序為 1->3->2 的可能,即變為:
a. 分配一個 Singleton 對應的內存
b. 將 instance 指向對應的內存的地址
c. 初始化這個 Singleton 對應的內存
此時如果 線程A 執行完 b,那么此時的 instance 指向的內存并不為 null,然而這塊內存卻還沒有被初始化。當 線程B 此時判斷第一個 if (instance == null) 時發現 instance 并不為 null,便會將此時的 instance 返回 —— 但 Singleton 的初始化可能并未完成,此時 線程B 使用 instance 便可能會出現錯誤。
在 JDK 1.5 之后,增強了 volatile 的語義,嚴格限制 JVM (編譯器、處理器)不能對 volatile 修飾的變量涉及的操作指令進行重排序。
所以為了避免對 instance 變量涉及的操作進行重排序,保證 “雙重檢測鎖” 的正確性,我們可以將 instance 使用 volatile 修飾:
public class Singleton { /* 使用 volatile 修飾 */ public static volatile Singleton instance = null; private Singleton() { } public static Singleton getSingleton() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }三. 保證對 64 位變量讀寫的原子性
JVM 可以保證對 32位 數據讀寫的原子性,但是對于 long 和 double 這樣 64位 的數據的讀寫,會將其分為 高32位 和 低32位 分兩次讀寫。所以對于long 或 double 的讀寫并不是原子性的,這樣在并發程序中共享 long 或 double 變量就可能會出現問題,于是 JVM 提供了 volatile 關鍵字來解決這個問題:
使用 volatile 修飾的 long 或 double 變量,JVM 可以保證對其讀寫的原子性。
但值得注意的是,此處的 “寫” 僅指對 64位 的變量進行直接賦值。而對于 i++ 這個語句,事實上涉及了 讀取-修改-寫入 三個操作:
讀取變量到棧中某個位置
對棧中該位置的值進行自增
將自增后的值寫回到變量對應的存儲位置
因此哪怕變量 i 使用 volatile 修飾,也并不能使涉及上面三個操作的 i++ 具有原子性。所以多線程條件下使用 volatile 關鍵字的前提是:對變量的寫操作不依賴于變量的當前值,而賦值操作很明顯滿足這一前提。
在多線程環境下,正確使用 volatile 關鍵字可以比直接使用 synchronized 更加高效而且代碼簡潔,但是使用 volatile 關鍵字也更容易出錯。所以,除非十分清楚 volatile 的使用場景,否則還是應該選擇更加具有保障性的 synchronized。
Brian Goetz 大大寫過一篇 “volatile 變量使用指南”,有興趣的讀者可以參閱:Java 理論與實踐: 正確使用 Volatile 變量
volatile 變量的底層實現原理,有興趣的讀者可以參閱:
http://www.infoq.com/cn/artic...
http://www.cnblogs.com/paddix...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/66780.html
摘要:三關鍵字能保證原子性嗎并發編程藝術這本書上說保證但是在自增操作非原子操作上不保證,多線程編程核心藝術這本書說不保證。多線程訪問關鍵字不會發生阻塞,而關鍵字可能會發生阻塞關鍵字能保證數據的可見性,但不能保證數據的原子性。 系列文章傳送門: Java多線程學習(一)Java多線程入門 Java多線程學習(二)synchronized關鍵字(1) java多線程學習(二)synchroniz...
時間:2017年07月09日星期日說明:本文部分內容均來自慕課網。@慕課網:http://www.imooc.com教學源碼:無學習源碼:https://github.com/zccodere/s... 第一章:課程簡介 1-1 課程簡介 課程目標和學習內容 共享變量在線程間的可見性 synchronized實現可見性 volatile實現可見性 指令重排序 as-if-seria...
摘要:的缺點頻繁刷新主內存中變量,可能會造成性能瓶頸不具備操作的原子性,不適合在對該變量的寫操作依賴于變量本身自己。 作者:畢來生微信:878799579 1. 什么是JUC? JUC全稱 java.util.concurrent 是在并發編程中很常用的實用工具類 2.Volatile關鍵字 1、如果一個變量被volatile關鍵字修飾,那么這個變量對所有線程都是可見的。2、如果某條線程修...
摘要:本文從內存模型角度,探討的實現原理。通過共享內存或者消息通知這兩種方法,可以實現通信或同步。基于共享內存的線程通信是隱式的,線程同步是顯式的而基于消息通知的線程通信是顯式的,線程同步是隱式的。鎖規則鎖的解鎖,于于鎖的獲取或加鎖。 一、前言 在java多線程編程中,volatile可以用來定義輕量級的共享變量,它比synchronized的使用成本更低,因為它不會引起線程上下文的切換和調...
摘要:今天給大家總結一下,面試中出鏡率很高的幾個多線程面試題,希望對大家學習和面試都能有所幫助。指令重排在單線程環境下不會出先問題,但是在多線程環境下會導致一個線程獲得還沒有初始化的實例。使用可以禁止的指令重排,保證在多線程環境下也能正常運行。 下面最近發的一些并發編程的文章匯總,通過閱讀這些文章大家再看大廠面試中的并發編程問題就沒有那么頭疼了。今天給大家總結一下,面試中出鏡率很高的幾個多線...
閱讀 2330·2021-09-30 09:47
閱讀 2949·2019-08-30 11:05
閱讀 2526·2019-08-29 17:20
閱讀 1912·2019-08-29 13:01
閱讀 1721·2019-08-26 13:39
閱讀 1221·2019-08-26 13:26
閱讀 3205·2019-08-23 18:40
閱讀 1810·2019-08-23 17:09