Java-并發編程知識點總結

目錄:

  1. 線程基礎
  2. 線程池
  3. 各種各樣的鎖
  4. 并發容器
  5. 原子類
  6. Java 內存模型
  7. 線程協作
  8. AQS 框架

一、線程基礎

1. 為什么繼承 runnable 接口比繼承 Thread 類的線程實現方式好?

  • 可以把不同的執行內容解耦,全責分明
  • 某些情況可以減少開銷,提高性能(比如可用線程池中已有的線程去執行 runnable,而不用重新創建線程)
  • 繼承 Thread 類的單繼承特性會限制代碼的擴展性

2. 線程是如何在 6 種狀態之間轉化的?

  • 線程的 6 種狀態:New(新創建)、Runnable(可運行)、Blocked(被阻塞)、Waiting(等待)、Timed_waiting(計時等待)、Terminated(被終止)
  • 新創建線程處于 New 狀態,調用 Thread#start 方法后進入 Runnable 狀態,Runnable 對應操作系統的 Running 和 Ready 狀態,代表可能正在被執行或正在等待 CPU 分配資源
  • 當要進入 synchronized 方法或代碼塊時卻沒搶到 monitor 鎖,會由 Runnable 狀態進入 Blocked 狀態,獲取到 monitor 鎖后會進入 Runnable 狀態
  • 執行 Object#wait 或 LockSupport#park 會進入 Waiting 狀態;執行帶 timeOut 參數的 Object#wait 或 LockSupport#park 會進入 Timed_waiting 狀態
  • 調用 LockSupport#unpark 、被中斷或超時時間到會由 Waiting/Timed_waiting 狀態進入 Runnable 狀態
  • 被 notify/notifyAll 喚醒,會由 Waiting/Timed_waiting 狀態進入 Blocked 狀態
  • run 方法執行完或異常終止會進入 Terminated 狀態
  • 一個線程只會經歷一次 New 和 Terminated 狀態,中間狀態才可以相互轉換

3. 如何理解鎖池和等待池?

  • 如果某對象的鎖已被一個線程占有,其他線程調用此對象的 sychronized 代碼塊時無法獲取到鎖,就會進入此對象的鎖池,鎖池中的線程會競爭該對象的鎖
  • 如果一個線程調用了 Object#wait 方法,此線程就會進入此對象的等待池,等待池中的線程不會去競爭該對象的鎖
  • 調用 notify 方法會隨機喚醒一個等待池中的線程,并移到鎖池中;調用 notifyAll 方法會喚醒等待池中所有的線程,并全移到鎖池中

4. 為什么 Object#wait 要寫在 while(condition) 循環中?

  • 規避虛假喚醒導致的問題,虛假喚醒是指線程可能在未調用 notify/notifyAll、未被中斷和等待超時的情況下被意外喚醒,所以 wait 要寫在 while(condition) 循環中,保證在發生虛假喚醒時程序的正確性

5. 如何正確的中斷線程?

  • 調用 Thread#interrupt 方法給線程發送中斷信號,線程中通過 Thread#isInterrupted 方法判斷是否被中斷,若被中斷則停止當前執行任務
  • 線程中通過 Thread#sleep 或 BlockingQueue#put 等方法休眠時,若被中斷則會拋出 InterruptedException 異常并清除中斷標記位,所以要捕獲處理此異常,或再調用 Thread#interrupt 標記中斷使后續代碼能處理中斷
  • 使用 volatile 標記位變量中斷線程是錯誤的,因為不能中斷 Thread#sleep 或 BlockingQueue#put 等方法進入的休眠狀態

6. Object#wait 和 Thread#sleep 方法的異同?

  • 相同點:都可以讓線程阻塞;都可以響應線程中斷
  • 區別:wait 方法必須寫在 synchronized 代碼塊;wait 方法會主動釋放 monitor 鎖;sleep 方法必須傳入 timeout 參數

二、線程池

1. 使用線程池相比手動創建線程有什么優點?

  • 頻繁創建線程系統開銷大,而線程池可用一些固定的工作線程反復執行任務,避免頻繁創建線程
  • 過多線程會占用過多內存,而線程池可以控制線程的總數量,避免占用過多內存資源
  • 線程池可更方便的統籌管理任務執行和線程,避免手動創建線程難管理、難統計的問題

