Java學習總結 1-1-2 線程安全問題

筆記記錄,整理的亂七八糟~~

 

--> Java內存模型(JMM)、JVM運行時數據區

    Java虛擬機規範是對Java虛擬機的描述:     Java虛擬機規範 --(描述、約束)--> Java虛擬機
        
    Java語言規範是對Java語言(代碼)的描述: Java語言規範   --(描述、約束)--> Java代碼

    Java內存模型是對Java語言的描述、JVM運行時數據區是對Java虛擬機的描述
        
    
--> 多線程中的問題
    1、所見非所得
    2、無法直觀去檢測程序的準確性
    3、不同的運行平臺有不同的表現
    4、錯誤難重現

    
--> JIT編譯器(Just In Time Compiler)
    腳本語言與編譯語言的區別:解釋執行和編譯執行
    解釋執行:將每行代碼解析成指令逐行執行
    編譯執行:將所有代碼先進行編譯,之後統一交給CPU執行
    
    JIT編譯器:(指令重排序)
     僞代碼:

int i = 0;
boolean isRunning = true;
new Thread(new Runnable() {
    @Override
    public void run() {
        while(isRunning) {
            i++;
        }
    }
}).start();
Thread.sleep(1000);
isRunning = false;


        方法被多次調用或者循環體中的代碼被多次調用時執行編譯(編譯執行),JIT編譯器將代碼執行性能優化(cache,指令重排)例:

while(isRunning){
    i++
};

//指令重排爲:
boolean f = isRunning;
if(f){
    while(true){
        i++;
    }
} 


        使主線程修改isRunning對線程不起作用,導致線程內i++一直執行。怎麼解決?volitile ↓
        
    volatile關鍵字:cannot be cached
        可見性:禁止緩存、volatile描述的變量不做指令重排序
    
        上述代碼改爲 volatile boolean isRunning = true; 即可解決
        
        
--> Shared Variables共享變量
             所有實例字段、靜態字段和數組元素都存儲在堆內存中。這些字段和數組都是共享變量,共享變量有線程安全的問題
        
    問題:如果至少有一個訪問時寫操作,那麼對同一的變量的兩次訪問是衝突的
    
    線程操作:
            read操作
            write操作
            volatile read
            volataile write
            Lock,Unlock
            線程的第一個和最後一個操作
            外部操作(多個進程或線程對DB的操作等等)
            
        1、一個程序執行的操作可被其它線程感知或被其它線程直接影響
        2、Java內存模型只描述線程間操作,不描述線程內操作,線程內操作按照線程內語義執行
        3、所有線程間操作,都存在可見性問題,JVM需對其進行規範
        
        final在JMM中的處理:
            final在該對象的構造函數中設置對象的字段,當線程看到該對象時,將始終看到該對象的final字段的正確構造版本。
                  例:f = new finalDemo();讀到的f.x一定最新,x爲final字段
            如果在構造函數中設置字段後發生讀取,則會看到該final字段分配的值,否則它將看到默認值;
                  例:final = 1; public finalDemo(){x=1;y=x;};y會等於1
                
        字分裂(word Tearing):
              部分處理器沒有提供寫單個字節功能。在這樣的處理器上更新byte數組,則將讀取整個數組,更新對應字節,最後將整                            個內容寫回內存, 這是不合法的。因此,儘量不要對byte中的原始進行重新賦值,更不要在多線程程序中這樣做
        
        double和long特殊處理:
             在《Java語言規範》中,對非volatitle的double和long類型的單次寫作是分兩次進行的(一共64位,修改時只修改32
             位),每次操作其中32位,可能導致第一次寫入後,讀取的值是贓數據,第二次寫完成才能讀到正確值商業JVM不會存
             在這個問題,雖然規範沒要求實現原子性,但是出於考慮實際應用,大部分實現了原子性,在編程中推薦使用volatitle修
             飾

        
