java 鎖

1、 Java併發編程的三個概念

  原子性:一個或多個操做要麼所有執行成功要麼所有執行失敗;java

   可見性:當多個線程訪問同一個變量時,若是其中一個線程對其做了修改,其餘線程能當即獲取到最新的值;編程

   有序性:程序執行的順序按照代碼的前後順序執行(處理器可能會對指令進行重排序);緩存

 

2、單核CPU到多核CPU的變化 

  CPU愈來愈快,主存逐漸跟不上cpu的頻率,cpu須要等待主存浪費資源。因此cache的出現主要爲了解決cpu和內存之間頻率不匹配的問題。多線程

但cache也帶來了新的問題,併發處理的不一樣步(不過能夠經過總線鎖和緩存一致性來解決)。併發

  

3、重排序

  在執行程序時,爲了提升性能,編譯器和處理器經常會對指令作重排序。app

  重排序分類

    

    一、編譯器優化的重排序:編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序。jvm

    二、指令級並行的重排序:現代處理器採用了指令級並行技術(Instruction-Level Parallelism,ILP)來將多條指令重疊執行。性能

      若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序。優化

    三、內存系統的重排序:因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。以下圖:spa

    

  重排序的原則:as-if-serial語義

    as-if-serial語義的意思是:無論怎麼重排序(編譯器和處理器爲了提升並行度),單線程程序的執行結果不能被改變。

    編譯器、runtime和處理器都必須遵照as-if-serial語義。

    爲了遵照as-if-serial語義,編譯器和處理器不會對存在數據依賴關係的操做作重排序。數據依賴關係以下圖所示:

    
     as-if-serial語義只能保證單線程下,重排序引發的問題。在多線程狀況下,不存在數據依賴關係的重排序也會破壞程序的意圖。
    

    單線程狀況下,控制依賴關係的重排序,不影響最終結果。多線程狀況下,則可能會破壞程序的意圖。

 
    
 

  JMM禁止重排序的措施:

    一、對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是全部的編譯器重排序都要禁止)。

    二、對於處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定類型的內存屏障(Memory Barriers,Intel稱之爲MemoryFence)指令,

       經過內存屏障指令來禁止特定類型的處理器重排序。

  JMM的內存屏障插入策略:Load:加載(讀)、Store:保存(寫),屏障名稱就能夠看出讀寫的前後順序) 

    一、在每一個volatile寫操做前插入StroreStore屏障 
    二、在每一個volatile寫操做前插入StroreLoad屏障 
    三、在每一個volatile讀操做前插入LoadLoad屏障 
    四、在每一個volatile讀操做前插入LoadStore屏障

 

4、Volatile 

    volatile是輕量級的synchronized,它在多處理器開發中保證了共享變量的「可見性」。volatile不會引發上下文的切換和調度(禁止指令重排),執行開銷更小。  

  Volatile 的特性:

    可見性:對一個volatile變量的讀,老是能看到(任意線程)對這個volatile變量最後的寫入。

    原子性:對任意單個volatile變量的讀/寫具備原子性,但相似於volatile++這種複合操做不具備原子性。

  Volatile 的原理:

    1. 使用volitate修飾的變量在彙編階段,會多出一條lock前綴指令(ACC_VOLATILE)

    2. 它確保指令重排序時不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面;

      即在執行到內存屏障這句指令時,在它前面的操做已經所有完成

    3. 它會強制將對緩存的修改操做當即寫入主存

    4. 若是是寫操做,它會致使其餘CPU裏緩存了該內存地址的數據無效

  volatile寫-讀創建的happens-before關係:

    volatile的寫-讀與鎖的釋放-獲取有相同的內存效果。

    這裏A線程寫一個volatile變量後,B線程讀同一個volatile變量。

    A線程在寫volatile變量以前全部可見的共享變量,在B線程讀同一個volatile變量後,將當即變得對B線程可見。

  

  Volatile 寫-讀的內存語義:

    當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存。

    當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。

    注:關於volatile變量重排序,嚴格限制編譯器和處理器對volatile變量與普通變量的重排序,確保volatile的寫-讀和鎖的釋放-獲取具備相同的內存語義。

     

    

    JMM重排序分爲編譯器重排序和處理器重排序,JMM會限制這兩種類型的重排序類型來保證volatile的內存語義

    一、第二個操做是volatile寫時,第一個操做不論是什麼,都不能重排序 
    二、第一個操做是volatile讀時,第二個操做不論是什麼,都不能重排序 
    三、第一個操做是volatile是寫,第二個操做是volatile是讀,不能重排序

  volatile和synchronized的區別: 

    volatile本質是在告訴jvm當前變量在寄存器(工做內存)中的值是不肯定的,須要從主存中讀取;

    synchronized則是鎖定當前變量,只有當前線程能夠訪問該變量,其餘線程被阻塞住。

    volatile僅能使用在變量級別;synchronized則可使用在變量、方法、和類級別的

    volatile僅能實現變量的修改可見性,不能保證原子性;而synchronized則能夠保證變量的修改可見性和原子性

    volatile不會形成線程的阻塞;synchronized可能會形成線程的阻塞。

    volatile標記的變量不會被編譯器優化;synchronized標記的變量能夠被編譯器優化

 

5、Synchronized 鎖

    synchronized 是JVM實現的一種鎖,其中鎖的獲取和釋放分別是monitorenter 和 monitorexit 指令,該鎖在實現上分爲了偏向鎖、輕量級鎖和重量級鎖,

  其中偏向鎖在 java1.6 是默認開啓的,輕量級鎖在多線程競爭的狀況下會膨脹成重量級鎖,有關鎖的數據都保存在對象頭中。

    對於普通同步方法,鎖是當前實例對象;

    對於靜態同步方法,鎖是當前類Class對象;

    對於同步方法塊,鎖是Synchronized括號裏配置的對象;

  synchronized 的原理

    加了 synchronized 關鍵字的代碼段,生成的字節碼文件會多出 monitorenter 和 monitorexit 兩條指令(利用javap -v *.class字節碼文件)

    加了 synchronized 關鍵字的方法,生成的字節碼文件中會多一個 ACC_SYNCHRONIZED 標誌位,當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,

       若是設置了,執行線程將先獲取monitor,獲取成功以後才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其餘任何線程都沒法再得到同一個monitor對象。

    其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需經過字節碼來完成。

  synchronized 的缺點

    一、會讓沒有獲得鎖的資源進入Block狀態,爭奪到資源以後又轉爲Running狀態,這個過程涉及到操做系統用戶模式和內核模式的切換,代價比較高。

    二、Java1.6爲 synchronized 作了優化,增長了從偏向鎖到輕量級鎖再到重量級鎖的過分,可是在最終轉變爲重量級鎖以後,性能仍然較低。

  鎖的獲取和釋放 創建的happens-before關係

    

 

  鎖的釋放和獲取的內存語義

     JMM會把該線程對應的本地內存置爲無效。從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量。

    鎖釋放與volatile寫有相同的內存語義;鎖獲取與volatile讀有相同的內存語義。

    線程A釋放一個鎖,實質上是線程A向接下來將要獲取這個鎖的某個線程發出了(線程A對共享變量所作修改的)消息。

    線程B獲取一個鎖,實質上是線程B接收了以前某個線程發出的(在釋放這個鎖以前對共享變量所作修改的)消息。

    線程A釋放鎖,隨後線程B獲取這個鎖,這個過程實質上是線程A經過主內存向線程B發送消息。

  

相關文章
相關標籤/搜索