2. 線程池各個參數的含義?

  • corePoolSize 核心線程數:常駐的工作線程,初始化時核心線程數默認為 0,創建后不會被銷毀
  • maximumPoolSize 最大線程數:當 workQueue 存放滿時,線程池會進一步創建線程,可創建的最多數量為 maximumPoolSize
  • keepAliveTime/TimeUnit 空閑線程存活時間:當大于 corePoolSize 部分的線程空閑超過存活時間后,會被回收
  • threadFactory 用來創建線程的線程工廠:方便給線程自定義命名以及線程優先級
  • workQueue 存放任務的阻塞隊列:當線程數超過 corePoolSize 后,會將任務存放到 workQueue 中等待執行
  • handler 任務被拒絕時的處理:當線程池已 shutdown 關閉或線程數達到 maximumPoolSize 時新提交的任務會被拒絕
  • 注意:當 workQueue 為無界隊列時, maximumPoolSize 參數其實不會被用到,是沒意義的

3. 線程池的四種拒絕策略?

  • AbortPolicy:拋出 RejectedExecutionException 異常,可根據業務進行重試等操作
  • DiscardPolicy:直接丟棄新提交的任務,不做其他反饋,有任務丟失風險
  • DiscardOldestPolicy:如果線程池未關閉,就丟棄隊列中存活時間最長的任務,但不做其他反饋,有任務丟失風險
  • CallerRunsPolicy:如果線程池未關閉,就在提交任務的線程直接開始執行任務,任務不會被丟失,由于阻塞了提交任務的線程,相當于提供了負反饋

4. 有哪 6 種常見的線程池?

  • FixedThreadPool:固定線程數的線程池,核心線程數與最大線程數相同,任務存放隊列為無界阻塞隊列(LinkedBlockingQueue)
  • CachedThreadPool:可緩存線程池,核心線程數為 0,最大線程數為 Integer.MAX_VALUE,任務存放隊列為中轉阻塞隊列(SynchronousQueue)
  • SingleThreadExecutor:單工作線程線程池,核心線程數為 1,任務存放隊列為無界阻塞隊列(LinkedBlockingQueue)
  • ScheduledThreadPool:定時或周期性任務線程池,任務存放隊列為無界優先級阻塞隊列(DelayedWorkQueue)
  • SingleThreadScheduledExecutor:定時或周期性任務單工作線程線程池,核心線程數為 1,任務存放隊列為無界優先級阻塞隊列(DelayedWorkQueue)
  • ForkJoinPool:適合執行可以產生并行子任務的任務,可方便的分裂(Fork)成子任務執行并匯總(Join)結果,任務存放隊列為 WorkQueue,除了公用隊列外,每個線程還有一個獨立的隊列來存放任務

5. 線程池常用的阻塞隊列有哪些?

  • LinkedBlockingQueue 無界阻塞隊列:任務隊列容量為 Integer.MAX_VALUE,永遠不會放滿,所以對應線程池只會創建核心線程數量的工作線程,而最大線程數參數對線程池來說沒有意義,因為并不會觸發生成多于核心線程數的線程
  • SynchronousQueue 中轉阻塞隊列:不存放任務,一旦有任務被提交就直接轉發給線程或者創建新線程來執行
  • DelayedWorkQueue 無界優先級阻塞隊列:內部采用堆數據結構,按照延遲時間長短對任務進行排序,ScheduledThreadPool 和 SingleThreadScheduledExecutor 選擇 DelayedWorkQueue,正是因為它們本身是基于時間執行任務的,而延遲隊列正好可以把任務按時間進行排序,方便任務的執行
  • ArrayBlockingQueue 有界隊列:任務隊列容量可配置,結合最大線程數與拒絕策略可有效的規避資源被耗盡的風險

6. 為什么不建議使用常見的線程池?

  • FixedThreadPool 和 SingleThreadExecutor 任務存放隊列為無界隊列(LinkedBlockingQueue),任務過多時會占用大量內存并導致 OOM
  • CachedThreadPool 雖然不存儲任務,但線程數沒有上限,任務過多時會創建非常多的線程,導致超過線程數量上限或 OOM
  • ScheduledThreadPool 和 SingleThreadScheduledExecutor 任務存放隊列為無界隊列(DelayedWorkQueue),任務過多時會占用大量內存并導致 OOM
  • 手動創建可以根據業務選擇合適的線程數量,制定拒絕策略,避免資源耗盡的風險