--> happens-befare 先行發生
      在Java內存模型中,happens-before應該翻譯成:前一個操作的結果可以被後續的操作獲取。意思爲前面一個操作把變量a                    賦值爲1,那後面一個操作肯定能知道a已經變成了1。

    先行發生規則:現在電腦都是多CPU,並且都有緩存,導致多線程直接的可見性問題,所以JMM針對線程同步制定以下規則
        1、程序次序規則:在一個線程內一段代碼的執行結果是有序的。就是還會指令重排,但是無論怎麼排,結果是按照代碼順                                                   序生成的不會變
        2、管程鎖定規則:無論在單線程環境還是多線程環境,對於同一個鎖來說,一個線程對這個鎖解鎖之後,另一個線程獲取                                                   了這個鎖都能看到前一個線程的操作結果(管程是一種通用的同步原語,synchronized就是管程的實現)
        3、volatitle變量規則:如果一個線程先去寫一個volatitle變量,然後一個線程去讀這個變量,那麼這個寫操作的結果一定對                                                       讀的這個線程可見
        4、線程啓動規則:在主線程A執行過程中,啓動子線程B,那麼線程A在啓動子線程B之前對共享變量的修改結果對線程B可                                                   見
        5、線程終止規則:在主線程A執行過程中,子線程B終止,那麼線程B在終止之前對共享變量的修改結果在線程A中可見
        6、線程終端規則:對線程interrupt()方法的調用先行發生於被終端線程代碼檢測帶終端時間的發生,可以通過                                                                     Thread.interupted()檢測到是否發生終中斷
        7、傳遞規則:happens-before原則具有傳遞性,即A happens-before B,B happens-before C,則A happens-before C
        8、對象終結規則:一個對象的初始化的完成,也就是構造函數執行的結果一定happens-before他的finalize()方法
        
        
-- >Java原子操作 CAS
       將整個操作視作一個整體,資源在該次操作中保持一致,這是原子性的核心特徵。 原子操作可以是一個步驟,也可以是多                       個操作步驟,但是其順序不可以被打亂,也不可以被切割而只執行其中的一部分(不可中斷性)
    
    CAS(Compare and swap):
            比較和替換,屬於硬件同步原語,處理器提供了基本內存操作的原子性保障。CAS操作需要輸入兩個值,一箇舊值A(期
            望操作前的值)和一個新值(期望修改後的值),在操作期間先對舊值進行比較,如沒有發生變化才交換爲新值,反之
            則不替換
        
        自旋:修改內存中數據時進行CAS操作,操作失敗重新讀取數據並再次進行操作直到成功爲止
        
        JVM提供CAS操作的API:Unsafe類
                         java.util.concurrent.atomic  包下均爲原子性操作類,基於Unsafe實現
            
         jdk1.8更新:
              計數器增強版,高併發下性能更好:分成多個操作單元,不同線程更新不同的單元,只需要彙總時計算所有單元的操作
              場景:高併發頻繁更新、不太頻繁讀取
                    更新器:DoubleAccumulator、LongAccumulator
                    計數器:DobbleAdder、LongAdder
    
    CAS的三個問題:
        1.循環+CAS,自旋的實現讓所有線程都處於高頻運行,爭搶CPU執行時間的狀態。如果操作長時間不成功會帶來很大的
           CPU消耗
        2.僅針對單個變量的操作,不能用於多個變量來實現原子操作
        3.ABA問題 ->A的版本問題    
    
    線程安全概念:(可見性,原子性)
        竟態條件:如果程序運行順序的改變會影響最終結果,就說存在竟態條件。大多數竟態條件的本質,就是基於某種可能失
                         效的觀察結果來做出判斷或執行計算
        臨街區:存在竟態條件的代碼區域
    
    共享資源:
        1、只有多個線程更新共享資源時,纔會發生竟態條件,可能會出現線程安全問題
        2、棧封閉時,不會在線程之間共享的變量,都是線程安全的(局部變量)
        3、局部對象引用不共享,但是引用的對象存儲在共享堆中。如果方法內創建的對象,只是在方法中傳遞,並且不對其他線
              程可用,那麼也是線程安全的
        4、不可變的共享對象來保證對象在線程間共享時不會被修改,從而實現線程安全。實例被創建,value變量就不能被修
              改,這就是不可變性
        5、使用Threaddlocal時,相當於不同的線程操作的是不同的資源,所以不存在線程安全問題


