讀書筆記 | Java併發編程實戰

讀書筆記 | 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的代碼變得複雜,由於它要對不一樣的異常進行不一樣的處理.
// 使用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不是原子操做.

2、結構化併發引應用程序

1. 任務執行

相關文章
相關標籤/搜索