Java併發程式設計實戰——讀後感

卡斯特梅的雨傘 發表於 2021-10-11
Java

未完待續。

閱讀幫助

本文運用《如何閱讀一本書》的學習方法進行學習。

P15 表示對於書的第15頁。

Java併發程式設計實戰簡稱為併發書或者該書之類的。

熟能生巧,不斷地去理解,就像欣賞一部喜歡的電影,時不時就再看一遍,甚至把劇本下下來通讀。


思想

1、雖然現在都是分散式系統,日新月異,但是程式碼層面的併發思想是可以學習借鑑的,在不同的層面上使用併發的設計理念。

2、本地上的併發安全加鎖執行起來是比分散式redis鎖快的,比較redis鎖的互動需要通訊,本地是程式碼層級的執行,還有就是用redis鎖主要是我們現在是分散式系統,該鎖的作用可能作用的範圍是這幾臺伺服器,用於比如重試、扣庫存之類的,那麼加鎖的層級就必須是像redis這樣公用的鎖平臺才行。

3、程式碼的執行效率問題,如果能在程式碼層級範圍來操作併發執行安全的程式碼操作,即併發寫得好,這樣就能提高單臺伺服器的服務能力,比如1000tps和200tps的區別,還有就是分散式服務的線性擴充套件能力,不過這方面應該是分散式的架構問題,但回到第一個思想上來,我們可以學習其思想。

檢視閱讀

1 有系統的略讀(目錄,索引,關鍵欄位等,已完成)

2 粗淺的閱讀(不求甚解的全本閱讀,已完成)

有個不合格的地方是閱讀效率不高,耗費時間太長,不能在有效的時間內看完,主動性閱讀只能算及格,還不是很投入式,閱讀的主次不分,不能對需要花費心思理解的部分多花時間加深理解,而對於可以略讀的部分加快閱讀速度(當然,對於這種實用技術書一般都是重點)。

前言部分提供了這本書的各個章節的大體介紹,每個章節分屬的部分。

自我要求的4個基本問題

1、這本書在談些什麼

什麼是併發,為什麼要用到併發,以及如何使用併發,特別是編寫效能更好的併發程式碼。還有一部分併發程式碼如何測試的內容以及瞭解jvm的記憶體模型。

2、作者的主要想法、宣告、和論點。

作者想通過這本書教會學者如何更好更準確的編寫併發程式碼,關於併發程式碼的核心思想其實有三個方式解決問題:

一是不線上程間共享狀態變數,即不用。

1、執行緒封閉

2、棧封閉(區域性變數)

3、ThreadLocal(執行緒本地變數)類儲存。

最常見的ThreadLocal使用場景為用來解決資料庫連線、Session管理 。

二是將狀態變數修改為不可變的變數,即保證不變性。

滿足以下條件構造不變性物件:

1、物件建立以後其狀態(物件的成員變數、或者說域)就不能修改。(賦值的時候注意物件賦值是賦值地址(引用傳遞),所以要通過copy物件內容建立新物件賦值)。

2、物件的域都是final型別。

3、物件是正確構建的(在物件的建立期間,this引用沒有逸出)。

當多個執行緒同時訪問同一個資源,並且其中的一個或者多個執行緒對這個資源進行了寫操作,才會產生競態條件多個執行緒同時讀同一個資源不會產生競態條件

注意:

