1. 程式人生 > >Volatile關鍵字以及java併發和鎖的相關知識

Volatile關鍵字以及java併發和鎖的相關知識

volatile關鍵字可以說是Java虛擬機器提供的最輕量級的同步機制,但是它並不容易完全被正確、完整地理解,以至於許多程式設計師都習慣不去使用它,遇到需要處理多執行緒資料競爭問題的時候一律使用synchronized來進行同步。
本文我們來解釋一下Volatile關鍵字修飾變數的意義。
首先我們先簡述一下記憶體模型的相關概念:
1.快取記憶體:解決CPU執行速度和記憶體讀寫速度不匹配的問題。
也就是,當程式在執行過程中,會將運算需要的資料從主存複製一份到CPU的快取記憶體當中,那麼CPU進行計算時就可以直接從它的快取記憶體讀取資料和向其中寫入資料,當運算結束之後,再將快取記憶體中的資料重新整理到主存當中。
上述過程對於單執行緒自然是沒有問題的,但是對於多執行緒來說,每一個執行緒都有一個快取記憶體,就會出現讀取共享資料不一致的問題。
 為了解決快取不一致性問題,通常來說有以下2種解決方法:
  1)通過在匯流排加LOCK#鎖的方式
  2)通過快取一致性協議
  這2種方式都是硬體層面上提供的方式。
  
  第一種方式是早期CPU解決辦法,但是由於CPU和其他部件(例如記憶體)通訊都是通過匯流排來進行的總線上加了Lock之後,會阻塞其他CPU對其他部件的訪問,也就是說這樣只能有一個CPU使用某個變數的記憶體,導致效率低下,所以出現了快取一致性協議。
  最出名的就是Intel 的MESI協議,MESI協議保證了每個快取中使用的共享變數的副本是一致的。它核心的思想是:當CPU寫資料時,如果發現操作的變數是共享變數,即在其他CPU中也存在該變數的副本,會發出訊號通知其他CPU將該變數的快取行置為無效狀態

,因此當其他CPU需要讀取這個變數時,發現自己快取中快取該變數的快取行是無效的,那麼它就會從記憶體重新讀取
在這裡插入圖片描述
然後我們來學習一下併發程式設計中的三個概念
原子性:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行
可見性:可見性是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。
有序性:即程式執行的順序按照程式碼的先後順序執行。
接下來我們來看一下java中的記憶體模型
為了獲得較好的執行效能,Java記憶體模型並沒有限制執行引擎使用處理器的暫存器或者快取記憶體來提升指令執行速度,也沒有限制編譯器對指令進行重排序。也就是說,在java記憶體模型中,也會存在快取一致性問題和指令重排序的問題。
  Java記憶體模型規定所有的變數都是存在主存當中(類似於前面說的實體記憶體),每個執行緒都有自己的工作記憶體(類似於前面的快取記憶體)。執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接對主存進行操作。並且每個執行緒不能訪問其他執行緒的工作記憶體。
  在這裡插入圖片描述

 原子性:在Java中,對基本資料型別的變數的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要麼執行,要麼不執行。
 注意區分基本型別的變數的讀取和賦值:i=20;是原子操作。I++不是原子操作:包含了獲取i,給i+1,重新賦值三個步驟。
 可見性:Java提供了volatile關鍵字來保證可見性:當一個共享變數被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他執行緒需要讀取時,它會去記憶體中讀取新值。
 當然,Lock和Synchronized都可以實現可見性。synchronized和Lock能保證同一時刻只有一個執行緒獲取鎖然後執行同步程式碼,並且在釋放鎖之前會將對變數的修改重新整理到主存當中。
有序性:

在Java記憶體模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單執行緒程式的執行,卻會影響到多執行緒併發執行的正確性。
  在Java裡面,可以通過volatile關鍵字來保證一定的“有序性”(具體原理在下一節講述)。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個執行緒執行同步程式碼,相當於是讓執行緒順序執行同步程式碼,自然就保證了有序性。
