Volatile能夠說是咱們Java虛擬機給咱們提供的一個輕量級的同步機制,與Synchronized相似,可是卻沒有它那麼強大。關於Volatile最主要的特色呢就是它的三大特性:java
而要了解Volatile的話,咱們就須要有JMM的基礎,因此咱們要介紹JMM的相關知識。編程
JMM是Java內存模型的縮寫(Java Memory Model),是一種邏輯的東西,物理上不存在的。能夠說是一種概念或者約定。好比關於約定有如下的一些:安全
一、線程在解鎖前,必須把共享的變量馬上刷新回主存!多線程
二、線程在加鎖前,必須讀取主存中最新的值到工做內存(線程有本身的工做內存)中!併發
三、加鎖和解鎖是同一把鎖app
JMM呢咱們邏輯上能夠把它分爲主內存和工做內存。而兩個內存之間也是有進行交互的,就是一個變量如何從主內存傳輸到工做內存中,如何把修改後的變量從工做內存回到主內存。關於這些操做咱們主要是有八種:函數
lock(鎖定):做用於主內存的變量,一個變量在同一時間只能一個線程鎖定,該操做表示這條線成獨佔這個變量this
unlock(解鎖):做用於主內存的變量,表示這個變量的狀態由處於鎖定狀態被釋放,這樣其餘線程才能對該變量進行鎖定線程
read(讀取):做用於主內存變量,表示把一個主內存變量的值傳輸到線程的工做內存,以便隨後的load操做使用設計
load(載入):做用於線程的工做內存的變量,表示把read操做從主內存中讀取的變量的值放到工做內存的變量副本中(副本是相對於主內存的變量而言的)
use(使用):做用於線程的工做內存中的變量,表示把工做內存中的一個變量的值傳遞給執行引擎,每當虛擬機遇到一個須要使用變量的值的字節碼指令時就會執行該操做
assign(賦值):做用於線程的工做內存的變量,表示把執行引擎返回的結果賦值給工做內存中的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時就會執行該操做
store(存儲):做用於線程的工做內存中的變量,把工做內存中的一個變量的值傳遞給主內存,以便隨後的write操做使用
write(寫入):做用於主內存的變量,把store操做從工做內存中獲得的變量的值放入主內存的變量中
在使用上的流程就是以下順序,咱們能夠畫一個圖就更加清晰明瞭:
個人主內存有一個flag = false;經過線程A來修改成true。
可是須要注意的是,在使用這些指令的時候也是須要知足一些規則的:
- 不容許read和load、store和write操做之一單獨出現,即不容許一個變量從主內存讀取了但工做內存不接受,或者工做內存發起回寫了但主內存不接受的狀況出現。
- 不容許一個線程丟棄它最近的assign操做,即變量在工做內存中改變了以後必須把該變化同步回
主內存- 不容許一個線程無緣由地(沒有發生過任何assign操做)把數據從線程的工做內存同步回主內存
中。- 一個新的變量只能在主內存中「誕生」,不容許在工做內存中直接使用一個未被初始化(load或
assign)的變量,換句話說就是對一個變量實施use、store操做以前,必須先執行assign和load操做。- 一個變量在同一個時刻只容許一條線程對其進行lock操做,但lock操做能夠被同一條線程重複執
行屢次,屢次執行lock後,只有執行相同次數的unlock操做,變量纔會被解鎖。(也就是可重入鎖的概念)- 若是對一個變量執行lock操做,那將會清空工做內存中此變量的值,在執行引擎使用這個變量
前,須要從新執行load或assign操做以初始化變量的值。- 若是一個變量事先沒有被lock操做鎖定,那就不容許對它執行unlock操做,也不容許去unlock一個被其餘線程鎖定的變量。
- 對一個變量執行unlock操做以前,必須先把此變量同步回主內存中(執行store、write操做)。
這8種內存訪問操做以及上述規則限定明確地描述了在咱們Java程序中哪些內存訪問操做在併發下是安全的。可是這樣操做倒是極其繁瑣的,因此被簡化成了read,write,lock,unlock四種操做,但也只是語言上的簡化,實際模型的基礎設計並未簡化。
咱們在開頭提到了volatile的三大特性,而後要介紹就要先普及JMM的基礎,其實並非沒有道理的。關於JMM咱們也有三大特性(JMM保證),總結起來就是:
能夠發現,這與volatile是很相似的。下面一張圖能夠很好的理清之間的關係:
在上面基本的數據類型讀或寫,咱們看到了long,double除外,這涉及到了針對long和double型變量的特殊規則。
Java內存模型要求lock、unlock、read、load、assign、use、store、write這八種操做都具備原子性,可是對於64位的數據類型(long和double),在模型中特別定義了一條寬鬆的規定:容許虛擬機將沒有被volatile修飾的64位數據的讀寫操做劃分爲兩次32位的操做來進行,即容許虛擬機實現自行選擇是否要保證64位數據類型的load、store、read和write這四個操做的原子性,這就是所謂的「long和double的非原子性協定」(Non-Atomic Treatment of double and long Variables)。
關於三大特性,咱們這裏簡單介紹一下
原子性呢是指操做是一體的,要麼成功,要麼失敗,沒有第三種狀況。上圖咱們也說到Java中的基本數據類型的訪問,讀或寫都是具有原子性的。這裏咱們舉一個例子
int i = 5;
這裏咱們的賦值操做就是原子性,而一個比較經典的就是
int i = 0; i++;
這裏i++
就不是原子性的,咱們能夠看做它是先獲取了i的值,而後進行寫入值i = 1
的操做。咱們常見的數據類型的操做都是原子性的,可是若是應用場景須要一個更大範圍的原子性保證的話,Java內存模型提供了lock和unlock操做來知足這種需求。也提供了更方便快捷的synchronized關鍵字,一樣具有原子性。
可見性就是指當一個線程修改了共享變量的值時,其餘線程可以當即得知這個修改。好比當咱們沒有任何操做來處理兩個線程的時候:
int i = 0; //主線程 i++; //線程1 j = i; //線程2
咱們能夠發現線程1修改了i的值,可是沒有刷回主內存,線程2讀取了i的值,賦值給了j,咱們指望j就是1,可是由於雖然線程1修改了,沒有來得及複製到主內存中,線程2讀取後,j仍是0。這就是內存不可見性,同理咱們就能夠理解了可見性。
常見的咱們能夠用volatile關鍵字來修飾變量,達到了內存可見性。
Java內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存做爲傳遞媒介的方式來實現可見性的,不管是普通變量仍是volatile變量都是如此。普通變量與volatile變量的區別是,volatile的特殊規則保證了新值能當即同步到主內存,以及每次使用前當即從主內存刷新。所以咱們能夠說volatile保證了多線程操做時變量的可見性,而普通變量則不能保證這一點。
除了volatile之類,還有synchronized和final也能夠保證可見性。syschronized是由於JMM的規則限定對一個變量執行unlock操做以前,必須先把此變量同步回主內存中(執行store、write操做),而final關鍵字的可見性是指:
被final修飾的字段在構造器中一旦被初始化完成,而且構造器沒有把「this」的引用傳遞出去(this引用逃逸是一件很危險的事情,其餘線程有可能經過這個引用訪問到「初始化了一半」的對象),那麼在其餘線程中就能看見final字段的值。
一句話總結的話就是
若是在本線程內觀察,全部的操做都是有序的;若是在一個線程中觀察另外一個線程,全部的操做都是無序的。
問題來了,爲何在一個線程觀察另外一個線程的時候,操做都是無序的呢?
這就涉及到了指令重排:你寫的程序,計算機並非按照你寫的那樣去執行的,咱們能夠舉個例子來講明
int x = 1; // 1 int y = 2; // 2 x = x + 5; // 3 y = x * x; // 4
咱們指望的程序執行順序是1->2->3->4,咱們發現若是程序是2->1->3->4執行結果也是同樣的,或者1->3->2->4也行,可是若是按照1—>2->4->3之類的呢?就得不到咱們的指望結果,這就是指令重排致使的。
Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操做的有序性,volatile關鍵字自己就包含了禁止指令重排序的語義,而synchronized則是由「一個變量在同一個時刻只容許一條線程對其進行lock操做」這條規則得到的,這個規則決定了持有同一個鎖的兩個同步塊只能串行地進入。
可是若是全部的有序性都靠這兩個關鍵字來完成的話,那麼不少操做就會變得特別囉嗦,因此就有了一個Happens-Before原則。
- 程序次序規則(Program Order Rule):在一個線程內,按照控制流順序,書寫在前面的操做先行發生於書寫在後面的操做。注意,這裏說的是控制流順序而不是程序代碼順序,由於要考慮分支、循環等結構。
- 管程鎖定規則(Monitor Lock Rule):一個unlock操做先行發生於後面對同一個鎖的lock操做。這裏必須強調的是「同一個鎖」,而「後面」是指時間上的前後。
·volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操做先行發生於後面對這個變量的讀操做,這裏的「後面」一樣是指時間上的前後。- 線程啓動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的每個動做。
- 線程終止規則(Thread Termination Rule):線程中的全部操做都先行發生於對此線程的終止檢測,咱們能夠經過Thread::join()方法是否結束、Thread::isAlive()的返回值等手段檢測線程是否已經終止
執行。- 線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過Thread::interrupted()方法檢測到是否有中斷髮生。
- 對象終結規則(Finalizer Rule):一個對象的初始化完成(構造函數執行結束)先行發生於它的finalize()方法的開始。
- 傳遞性(Transitivity):若是操做A先行發生於操做B,操做B先行發生於操做C,那就能夠得出操做A先行發生於操做C的結論。
上面咱們介紹了JMM的特性的時候,也瞭解到了Synchronized是一個比較全能的同步塊,能夠保證不少的特性。
synchronized塊是Java提供的一種原子內置鎖,Java中的每一個對象均可以把它看成一個同步塊來使用,這些Java內置的使用者看不到的鎖被稱爲內部鎖,也叫作監視器鎖。程的執行代碼在進入synchronized代碼塊前會自動獲取內部鎖,這時候其餘線程訪問該同步代碼塊時會被阻塞掛起。拿到內部鎖的線程會在正常退出同步代碼塊或者拋出異後或者在同步塊內調用了該內置鎖資源的wai t系列方法時釋放該內置鎖。內置鎖是排它鎖,也就是當一個線程獲取這個鎖後,其餘線程必須等待該線程釋放鎖後才能獲取該鎖。
使用synchronized能夠解決共享變量內存可見性的問題。
synchronized塊的內存語義是把在synchronized塊內使用到的變量從線程的工做內存中清除,這樣在synchronized塊內使用到該變量時就不會從線程的工做內存中獲取,而是直接從主內存中獲取。退出synchronized塊的內存語義是把在synchronized塊內對共享變量的修改刷新到主內存。其實這也是加鎖和釋放鎖的語義,當獲取鎖後會清空鎖塊內本地內存中將會被用到的共享變量,在使用這些共享變量時從主內存進行加載,在釋放鎖時將本地內存中修改的共享變量刷新到主內存。
除能夠解決共享變量內存可見性問題外,synchronized常常被用來實現原子性操做。另外請注意,synchronized關鍵字會引發線程上下文切換並帶來線程調度開銷。
關於synchronized的使用,這裏能夠看看個人synchronized實現生產者消費者問題
其實關於volatile的特性,咱們在介紹JMM的時候都已經瞭解很大一部分了。可是咱們也說過volatile是不保證原子性的,這裏咱們仍是須要用代碼來展現的。
咱們開十個線程,每一個線程都對被volatile修飾的共享變量進行1000次自增操做。
public class Demo01 { private volatile int num = 0; private void increase(){ num++; } public static void main(String[] args) throws InterruptedException { final Demo01 demo01 = new Demo01(); for(int i = 0; i < 10; i++){ new Thread(()->{ for(int j = 0; j < 1000; j++){ demo01.increase(); } }).start(); } //保證在主線程結束以前,其餘線程執行完畢 TimeUnit.SECONDS.sleep(2); System.out.println(demo01.num); } }
咱們執行以後就能夠看,打印的數有時候並非咱們想要的10000,並且接近這個數。這充分體現了volatile並不能保證原子性。而要解決這個問題的話,就須要用加鎖或者用synchronized來修飾方法。
還有一種狀況比較經典的以下:
public class Demo02 { private int value; public int getValue() { return value; } public void setValue(int value) { this.value = value; } }
這裏在併發下是不安全的,由於沒有適當的同步措施。就會致使內存不可見,getValue
有時候取到的值的以前調用了setValue
,可是尚未刷回主內存。這裏咱們就能夠用到synchronized或者是volatile:
public class Demo02 { private int value; public synchronized int getValue() { return value; } public synchronized void setValue(int value) { this.value = value; } }
public class Demo02 { private volatile int value; public int getValue() { return value; } public void setValue(int value) { this.value = value; } }
在這裏這兩種方法是等價的,均可以解決內存可見性的問題。可是須要注意的是synchronized內置的鎖是獨佔鎖,這個時候同時只能有一個線程調用getValue
方法,其餘線程會被阻塞,同時也會存在線程上下文切換和線程從新調度的開銷,這也是使用鎖很差的地方。
到了這裏咱們也瞭解到了volatile關鍵字的兩個比較重要的特性,那麼咱們如何將兩個特性用在該用的地方呢?也就是下面最後的一個問題
《深刻理解Java虛擬機:Jvm高級特性與最佳實踐》
《Java併發編程之美》