《java併發程式設計的藝術》併發容器和框架

sayWhat_sayHello發表於2018-07-23

ConcurrentHashMap

HashMap在併發執行put操作時會引起死迴圈,是因為多執行緒會導致HashMap的Entry列表形成環形資料結構,一旦形成環形資料結構,Entry的next結點永遠不為空,產生死迴圈獲取Entry.

HashTable使用synchronized來保證執行緒安全,因此效率低下。

ConcurrentHashMap使用鎖分段技術:首先將資料分成一段一段儲存,然後給每一段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他執行緒訪問。

結構

ConcurrentHashMap由Segment陣列結構和HashEntry陣列結構組成。Segment是可重入鎖,扮演鎖的角色;HashEntry儲存鍵值對資料。
一個Segment包含一個HashEntry陣列,每個HashEntry是一個連結串列結構的元素,每個Segment守護一個HashEntry陣列裡的元素,當對HashEntry陣列的資料進行修改的時候,必須首先獲得與它對應的Segment鎖。

初始化

ConcurrentHashMap的初始化方法是通過initialCapacity,loadFactor,concurrencyLevel等幾個引數來初始化segment陣列,段偏移量segmentShift,段掩碼segmentMask和每個segment裡的HashEntry陣列來實現。預設情況下concurrencyLevel等於16.

初始化segment:segment陣列的長度ssize是大於或等於concurrentLevel的2的N次方值。
初始化segmentShift和segmentMask:sshift等於ssize從1向左移位的次數。預設情況下等於1需要向左位移4次,所以sshift=4.segmentShift等於32-sshift,segmentMask=ssize-1;
初始化每個segment:同樣的initialCapacity,loadFactor.segment裡HashEntry陣列的長度cap=initialCapacity/ssize*c;c是倍數,如果c>1,取c的2的N次方值。所以cap不是1就是2的N次方。segment的容量threshold = (int)cap x loadFactor.

綜上預設情況下,initialCapacity=16,loadFactor=0.75,concurrencyLevel=16。對應的ssize=16;sshift=4;segmentShift=28;segmentMask=15;cap=1;threshold=0。

定位Segment

分段鎖Segment保護不同段的資料,那麼在插入和獲取ConcurrentHashMap元素的時候,必須先通過雜湊演算法定位到Segment.
ConcurrentHashMap會對元素的hashcode進行二次hash,以減少hash衝突、

操作

  • get,get過程不需要加鎖,只有值為空值的時候才加鎖重讀。內部value定義為volatile。
  • put,put過程必須加鎖,首先定位到Segment,然後在segment進行插入操作。第一步判斷是否需要對Segment裡的HashEntry陣列進行擴容,第二步定位新增元素的位置,然後將其放到HashEntry陣列裡。
  • size,先嚐試2次不鎖住Segment的方式統計各個Segment大小,如果統計過程中count發生了變化,對所有的Segment的put、remove、clean進行加鎖再統計。

ConcurrentLinkedQueue

實現一個執行緒安全的佇列有兩種方式:
- 使用阻塞方法:用一個鎖(入隊和出隊用同一把鎖)或者用兩個鎖(入隊和出隊用不同的鎖)等方式實現。
- 使用非阻塞的方法:使用迴圈CAS。

ConcurrentLinkedQueue是一個基於連結結點的無界執行緒安全佇列,採用FIFO對結點進行排序。

阻塞佇列

阻塞佇列是支援兩個附加操作的佇列,插入and移除方法:
- 插入:佇列滿時不進行插入
- 移除:佇列空時不進行移除

ArrayBlockingQueue

用陣列實現的有界阻塞佇列

LinkedBlockingQueue

用連結串列實現的有界阻塞佇列,預設最大長度為Integer.MAX_VALUE.

PriorityBlockingQueue

支援優先順序的無界阻塞佇列。預設情況下使用自然順序排序,可以自定義類實現compareTo方法或初始化時特定構造引數Comparator。需要注意的是不能保證同優先順序元素的順序。

DelayQueue

支援延時獲取元素的無界阻塞佇列。佇列使用PriorityQueue實現。佇列中的元素必須實現Delayed介面,在建立元素時可以指定多久才能從佇列中獲取當前元素,只有在延遲期滿時才能從佇列中獲取元素。

SynchronousQueue

不儲存元素的阻塞佇列。每一個put必須等待一個take操作,否則不能繼續新增元素。

LinkedTransferQueue

由連結串列結構構成的無界阻塞佇列。實現了tryTransfer和transfer操作。tryTransfer方法是用來試探傳入的元素是否能直接傳給消費者。如果沒有消費者等待接收元素,返回false。transfer等待消費者消費後返回。

LinkedBlockingDeque

LinkedBlockingDeque是一個由連結串列結構組成的雙向阻塞佇列。

Fork/Join框架

將大任務fork成小任務,最後Join各個任務的結果。

ForkJoinTask:
RecursiveAction:用於沒有返回結果的任務。
RecursiveTask:用於有返回結果的任務。

ForkJoinPool:ForkJoinTask要通過ForkJoinPool執行。

舉例,計算1+2+3+4:

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;

public class CountTask extends RecursiveTask<Integer> {

    public static final int THRESHOLD = 2;
    private int start;
    private int end;

    public CountTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;

        boolean canCompute = (end-start) <= THRESHOLD;
        if (canCompute) {
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        }else {
            int middle = (start+end)/2;
            CountTask leftTask = new CountTask(start, middle);
            CountTask rightTask = new CountTask(middle + 1, end);
            leftTask.fork();
            rightTask.fork();
            int leftResult = leftTask.join();
            int rightResult = rightTask.join();

            sum = leftResult+rightResult;
        }
        return sum;
    }

    public static void main(String[] args){
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        CountTask task = new CountTask(1, 4);
        Future<Integer> result = forkJoinPool.submit(task);
        try {
            System.out.println(result.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

步驟:
1. 類繼承RecursiveTask
2. 類實現方法compute()
3. 主執行緒呼叫

Fork/Join框架的異常處理:
ForkJoinTask在執行時可能拋異常但是我們在主執行緒無法直接捕獲異常,所以ForkJoinTask提高了isCompletedAbnormally()方法來檢查任務是否已經丟擲異常或已經被取消了,
並且可以通過getException方法獲取異常。

工作竊取演算法

工作竊取演算法是指某個執行緒從其他佇列裡竊取任務來執行。先幹完自己佇列任務的執行緒幫其他執行緒幹活。常用雙端佇列。

相關文章