-- >鎖   詳細參考https://www.cnblogs.com/paddix/p/5405678.html

        自旋鎖:當一個線程在獲取鎖的時候,如果鎖已經被其它線程獲取,那麼該線程將循環等待,然後不斷的判斷鎖是否能被                                     成功獲取,直到獲取鎖纔會退出循環
    
    樂觀鎖:假定沒有衝突,在修改數據時如果發現數據和之前獲取的不一致,則讀到最新數據,修改後重試修改
    悲觀鎖:假定會發生併發衝突,同步所有對數據的相關操作,從數據就開始上鎖
    獨享鎖(寫):給資源加上寫鎖,線程可以修改資源,其他線程不能再加鎖(單寫)
    共享鎖(讀):給資源加上讀鎖後只能讀不能改,其他線程也只能加讀鎖,不能加寫鎖(多讀)
    可重入鎖/不可重入鎖:線程時候可以進入任何一個它已經擁有的鎖所同步着的代碼塊。解鎖重入鎖時,解鎖次數需和重入上                                                       鎖的次數相等
   公平鎖/非公平鎖:爭搶鎖的順序,如果是按先來後到,則爲公平
    
    輕量級鎖:無實際競爭,多個線程交替使用鎖;允許短時間的鎖競爭:
                鎖的狀態總共有四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨着鎖的競爭,鎖可以從偏向鎖升級到輕量級
                                                  鎖,再升級的重量級鎖(但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的
                                                  降級)。JDK 1.6中默認是開啓偏向鎖和輕量級鎖的,可以通過-XX:-UseBiasedLocking來禁
                                                  用偏向鎖
        輕量級鎖的加鎖過程:
          (1)在代碼進入同步塊的時候,如果同步對象鎖狀態爲無鎖狀態(鎖標誌位爲“01”狀態,是否爲偏向鎖爲“0”),虛擬                           機首先將在當前線程的棧幀中建立一個名爲=鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word                                   的拷貝,稱之爲 Displaced Mark Word。
          (2)拷貝對象頭中的Mark Word複製到鎖記錄中。
          (3)拷貝成功後,虛擬機將使用CAS操作嘗試將對象的Mark Word更新爲指向Lock Record的指針,並將Lock record                                   裏的owner指針指向object mark word。如果更新成功,則執行步驟(3),否則執行步驟(4)。
          (4)如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標誌位設置爲“00”,即                               表示此對象處於輕量級鎖定狀態
          (5)如果這個更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線
                        程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行。否則說明多個線程競爭鎖,輕量級鎖就要膨脹
                        爲重量級鎖,鎖標誌的狀態值變爲“10”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖
                        的線程也要進入阻塞狀態。 而當前線程便嘗試使用自旋來獲取鎖,自旋就是爲了不讓線程阻塞,而採用循環去獲
                        取鎖的過程。
        輕量級鎖的解鎖過程:
          (1)通過CAS操作嘗試把線程中複製的Displaced Mark Word對象替換當前的Mark Word
          (2)如果替換成功,整個同步過程就完成了
          (3)如果替換失敗,說明有其他線程嘗試過獲取該鎖(此時鎖已膨脹),那就要在釋放鎖的同時,喚醒被掛起的線
                        程
        
    重量級鎖:有實際競爭,且鎖競爭時間長(monitor,mutex):
                Synchronized是通過對象內部的一個叫做監視器鎖(monitor)來實現的。但是監視器鎖本質又是依賴於底層的操作系
                統的Mutex Lock來實現的。而操作系統=實現線程之間的切換這就需要從用戶態轉換到核心態,這個成本非常高,狀態
                之間的轉換需要相對比較長的時間,這就是爲什麼Synchronized效率低的原因。
                因此,這種依賴於操作系統Mutex Lock所實現的鎖我們稱之爲“重量級鎖”。JDK中對Synchronized做的種種優化,其
                核心都是爲了減少這種重量級鎖的使用。JDK1.6以後,爲了減少獲得鎖和釋放鎖所帶來的性能消耗,提高性能,引入
                了“輕量級鎖”和“偏向鎖”
    
    偏向鎖:無實際競爭,且將來只有第一個申請鎖的線程會使用鎖:
                引入偏向鎖是爲了在無多線程競爭的情況下儘量減少不必要的輕量級鎖執行路徑,因爲輕量級鎖的獲取及釋放依賴多次
                CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(由於一旦出現多線程競爭的情況就必須
                撤銷偏向鎖,所以偏向鎖的撤銷操作的性能損耗必須小於節省下來的CAS原子指令的 性能消耗)。上面說過,輕量級鎖
                是爲了在線程交替執行同步塊時提高性能,而偏向鎖則是在只有一個線程執行同步塊時進一步提高性能。
        偏向鎖獲取過程:
          (1)訪問Mark Word中偏向鎖的標識是否設置成1,鎖標誌位是否爲01——確認爲可偏向狀態。
          (2)如果爲可偏向狀態,則測試線程ID是否指向當前線程,如果是,進入步驟(5),否則進入步驟(3)。
          (3)如果線程ID並未指向當前線程,則通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中線程ID設置爲當前線                                    程ID,然後執行(5);如果競爭失敗,執行(4)。
          (4)如果CAS獲取偏向鎖失敗,則表示有競爭。當到達全局安全點(safepoint)時獲得偏向鎖的線程被掛起,偏向鎖                                    升級爲輕量級鎖,然後被阻塞在安全點的線程繼續往下執行同步代碼。
          (5)執行同步代碼。
        偏向鎖的釋放:
          偏向鎖的撤銷在上述第四步驟中有提到。偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放
               鎖,線程不會主動去釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節碼正在執行),它
               會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態,撤銷偏向鎖後恢復到未鎖定(標誌位爲“01”)或輕
               量級鎖(標誌位爲“00”)的狀態。
    
    幾種實現鎖的方式:synchronized、ReentrantLock、ReentrantReadWriteLock
    
    同步關鍵字synchronized:
        鎖的狀態總共有四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖
        特性:可重入、獨享、悲觀鎖、非公平鎖、原子性(wait、notfiy會破壞synchronized原子性)
        鎖優化:當代碼被調用多次觸發JIT編譯,對鎖進行優化
            適應性自旋(Adaptive Spinning):從輕量級鎖獲取的流程中我們知道,當線程在獲取輕量級鎖的過程中執行CAS操作
                                 失敗時,是要通過自旋來獲取重量級鎖的。問題在於,自旋是需要消耗CPU的,如果一直獲取不到鎖的話,
                                 那該線程就一直處在自旋狀態,白白浪費CPU資源。解決這個問題最簡單的辦法就是指定自旋的次數,例如
                                 讓其循環10次,如果還沒獲取到鎖就進入阻塞狀態。但是JDK採用了更聰明的方式——適應性自旋,簡單
                                 說就是線程如果自旋成功了,則下次自旋的次數會更多,如果自旋失敗了,則自旋的次數就會減少
            鎖消除:在單線程多次加鎖解鎖造成不必要的損失時
            鎖粗化:就是將多次連接在一起的加鎖、解鎖操作合併爲一次,將多個連續的鎖擴展成一個範圍更大的鎖,可從代碼層
                          面優化
            
        synchronized關鍵字不僅實現同步,JMM中規定,synchronized要保證可見性(不可被緩存)
    
        鎖升級:


    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章