讀書筆記 | Java併發編程實戰
1、基礎知識
1. 線程安全性
線程安全的代碼,核心在於對狀態訪問操做的管理特別是共享和可變狀態的管理html
- 對象的狀態:存儲在狀態變量(如實例或靜態域)中的數據
- 共享意味着變量能夠由多個線程同時訪問
- 可變意味着變量的值會發生改變
當多個線程訪問某個狀態變量而且其中有一個線程執行寫入操做時,必須採用同步機制來協同這些線程對變量的訪問.
Java常見的同步機制:
* synchronized
* volatile
* 顯示鎖(Explicit Lock)
* 原子變量java
2. 什麼是線程的安全性
線程的安全性就是當多個線程訪問某個類時,這個類始終都能表現出正確的行爲,那麼這個類就是線程安全的.編程
3. 非原子的64位操做
- Java內存模型要求,變量的讀取和寫入操做都必須是原子操做,但對於非volatile類型的long和double變量,JVM容許將64位的讀操做或寫操做分爲兩個32位的操做.當讀取一個非volatile類型的long變量時,若是對該變量的讀操做和寫操做在不一樣的線程中執行,那麼極可能會讀到某個值的高32位和另外一個值得低32位.所以,即便不考慮失效數據問題,在多線程中使用共享且可變的long和double等類型的變量也是不安全的,除非用關鍵字volatile來聲明它們,或者用鎖保護起來
- 如何復現問題,64位系統上也會有這種問題嗎
- 64位系統中沒有這個問題
4. volatile
- volatile變量用來確保將變量的更新操做通知到其餘線程.當把變量聲明爲volatile類型後,編譯器和運行時都會注意到這個變量是共享的,所以不會將該變量上的操做與其餘內存操做一塊兒重排序.volatile變量不會被緩存在寄存器或者對其餘處理器不可見的地方.所以在讀取volatile變量時,老是返回最新寫入的值.
- volatile變量對可見性的影響:
- 當線程A首先寫入一個volatile變量而且線程B隨後讀取該變量時,在寫入volatile變量以前全部對A可見的全部變量的值,在B讀取了volatile變量後,對B也是可見的.
- volatile變量的正確使用方式包括
- 確保它們自身狀態的可見性
- 確保它們所引用對象的狀態的可見性
- 做爲一些事件的開關.
5. 發佈與逸出
- 發佈:
- 將一個指向該對象的引用保存到其餘代碼能訪問的地方
- 或者在一個非私有的方法中返回該引用
- 或者將一個引用傳遞到其餘類的方法中
- 逸出:
- 當某個不應發佈的對象被髮布時,這種狀況就是逸出
6. 併發容器
6-1. ConcurrentHashMap
- 內部結構
- add
- get
- size
6-2. CopyOnWriteArrayList
- 用途:在一些讀操做遠大於寫操做的狀況下,纔可使用寫入時複製容器
- 在事件通知系統中,在分發通知時,須要迭代已註冊的監聽器鏈表,在大多數狀況下,註冊和註銷事件監聽器的操做遠小於接收事件的操做.
6-3. 阻塞隊列和生產者消費者模式
- 阻塞隊列提供了可阻塞的put和take方法,以及支持定時的offer和poll方法.
6-4. 同步工具類
6-4-1. 閉鎖
- 閉鎖是一種同步工具類,能夠延遲線程的進度知道其達到終止狀態.
- 閉鎖能夠用來確保某些活動直到其餘活動都完成後才繼續執行
- 確保某個計算所須要的資源都初始化後纔開始執行(資源初始化)
- 確保某個服務所依賴的其餘服務都啓動後才啓動(服務依賴)
- 確保某個操做的全部操做者都就緒後再繼續執行
- CountDownLatch
- CountDownLatch(int)
- await():void
- await(long,TimeUnit):boolean
- countDown():void
// 在計時測試中使用CountDownLatch來啓動和中止線程 public long timeTasks(int nThreads, Runnable task) throws InterruptedException { final CountDownLatch startGate = new CountDownLatch(1); final CountDownLatch endGate = new CountDownLatch(nThreads); for (int i = 0; i < nThreads; i++) { Thread t = new Thread(() -> { try { //線程啓動後都在這裏等待startGate變爲0 startGate.wait(); try { task.run(); } finally { //任務運行完,endGate減一 endGate.countDown(); } } catch (InterruptedException e) { e.printStackTrace(); } }); t.start(); } long start = System.nanoTime(); startGate.countDown(); //全部線程開始任務 endGate.wait(); //等待全部線程執行完成 long end = System.nanoTime(); return end - start; }
6-4-2. FutureTask
- FutureTask實現了Future語義,表示一種抽象的可生成結果的計算.FutureTask表示的計算是經過Callable來實現的,至關於一種可生成結果的Runnable,而且能夠處於一下三種狀態:等待運行,正在運行,運行完成.運行完成表示計算的全部可能結束方式,包括正常結束,因爲取消而結束和因爲異常而結束等.當FutureTask進入完成狀態後,它會永遠中止在這個狀態上.
- FutureTask的用途:
- 在Executor框架中表示異步任務
- 還能夠用來表示一些時間較長的計算,這些計算能夠在使用計算結果以前啓動.經過提早啓動計算,能夠減小等待結果時須要的時間.
- FutureTask的問題
- Callable表示的任務能夠拋出受檢查的或不受檢查的異常,這些異常被封裝到
ExecutionException
中,並在Future.get中被從新拋出
,這將使得調用get的代碼變得複雜,由於它要對不一樣的異常進行不一樣的處理.
- Callable表示的任務能夠拋出受檢查的或不受檢查的異常,這些異常被封裝到
// 使用FutureTask來提早加載稍後須要的數據 public class N5_5_12Proloader { private final FutureTask<ProductInfo> future = new FutureTask<>(ProductInfo::new); private final Thread thread = new Thread(future); public void start() { thread.start(); } public ProductInfo get() throws InterruptedException { try { return future.get(); } catch (ExecutionException e) { System.out.println("初始化ProductionInfo發生錯誤"); return null; } } } class ProductInfo { }
6-4-3. 信號量Semaphore
- 計數信號量用來控制同時訪問某個特定資源的操做數量,或者同時執行某個操做的數量.計數信號量還能夠用來實現某種資源池,或者對容器施加邊界.
- Semaphore
public Semaphore(int permits)
public Semaphore(int permits, boolean fair)
public void acquire() throws InterruptedException
public void release()
6-4-4. 柵欄CyclicBarrier
- CyclicBarrier
public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)
public int await()
- 等到設定的n個線程都到達了指定位置後在再同時繼續往下執行,CyclicBarrier在初始化時還能夠設置Runnable的action,最後一個到達指定位置的線程會去運行這個action
6-5. 構建高效且可伸縮的結果緩存
- 場景:假設有個函數 value = fun(key),這個計算過程須要消耗必定的時間和資源,如今想要將計算的結果緩存下來,下次再計算同一個key時能夠從緩存中直接獲取value.
- 思路:可使用map類將key和value緩存起來,每次計算key的值時,先看map中有沒有這個key對應的value,若是有,直接返回,若是沒有,計算結果並存入map中.
- 其中的坑:
- 涉及到多線程,要使用ConcurrentHashMap,確保get和set時的線程安全
- 由於計算須要消耗必定時間,若是一個線程在計算key的時候,另外一個線程也來請求計算key,這個時候由於第一個線程的計算結果沒出來,因此map中是空的,這時候第二個線程會再去計算.
- 解決辦法:map中不保存key和value的鍵值對,而是保存key和Future,其中Future中在計算value的值,經過future.get()方法,若是計算完成了直接返回value的值,若是計算還沒結束,會阻塞一直等到它計算完成並返回.還須要注意的是,須要使用map.putIfAbsent(key,future)方法存入key和future,由於判斷key是否存在和放入key不是原子操做.