7. 合適的線程數量是多少?

  • CPU 密集型任務無需設置過多線程數,因為此類任務需占用大量 CPU 資源,設置過多線程數會導致多個線程都去搶占 CPU 資源,產生不必要的上下文切換,從而造成整體性能下降
  • IO 密集型任務可設置較多線程數,因為此類任務 IO 操作較耗時,但不會占用太多 CPU 資源,設置過少線程數會導致 CPU 資源空閑,導致 CPU 資源的浪費
  • 所以 CPU 耗時所占比例越高,就需要越少的線程;IO 耗時所占比例越高,就需要越多的線程
  • 通用公式:線程數 = CPU 核心數 * (1 + IO 耗時/CPU 耗時)
  • 例如 8 核機器執行一個 CPU 耗時 5ms,DB 耗時 100ms 的任務,線程數 = 8*(1+100/5) = 168 個
  • QPS(req pre second) 即一秒可執行次數,上例中 QPS = 168(1000/105) = 1600 。若 DB 最大 QPS 限制為 1000,則按比例減少線程數為 168(1000/1600) = 105 個
  • 如果不同任務的 CPU 耗時和 IO 耗時各不相同,可對所有任務的 CPU 耗時和 IO 耗時求個平均值進行計算;

8. 如何正確的關閉線程池?

  • shutdown():調用后會在執行完正在執行任務和隊列中等待任務后才徹底關閉,并會根據拒絕策略拒絕后續新提交的任務
  • shutdownNow():調用后會給正在執行任務線程發送中斷信號,并將任務隊列中等待的任務轉移到一個 List 中返回,后續會根據拒絕策略拒絕新提交的任務
  • isShutdown():判斷是否開始關閉線程池,即是否調用了 shutdown() 或 shutdownNow() 方法
  • isTerminated():判斷線程池是否真正終止,即線程池已關閉且所有剩余的任務都執行完了
  • awaitTermination():阻塞一段時間等待線程池終止,返回 true 代表線程池真正終止否則為等待超時

9. 線程池線程復用的原理?

  • 線程池將線程和任務解耦,一個線程可以從任務隊列中獲取多個任務執行
  • 關鍵類為 ThreadPoolExecutor 內部的 Worker 類,對應于一個線程,其內部會從任務隊列中獲取多個任務執行

三、各種各樣的鎖

1. 悲觀鎖/樂觀鎖

  • 悲觀鎖指在操作同步資源前必須先拿到鎖;而樂觀鎖利用 CAS 理念,在不獨占資源的情況下對資源進行修改
  • 悲觀鎖適合用于并發寫入多、臨界區代碼復雜、競爭激烈等場景,這種場景下悲觀鎖可以避免大量的無用的反復嘗試等消耗
  • 樂觀鎖適用于大部分是讀取,少部分是修改的場景,也適合雖然讀寫都很多,但是并發并不激烈的場景。在這些場景下,樂觀鎖不加鎖的特點能讓性能大幅提高

2. 可重入鎖/非可重入

  • 可重入是如果指線程已經持有鎖,則能在不釋放這把鎖的情況下,再次獲取這把鎖
  • Java 中的 ReentrantLock 和 synchronized 都是可重入鎖

3. 共享鎖/獨占鎖

  • 共享鎖指同一把鎖可以同時被多個線程獲取,而獨占鎖指一把鎖只能同時被一個線程獲取
  • ReentrantReadWriteLock 的讀鎖就是共享鎖,可以同時被多個線程讀??;寫鎖則為獨占鎖,同時只能被一個線程寫

4. 自旋鎖/非自旋鎖

  • 自旋是指拿不到鎖時不陷入阻塞,而是循環嘗試獲取鎖
  • 自旋鎖適用于并發度不是特別高的場景,以及臨界區比較短小的情況,這樣我們可以利用避免線程切換來提高效率
  • 如果臨界區很大,線程一旦拿到鎖,很久才會釋放的話,那就不合適用自旋鎖,因為自旋會一直占用 CPU 卻無法拿到鎖,白白消耗資源

5. 公平鎖/非公平鎖

  • 公平鎖是指各個線程公平平等,排隊獲取鎖時等待的時間越長就會優先獲取到鎖,
  • 非公平鎖是指線程可能存在插隊現象,比如一個阻塞等待中的線程 A 和新來的線程 B 同時競爭一把鎖時線程 B 會插隊先獲取到鎖
  • 非公平鎖整體執行速度為什么能更快:如上例,喚醒線程是需要耗時的,與其漫長的等待喚醒 A,不如直接先讓 B 插隊執行,這樣可以跳過 B 阻塞、喚醒的狀態切換
  • 非公平鎖的優缺點:整體執行速度更快、吞吐量更大,但可能產生線程饑餓導致某個線程長時間得不到執行