A(譯者注:注意,“不變”(Immutable)和“只讀”(Read Only)是不同的。當一個變數是“只讀”時,變數的值不能直接改變,但是可以在其它變數發生改變的時候發生改變。比如,一個人的出生年月日是“不變”屬性,而一個人的年齡便是“只讀”屬性,但不是“不變”屬性。隨著時間的變化,一個人的年齡會隨之發生變化,而一個人的出生年月日則不會變化。這就是“不變”和“只讀”的區別。(摘自《Java與模式》第34章)

B 只要不是帶有屬性值的引用型別物件都不會因為原本值改變而導致物件的final域也改變.

三是如果一定要訪問到可變的共享狀態變數時使用同步,即加鎖同步程式碼。

juc併發包的類和加鎖同步。

四是安全釋出物件

要安全地釋出一個物件,物件的引用以及物件的狀態(成員屬性)必須同時對其他執行緒可見。可通過4種方式安全釋出:

  1. 在靜態初始化函式中初始化一個物件引用。(或者靜態初始化塊)

    //靜態的初始化器,由於在JVM內部存在著同步機制,保證其安全釋出。
    public staitc Holder holder = new Holder(42);
    
  2. 將物件的引用儲存在volatile型別的域或者AtomicReferance物件中(可見性)。

  3. 將物件的引用儲存到某個正確構造物件的final型別域中(不變性)。

  4. 將物件的引用儲存到一個由鎖保護的域中(直接加鎖同步)。

3、這本書是否有道理?有合理性?

有道理且合理,不過有些還沒弄明白,還有就是目前高併發的技術不止是在程式碼層面的編寫,還有分散式應用等。

4、這本書跟我的關係和意義?

提高我的併發程式碼的理解領悟和編寫能力,理解併發處理的思想,通過併發可以更高效地發揮程式碼的效率。

分析閱讀

核心:提出問題,找出答案

一、在談些什麼

1、根據書的種類和主題分類

實用性書,工具書型別。

2、簡短概況本書內容

合理的配置執行緒和編寫併發程式碼可以更高效率地提高多核系統的效率,提高資源利用率和系統吞吐率,而編寫併發程式碼的原則是能不線上程間共享狀態變數儘量不共享;如果需要共享則最好是不變的狀態變數;上面這兩種都可以有效避免執行緒間的問題,實在不能避免,則在編寫併發程式碼時用上jdk api裡提供的juc包裡各種執行緒安全類,並注意併發執行緒的活躍性、效能、和測試問題。

//待補充

3、按順序和關聯性列舉全書大綱(簡介描述)和各個部分大綱

先大後小,先粗後細,搭出骨架後再充填細的部分,分起碼兩層分部結構,一,一之一,一之二等

一、併發理論基礎及規則(2-3章)
  1. 避免併發危險

  2. 構造執行緒安全的類

    加鎖機制:

    1、內建鎖

    同步程式碼塊的鎖就是方法呼叫所在的物件,該類的例項(所以不同例項可以有多個鎖);靜態同步方法的鎖是以類Class作為鎖,所以只有一把。

    2、重入

    鎖是可重入的,獲取鎖的操作的粒度是執行緒而不是呼叫(即不是每次呼叫就獲取一此鎖,如果該執行緒已經持有了,則他可以重入該方法,如果不可重入,則將因為獲取不到鎖而永遠停頓下去(已經持有了))。

    注意:能不能進入某個同步方法取決於是否取到該方法的的鎖,如果是例項鎖物件,則不同例項的方法呼叫操作分別獲取不同的鎖,並不會因為是同一個Class類而阻塞,只有靜態類才有且只有一個鎖物件。

    3、儘量縮小同步程式碼塊的作用範圍。

    4、不可能三角:安全性、簡單性和效能是不能同時達到的,而安全性是必須的,所以簡單些和效能之間存在著相互制約,一定不要盲目地為了效能而犧牲簡單性(可能破壞安全性)。

    5、持有鎖時間過長則會帶來活躍性或效能問題。因此,當執行時間較長的計算或者無法快速完成的操作(I/O),一定不要持有鎖。

    6、自己構建執行緒安全類和juc類庫來構建併發應用程式。

  3. 驗證執行緒安全

  4. 如何共享和釋出物件?

  5. 釋出和逸出 。當把一個物件傳遞給某個外部方法時(public),就相當於釋出了這個物件。釋出一個引用出去就可能帶來安全性問題,因為該引用資訊逸出了。所以我們使用封裝的最主要原因就是封裝能夠使得對程式的正確性進行分析變得可能,減少可能的破壞設計約束條件。

  6. 執行緒封閉:不共享資料,就不會有併發問題。區域性變數(即棧封閉)和ThreadLocal類是執行緒封閉的,執行緒封閉是在程式設計中的一個考慮因素(Swing、JDBC的Connection物件)

二、如何組合執行緒安全類juc模組介紹(4-5章)
  1. 執行緒安全組合

    設計執行緒安全類的三個基本要素:

    • 找出構成物件狀態的所以變數。
    • 找出約束狀態變數的不變性條件。
    • 簡歷物件狀態(變數)的併發訪問管理策略(形成正式文件)。

​ 例項封閉:將資料封裝在物件內部,可以將資料的訪問限制在物件的方法上,從而更容易保證現場在訪問資料時總能持有正確的鎖。(即把物件的資料訪問收斂到方法上,這樣只需要對訪問該方法加鎖同步就能保證對其訪問的執行緒安全)

  1. 執行緒安全容器類
  2. 執行緒安全同步工具類
三、結構化併發應用程式——如何利用執行緒提高併發應用程式的吞吐量或響應性(6-9章)
  1. 如何識別可並行執行的任務,以及如何在任務執行框架中執行任務。
  2. 健壯的併發應用程式的關鍵——如何實現取消和關閉執行緒執行任務。
  3. 執行緒池的使用
  4. 如何提高單執行緒子系統的響應性——圖形使用者介面應用程式
四、活躍性、效能與測試——確保併發程式執行預期任務,獲得理想效能(10-12章)
  1. 如何避免活躍性故障
  2. 如何提高併發程式碼的效能和可伸縮性。
  3. 測試併發程式碼的技術
五、高階主題—— (13-16章)
  1. 顯示鎖
  2. 原子變數
  3. 非阻塞演算法
  4. 自定義同步工具類

//待補充

4、作者想要我做什麼或者想要解決什麼問題。

理解和領悟併發的思想和其優缺點,並教我們怎麼編寫優秀的併發程式碼。

//待補充

二、詮釋這本書內容

5、關鍵字,與作者達成共識

安全性:永遠不會發生糟糕的事情。

活躍性:某件正確的事情最終會發生。(當某個操作無法繼續執行下去時,就會發生活躍性問題,如死鎖、飢餓、活鎖)

效能:包含多個方面,如服務時間過長(提供的服務介面響應太慢);響應不靈敏;吞吐率過低;資源消耗過高;可伸縮性降低等。

執行緒、鎖、

競態條件:由於不恰當的執行時序而出現不正確的結果。最常見的競態條件型別(先檢查後執行:CHECK-THEN-ACT)

最低安全性:執行緒在沒有同步情況下讀取變數時,可能會得到一個失效值,但至少不是隨機數,而是之前執行緒設定的值,這種安全保證稱為最低安全性。(最低安全性不適用於非volatile型別的64位數值變數--> double、long,因為jvm執行將64位的讀或寫操作分解成兩個32位的讀或寫操作)

依賴狀態的操作:即某個操作包含基於狀態的先驗條件(如佇列刪除前判空等),那麼這個操作就是依賴狀態的操作。要實現某個等待先驗條件為真時才執行的操作,可以通過(Blocking Queue , Semaphore 實現)。

//待補充

6、重要句子即作者的主旨

  • 要編寫執行緒安全的程式碼,其核心在於要對狀態訪問操作進行管理,特別是對共享的(shared)和可變的(mutable)狀態的訪問。物件的狀態(主要在例項或靜態域)中的資料。
  • 共享:多個執行緒訪問。
  • 可變:生命週期內可以發生變化。
  • 正確的程式設計方法:首先使程式碼正確執行,然後再提高程式碼的速度。(先完成,再優化)
  • 執行緒安全性定義:當多個執行緒訪問某個類時,這個類始終都能表現出正確的行為,說明這個類是執行緒安全的。

無狀態物件一定是執行緒安全的。(因為它既不包含任何域,也不包含任何對其他類的類中域的引用。)

  • 儘可能使用現有的執行緒安全物件(如AtomicLong)來管理類的狀態。如往無狀態的類中新增一個有執行緒安全的物件來管理狀態的類時,這個類仍然是安全的。
  • 要保持狀態的一致性,就需要在單個原子操作中更新所有相關的狀態變數。(即使這些狀態變數各自都是執行緒安全的,也要保證所以的狀態操作在同一個原子操作中,相當於一個事務)
  • 正如“除非需要更高的可見性,否則將所有的域(屬性)都宣告為私有域是一個良好的程式設計習慣”,“除非需要某個域是可變的,否則應將其宣告為final域”也是一個良好的程式設計習慣。
  • 任何執行緒都可以在不需要額外同步的情況下安全地訪問不可變物件,即使在釋出這些物件時沒有使用同步(疑問:我的理解是這些物件不可變,所以只發布一次;或者是這些物件內部不可變物件的三個條件,所以不會有不確定性)。
  • 例項封閉是構建執行緒安全類的最簡單方式。即:將資料封裝在物件內部,可以將資料的訪問限制在物件的方法上,從而更容易確保執行緒在訪問資料時總能持有正確的鎖。

例項封閉例項:

@ThreadSafe
public class TestBeanSet {
    @GuardedBy("this")
    private final Set<TestBean> mySet = new HashSet<TestBean>();

    public synchronized void addTestBean(TestBean t) {
        mySet.add(t);
    }

    public synchronized boolean containsTestBean(TestBean t) {
        return mySet.contains(t);
    }
}
  • 如果不瞭解物件的不變性條件(取值不能為負數)與後驗條件(取值變化範圍在兩個域值的範圍區間,如範圍的上下界,下界值應該小於等於上界值),那麼就不能確保執行緒安全性。要滿足在狀態變數的有效值或者狀態轉換上的各種約束條件,就需要藉助於原子性和封裝性(怎麼藉助封裝性?)。
  • 基於狀態的先驗條件判斷的操作稱為依賴狀態的操作。如判空移除元素等。這種比較多見與生存消費模式下的操作,常見有Blocking Queue 或 Semaphore 。

//待補充

7、找出作者的的論述,總結作者的想法、看法

歸納法或者演繹法

併發程式中使用和共享物件時的實用策略,有四:

1、執行緒封閉。執行緒封閉的物件只能由一個執行緒擁有,物件被封閉在該執行緒中,不共享,則執行緒安全使用物件。

2、只讀共享(即不可變物件)。在沒有額外的同步情況下,共享的只讀物件可以由多個執行緒併發訪問,但任何執行緒都不能修改它(有的情況下可以新建一個不可變物件去替換它)。共享的只讀物件包括不可變物件和事實不可變物件。

3、執行緒安全共享(經過執行緒安全同步的資料結構或者類,如currenthashmap等juc包下的執行緒安全類)。執行緒安全的物件在其內部實現同步,因此多個執行緒可以通過物件的公共介面(get/set)來進行訪問而不需要進一步同步(加鎖等操作)。

4、保護物件。被保護的物件只能通過持有特定的鎖來訪問。保護物件包括封裝在其他執行緒安全物件中的物件(是什麼?),以及已釋出的並且有某個特定鎖保護的物件(顯示鎖,@GuardedBy)。

//待補充

8、作者要我們這麼做的目的(解決了哪些問題)

寫出執行緒安全的類。

三、評論這本書

前提條件:完成大綱架構

在評論時能區分真正的知識和個人的觀點的不同

批評時要能證明作者的知識不足或錯誤、不合邏輯、分析與理由不完整等。

自己的評論,或者讀後感。

一些知識整理

1、併發中的單例模式寫法:

單例模式(從雙重加鎖走向延遲初始化佔位類模式)

java併發中的延遲初始化 推薦這個

2、高效是指能在序列性和非同步性之間找到合理的平衡。

3、讀取volatile 變數開銷只比讀取非volatile變數的開銷略高一點,因此遠低於加鎖的變數。volatile的正確使用方式包括:只能確保可見性,以及標識程式生命週期事件的發生(如初始化或關閉)。還需要尋找一下程式中volatile的使用。

原子變數(atomicInteger等)是一種更好的volatile。

滿足以下三個條件,才應該使用volatile變數:

  1. 對變數的寫入操作不依賴變數的當前值,或者確保只有單個執行緒更新變數的值(set方法是synchronized的同步方法)
  2. 該變數不會與其它狀態變數一起納入不變性條件中。(否則可見性就沒有意義,因為他如果本來就不變,那不需要可見性啊)
  3. 在訪問變數時不需要加鎖。

4、如果想在建構函式中註冊一個事件監聽器或啟動執行緒,可以使用一個私有的建構函式和一個公共的工廠方法。還需要理解下!

疑問解答

1、p1中的粗粒度通訊機制交換資料,包括:套接字、訊號處理器、共享記憶體、訊號量以及檔案等,裡面名稱的意思和通訊機制一般是什麼?

2、時間分片(time slicing)?

A:指每個CPU會把其時間進行分配,每個程式都有機會佔有一個或多個片段,去執行自己的任務。

3、什麼是馮諾依曼計算機?

4、什麼是控制程式碼?記憶體控制程式碼?檔案控制程式碼?

5、上下文切換會帶來多大的開銷呢?

6、抽象和封裝會降低程式的效能?是拿來跟程式導向(C)比?

7、volatile的使用?

8、釋出和逸出需要仔細瞭解下?p34:不正確構造?

9、p25:搞不清楚這兩個同步程式碼塊不會因為中間釋放鎖而導致失去鎖,應用安全性不能滿足麼?

10、為什麼final型別的域使用越多,就越能簡化物件可能狀態的分析過程?或者說final 域為什麼能保證是不變的?如果是個fina物件呢,物件裡面的屬性也必須是final的才能保證吧?

使用到併發的開原始碼或元件

1、Swing 的使用者介面框架

2、Servlet

3、RMI(rpc)

4、Swing、JDBC學習執行緒封閉技術

5、連線池是執行緒安全的。

參考

《Java併發程式設計實戰》