瞭解了上述知識之後,我們來詳細介紹一下volatile關鍵字:

1.volatile關鍵字的兩層語義
  一旦一個共享變數(類的成員變數、類的靜態成員變數)被volatile修飾之後,那麼就具備了兩層語義:
  1)保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。
  2)禁止進行指令重排序,保證了有序性。
  volatile關鍵字禁止指令重排序有兩層意思:
  1)當程式執行到volatile變數的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;
  2)在進行指令優化時,不能將在對volatile變數訪問的語句放在其後面執行,也不能把volatile變數後面的語句放到其前面執行。

2.volatile不能實現原子性。
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作類,即對基本資料型別的 自增(加1操作),自減(減1操作)、以及加法操作(加一個數),減法操作(減一個數)進行了封裝,保證這些操作是原子性操作。atomic是利用CAS來實現原子性操作的(Compare And Swap),CAS實際上是利用處理器提供的CMPXCHG指令實現的,而處理器執行CMPXCHG指令是一個原子性操作。

然後我們來學習一下volatile實現的原理和機制
下面這段話摘自《深入理解Java虛擬機器》:
  “觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編程式碼發現,加入volatile關鍵字時,會多出一個lock字首指令
  lock字首指令實際上相當於一個記憶體屏障(也成記憶體柵欄),記憶體屏障會提供3個功能:
  1)它確保指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成;
  2)它會強制將對快取的修改操作立即寫入主存;
  3)如果是寫操作,它會導致其他CPU中對應的快取行無效。

volatile使用場景:
使用volatile必須具備以下2個條件:
  1)對變數的寫操作不依賴於當前值
  2)該變數沒有包含在具有其他變數的不變式中
  實際上,這些條件表明,可以被寫入 volatile 變數的這些有效值獨立於任何程式的狀態,包括變數的當前狀態。

重排序
上面的有序性提到了重排序,這裡稍微介紹下重排序的基本內容。

1.什麼是重排序?
重排序是指編譯器和處理器為了優化程式效能而對指令序列進行重新排序的一種手段。
2.重排序有哪些?
編譯器優化的重排序。編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序。
指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
記憶體系統的重排序。由於處理器使用快取和讀/寫緩衝區,這使得載入和儲存操作看上去可能是在亂序執行。
3.為什麼要重排序?
為了提高效能。
4.重排序會導致不正確的結果嗎?
重排序保證在單執行緒下不會改變執行結果,但在多執行緒下可能會改變執行結果。
5.怎麼禁止重排序?
可以通過插入記憶體屏障指令來禁止特定型別的處理器重排序。例如本文將提到的volatile關鍵字就有這種功能。

先行發生原則
Java語言中有一個“先行發生”(happens-before)的原則。這個原則非常重要,它是判斷資料是否存在競爭、執行緒是否安全的主要依據,依靠這個原則,我們可以通過幾條規則解決併發環境下兩個操作之間是否可能存在衝突的所有問題。

現在就來看看“先行發生”原則指的是什麼。先行發生是Java記憶體模型中定義的兩項操作之間的偏序關係,如果說操作A先行發生於操作B,其實就是說在發生操作B之前,操作A產生的影響能被操作B觀察到,“影響”包括修改了記憶體中共享變數的值、傳送了訊息、呼叫了方法等。

滿足了下面規則的情況就是先行發生原則
程式次序規則(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的結論。

如果兩個操作之間的關係不在上述規則中,並且無法從上述規則推匯出來的話,它們就沒有順序性保障,虛擬機器可以對它們隨意地進行重排序。
時間先後順序與先行發生原則之間基本沒有太大的關係,所以我們衡量併發安全問題的時候不要受到時間順序的干擾,一切必須以先行發生原則為準。

部分轉自:https://blog.csdn.net/v123411739/article/details/79438066