6. 可中斷鎖/不可中斷鎖

  • 可中斷指等待獲取鎖時可被中斷從而取消等待;synchronized 是不可中斷鎖

7. 偏向鎖/輕量級鎖/重量級鎖

  • 特指 synchronized 鎖的幾種狀態
  • 鎖的升級路徑:無鎖->偏向鎖->輕量級鎖->重量級鎖
  • 偏向鎖:當一個線程第一次嘗試獲取某個對象的鎖時,僅記錄這個線程為偏向鎖的擁有者,后續獲取鎖時如果是同個線程,就可以直接獲取鎖,開銷很小,當多線程發生實際競爭時會升級為輕量級鎖
  • 輕量級鎖:線程會通過自旋的方式嘗試獲取鎖(自旋鎖),不會阻塞,開銷較小,當鎖競爭時間較長時會膨脹為重量級鎖
  • 重量級鎖:利用操作系統同步機制實現,會讓線程進入阻塞狀態,開銷較大

8. JVM 對 synchronized 鎖做了哪些優化?

  • 鎖的升級:無鎖->偏向鎖->輕量級鎖->重量級鎖
  • 鎖消除:虛擬機編譯時,對一些代碼上使用 synchronized 同步,但是被檢測到不可能存在共享數據競爭的鎖進行削除
  • 鎖粗化:把不間斷、高頻鎖的請求合并成一個請求,以降低短時間內大量鎖請求、同步、釋放帶來的性能損耗

四、并發容器

1. Vector/HashTab

  • 內部使用 synchronized 方法級別的鎖保證線程安全,鎖的粒度比較大
  • 在并發量高的時候很容易發生競爭,并發效率比較低

2. ConcurrentHashMap

  • Java7 中基于普通的 HashMap 數組+鏈表結構,采用分段鎖的機制
  • Java8 中基于數組+鏈表+紅黑樹結構,采用 CAS + synchronized 同步機制
  • 紅黑樹相比鏈表可以提高查找效率,復雜度為 O(log(n))
  • 為什么鏈表長度大于 8 時轉換為紅黑樹?如果 hashCode 分布離散良好、鏈表符合泊松分布,那鏈表長度為 8 的概率小于千萬分之一,紅黑樹更多的是一種保底策略,用來保證 hash 算法異常等極端情況下的查詢效率
  • 為什么不采取僅數組+紅黑樹的結構?紅黑樹節點相比鏈表占用內存約大一倍,而鏈表較短時查找也很快,所以優先采取鏈表結構

3. CopyOnWriteArrayList

  • 基于 CopyOnWrite 機制,寫入時會先創建一份副本,寫完副本后直接替換原內容
  • 優點:比讀寫鎖更近一步,只需寫寫互斥,讀取不用加鎖,對于讀多寫少的場景可以大幅提升性能
  • 缺點:寫入時存在創建副本開銷及副本所多占的內存,讀寫不互斥可能會導致數據無法及時保持同步

五、原子類

1. 基本類型原子類

  • 包括 AtomicInteger、AtomicLong、AtomicBoolean
  • 提供了基本類型的 getAndSet、compareAndSet 等原子操作
  • 底層基于 Unsafe#compareAndSwapInt、Unsafe#compareAndSwapLong 等實現

2. 數組類型原子類

  • 包括 AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
  • 數組里的元素都可以保證其原子性,相當于把基本類型原子類聚合起來,組合成一個數組

3. 引用類型原子類

  • 包括 AtomicReference、AtomicStampedReference、AtomicMarkableReference
  • 用于讓一個對象保證原子性,底層基于 Unsafe#compareAndSwapObject 等實現
  • AtomicStampedReference 是對 AtomicReference 的升級,在此基礎上加了時間戳,用于解決 CAS 的 ABA 問題

4. 升級類型原子類

  • 包括 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
  • 對于非原子的基本或引用類型,在不改變其原類型的前提下,提供原子更新的能力
  • 適用于由于歷史原因改動成本太大或極少情況用到原子性的場景

