1. 程式人生 > >[Java併發程式設計實戰] 基礎知識

[Java併發程式設計實戰] 基礎知識

  1. 什麼是執行緒安全性?
    定義執行緒安全性:當多個執行緒訪問某個類時,這個類始終都能表現正確的行為,那麼就稱這個類是執行緒安全的。 單執行緒:所見及所知(we know it when we see it)

  2. 競態條件
    當某個計算的正確性取決於多個執行緒的交替執行時序時,那麼就會發生競態條件。

    最常見的競態條件型別就是“先檢查後執行(Check-Then-Act)”操作,即通過一個可能失效的觀測結果來決定下一步的動作。

    例如,首先觀測某個條件為真(檔案X不存在),然後根據這個觀察結果採用相應的動作(建立檔案X),但事實上,在觀測到這個結果以及開始建立檔案檔案之間,觀測結果可能變得無效(另一個執行緒在這期間建立了檔案X),從而導致各種問題(未預期的異常、資料被覆蓋、檔案被破壞等)。 如:單例模式的懶載入

  3. 複合操作
    LazyInit遞增運算 包含一組需要以原子方式執行(或者說不可分割)的操作。

    要避免競態條件問題,就必須在某個執行緒修改該變數時,通過某種方式防止其他執行緒使用這個變數,從而確保其他執行緒只能在修改操作完成之前或者之後讀取和修改狀態,而不是在修改狀態的過程中。

    原子操作是指,對於訪問同一個狀態的所有操作(包括該操作本身)來說,這個操作是一個以原子方式執行的操作。

    在 java.util.concurrent.atomic 包中包含了一些原子變數類,用於實現在數值和物件引用上的原子狀態轉化。 通過用 AtomicLong 來代替 long 型別的計數器,能夠確保所有對計數器狀態的訪問操作都是原子的。


  4. 要保持狀態的一致性,就需要在單個原子操作中更新所有相關的狀態變數。

    4.1 內建鎖
    Java 提供了一種內建的鎖機制來支援原子性:同步程式碼塊(Synchronized Block)。同步程式碼塊包括兩部分:一個作為鎖的物件引用,一個作為由這個鎖保護的程式碼塊。

    以關鍵字 synchrogazed 來修飾的方法就是一種橫跨整個方法體的同步程式碼塊,其中該同步程式碼塊的鎖就是方法呼叫所在的物件。靜態的 synchrogazed 方法以 Class 物件作為鎖。

    synchronized (lock){
            //訪問或修改由鎖保護的共享狀態
    }
    

    每個 Java 物件都可以用做一個實現同步的鎖,這些鎖被稱為內建鎖(Intrinsic Lock)或監視器鎖(Monitor Lock)

    執行緒在進入同步程式碼塊之前會自動獲得鎖,並且在退出同步程式碼塊時自動釋放鎖,而無論是通過正常的控制路徑退出,還是通過從程式碼塊中丟擲異常退出。獲得內建鎖的唯一途徑就是進入由這個鎖保護的同步程式碼塊或方法。

    Java 內建鎖相當於一種互斥體(或互斥鎖),這意味著最多隻有一個執行緒能持有這種鎖。 當執行緒 A 嘗試獲取一個由執行緒 B 持有的鎖時,執行緒 A 必須等待或者阻塞,直到執行緒 B 釋放這個鎖。如果 B 永遠不釋放鎖,那麼 A 也將永遠地等下去存在問題:效能低,併發性差

    4.2 重入

    當某個執行緒請求一個由其他執行緒持有的鎖時,發出請求的執行緒就會阻塞。然而,由於內建鎖是可重入的,因此如果某個執行緒檢視獲得一個已經由它自己持有的鎖,那麼這個請求就會成功。

    “重入”意味著獲取鎖的操作的粒度是“執行緒”,而不是“呼叫”。重入的一種實現方式是:為每個鎖關聯一個獲取計數值和一個所有者執行緒。當計數值為0時,這個鎖就被認為是沒有被任何執行緒持有。當執行緒請求一個未被持有的鎖時,JVM 將記下鎖的持有者,並且將獲取計數值置為1。如果同一個執行緒再次獲取這個鎖,計數值將遞增,而當執行緒退出同步程式碼塊時,計數器會相應地遞減。當計數器為0時,這個鎖將被釋放

  5. 用鎖來保護狀態

    對於可能被多個執行緒同時訪問的可變狀態變數,在訪問它時都需要持有同一個鎖,在這種情況下,我們稱狀態變數是由這個鎖保護的。

    每個共享的和可變的變數都應該只由一個鎖來保護,從而使維護人員知道是哪一個鎖。

    對於每個包含多個變數的不變性條件,其中涉及的所有變數都需要由同一個鎖來保護。

  6. 活躍性與效能

    如果對整個方法進行同步,這中簡單且粗粒度的方法能確保執行緒安全性,但付出的代價卻很高。不良併發

    使用兩個獨立的同步程式碼塊,每個同步程式碼塊都只包含一小段程式碼

    當執行時間較長的計算或者可能無法快速完成的操作(例如,網路I/O或控制檯I/O),一定不要持有鎖。