Java併發編程實戰第一部分學習記錄

在這裏插入圖片描述

01 | 可見性、原子性和有序性問題:併發編程Bug的源頭

併發程序幕後的故事

在這裏插入圖片描述

源頭之一:緩存導致的可見性問題

源頭之二:線程切換帶來的原子性問題

在這裏插入圖片描述

源頭之三:編譯優化帶來的有序性問題

在這裏插入圖片描述

總結

在這裏插入圖片描述

02 | Java內存模型:看Java如何解決可見性和有序性問題

什麼是Java內存模型?

在這裏插入圖片描述

使用volatile的困惑

Happens-Before 規則

前面一個操作的結果對後續操作是可見的

  1. 程序的順序性規則
    這條規則是指在一個線程中,按照程序順序,前面的操作 Happens-Before 於後續的任意操作。

  2. volatile變量規則
    這條規則是指對一個volatile變量的寫操作, Happens-Before 於後續對這個volatile變量的讀操作。

  3. 傳遞性
    這條規則是指如果A Happens-Before B,且B Happens-Before C,那麼A Happens-Before C。

  4. 管程中鎖的規則
    這條規則是指對一個鎖的解鎖 Happens-Before 於後續對這個鎖的加鎖。

  5. 線程 start() 規則
    這條是關於線程啓動的。它是指主線程A啓動子線程B後,子線程B能夠看到主線程在啓動子線程B前的操作。

  6. 線程 join() 規則
    這條是關於線程等待的。它是指主線程A等待子線程B完成(主線程A通過調用子線程B的join()方法實現),當子線程B完成後(主線程A中join()方法返回),主線程能夠看到子線程的操作。當然所謂的“看到”,指的是對共享變量的操作。

總結

在Java語言裏面,Happens-Before的語義本質上是一種可見性,A Happens-Before B 意味着A事件對B事件來說是可見的,無論A事件和B事件是否發生在同一個線程裏。例如A事件發生在線程1上,B事件發生在線程2上,Happens-Before規則保證線程2上也能看到A事件的發生。

03 | 互斥鎖(上):解決原子性問題

那原子性問題到底該如何解決呢?

你已經知道,原子性問題的源頭是線程切換
“同一時刻只有一個線程執行”這個條件非常重要,我們稱之爲互斥。

簡易鎖模型

改進後的鎖模型

Java語言提供的鎖技術:synchronized

04 | 互斥鎖(下):如何用一把鎖保護多個資源?

保護沒有關聯關係的多個資源

保護有關聯關係的多個資源

總結

“原子性”的本質是什麼?其實不是不可分割,不可分割只是外在表現,其本質是多個資源間有一致性的要求,操作的中間狀態對外不可見。

05 | 一不小心就死鎖了,怎麼辦?

如何預防死鎖

在這裏插入圖片描述

  1. 破壞佔用且等待條件
  2. 破壞不可搶佔條件
  3. 破壞循環等待條件

06 | 用“等待-通知”機制優化循環等待

用synchronized實現等待-通知機制

07 | 安全性、活躍性以及性能問題

那是不是所有的代碼都需要認真分析一遍是否存在這三個問題呢?當然不是,其實只有一種情況需要:存在共享數據並且該數據會發生變化,通俗地講就是有多個線程會同時讀寫同一數據。

併發編程中我們需要注意的問題有很多,很慶幸前人已經幫我們總結過了,主要有三個方面,分別是:安全性問題、活躍性問題和性能問題。

安全性問題

當多個線程同時訪問同一數據,並且至少有一個線程會寫這個數據的時候,如果我們不採取防護措施,那麼就會導致併發Bug,對此還有一個專業的術語,叫做數據競爭(Data Race)

競態條件,指的是程序的執行結果依賴線程執行的順序。

面對數據競爭競態條件問題,又該如何保證線程的安全性呢?其實這兩類問題,都可以用互斥這個技術方案,而實現互斥的方案有很多,CPU提供了相關的互斥指令,操作系統、編程語言也會提供相關的API。從邏輯上來看,我們可以統一歸爲:

活躍性問題

所謂活躍性問題,指的是某個操作無法執行下去。我們常見的“死鎖”就是一種典型的活躍性問題,當然除了死鎖外,還有兩種情況,分別是“活鎖”和“飢餓”

有時線程雖然沒有發生阻塞,但仍然會存在執行不下去的情況,這就是所謂的“活鎖”。
所謂“飢餓”指的是線程因無法訪問所需資源而無法執行下去的情況。

性能問題

Java SDK併發包裏之所以有那麼多東西,有很大一部分原因就是要提升在某個特定領域的性能。
在這裏插入圖片描述

總結

總結

08 | 管程:併發編程的萬能鑰匙

什麼是管程

在這裏插入圖片描述

MESA模型

在這裏插入圖片描述
在這裏插入圖片描述

wait()的正確姿勢

notify()何時可以使用

總結

在這裏插入圖片描述

09 | Java線程(上):Java線程的生命週期

通用的線程生命週期

在這裏插入圖片描述

Java中線程的生命週期

在這裏插入圖片描述

  1. RUNNABLE與BLOCKED的狀態轉換
    只有一種場景會觸發這種轉換,就是線程等待synchronized的隱式鎖

  2. RUNNABLE與WAITING的狀態轉換
    第一種場景,獲得synchronized隱式鎖的線程,調用無參數的Object.wait()方法
    第二種場景,調用無參數的Thread.join()方法。
    第三種場景,調用LockSupport.park()方法。調用LockSupport.park()方法,當前線程會阻塞,線程的狀態會從RUNNABLE轉換到WAITING。調用LockSupport.unpark(Thread thread)可喚醒目標線程,目標線程的狀態又會從WAITING狀態轉換到RUNNABLE。

  3. RUNNABLE與TIMED_WAITING的狀態轉換
    在這裏插入圖片描述

  4. 從NEW到RUNNABLE狀態,調用線程對象的start()方法

  5. 從RUNNABLE到TERMINATED狀態

那stop()和interrupt()方法的主要區別是什麼呢?

10 | Java線程(中):創建多少線程纔是合適的?

爲什麼要使用多線程?

度量性能的指標有很多,但是有兩個指標是最核心的,它們就是延遲和吞吐量

多線程的應用場景

創建多少線程合適?

CPU密集型的計算場景,理論上“線程的數量=CPU核數”就是最合適的。不過在工程上,線程的數量一般會設置爲“CPU核數+1”

對於I/O密集型計算場景,最佳線程數=CPU核數 * [ 1 +(I/O耗時 / CPU耗時)]

其實只要把握住一條原則就可以了,這條原則就是將硬件的性能發揮到極致。

11 | Java線程(下):爲什麼局部變量是線程安全的?

方法是如何被執行的

局部變量存哪裏?

調用棧與線程

線程封閉

12 | 如何用面向對象思想寫好併發程序?

一、封裝共享變量

對於這些不會發生變化的共享變量,建議你用final關鍵字來修飾

二、識別共享變量間的約束條件

三、制定併發訪問策略

在這裏插入圖片描述

13 | 理論基礎模塊熱點問題答疑

那這個“串行的故事”是怎樣的呢?

在這裏插入圖片描述
在這裏插入圖片描述

1. 用鎖的最佳實踐

2. 鎖的性能要看場景

3. 競態條件需要格外關注

4. 方法調用是先計算參數

5. InterruptedException異常處理需小心

6. 理論值 or 經驗值

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