5. 累加器

  • 包括 LongAdder、DoubleAdder
  • 相比于基本類型原子類,累加器沒有 compareAndSwap、addAndGet 等方法,功能較少
  • 設計原理:將 value 分散到一個數組中,不同線程只針對自己命中的槽位進行修改,減小高并發場景的線程競爭概率,類似于 ConcurrentHashMap 的分段鎖思想
  • 可解決高并發場景 AtomicLong 的過多自旋問題

6. 積累器

  • 包括 LongAccumulator、DoubleAccumulator
  • 是 LongAdder、DoubleAdder 的功能增強版,提供了自定義的函數操作

7. 原子類與鎖

  • 都是為了保證并發場景下線程安全
  • 原子類粒度更細,競爭范圍為變量級別
  • 原子類效率更高,底層采取 CAS 操作,不會阻塞線程
  • 原子類不適用于高并發場景,因為無限循環的 CAS 操作會占用 CPU

8. 原子類與 volatile

  • volatile 具有可見性和有序性,但不具備原子性
  • volatile 修飾 boolean 類型通常保證線程安全,因為賦值操作具有原子性
  • volatile 修飾 int 類型通常無法保證線程安全,因為 int 類型的計算操作需要讀取、修改、賦值回去,不是原子操作,這時需要使用原子類

六、Java 內存模型

1. 內存結構與內存模型

  • 內存結構描述了 JVM 運行時內存區域結構,包括:堆、方法區、虛擬機棧、本地方法棧、程序計數器、運行時常量池
  • 內存模型(JMM)是和多線程相關的一組規范,與 Java 并發編程有關

2. 主內存和工作內存

  • CPU 有多級緩存,會存在數據不同步的情況,JMM 屏蔽了 CPU 緩存的底層細節,抽象為主內存和工作內存
  • 工作內存中存在一份主內存數據的副本,每個線程只能接觸工作內存,無法直接操作主內存

3. 內存可見性

  • 指一個線程修改了工作內存的值后,其他線程能正確感知到最新的值
  • 滿足于 happens-before 關系的原則具備可見行,比如單線程、volatile、鎖同步等規則

4. 指令重排序

  • 編譯器、JVM 或者 CPU 都有可能出于優化等目的,對于實際指令執行的順序進行調整,這就是重排序
  • volatile 具備禁止重排序的特性
  • 單例模式的雙重檢查模式需要添加 volatile 修飾,規避指令重排序導致的對象引用判斷不為 null,但對象仍未初始化完的問題

七、線程協作

1. Semaphore

  • 通過控制許可證的發放和歸還實現統一時刻可執行某任務的最大線程數
  • 信號量可以被 FixedThreadPool 代替嗎?不能,信號量具有可跨線程、跨線程池的特性,相比 FixedThreadPool 更靈活,更適合于限制并發訪問的線程數

2. CountDownLatch

  • 用于并發流程控制,等到一個設定的數值達到之后,才能開始執行
  • 不可重用,若已完成倒數,則不能再重置使用

3. CyclicBarrier

  • 與 CountDownLatch 類似,都能阻塞一個或一組線程,直到某個預設的條件達成,再統一出發
  • CountDownLatch 作用于一個線程,CountDownLatch 作用于事件
  • 可重用,若已達成條件,可重置繼續使用
  • 可定義條件達成后的自定義執行動作

八、AQS 框架

1. AQS 及存在的意義?

  • AQS 是一個用于構建鎖、同步器等線程協作工具類的框架,即 AbstractQueuedSynchronizer 類
  • ReentrantLock、Semaphore、CountDownLatch 等工具類的工作都是類似的,AQS 就是這些類似工作提取出來的公共部分,比如閥門功能、調度線程等
  • AQS 可以極大的減少上層工具類的開發工作量,也可以避免上層處理不當導致的線程安全問題

2. AQS 內部的關鍵原理

  • state 值:AQS 中具有一個 int 類型的 state 變量,在不同工具類中代表不同的含義,比如在 Semaphore 中代表剩余許可證的數量;在 CountDownLatch 中代表需要倒數的數量;在 ReentrantLock 中代表鎖的占有情況,0 代表沒被占有,1 代表被占有,大于 1 代表同個線程重入了
  • FIFO 隊列:用于存儲、管理等待的線程
  • 獲取、釋放鎖:需工具類自行實現,比如 Semaphore#acquire、ReentrantLock#lock 為獲??; Semaphore#release、ReentrantLock#unlock 為釋放
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。