併發容器和框架(Java併發編程的藝術筆記)

ConcurrentHashMap的實現原理與使用

線程不安全的HashMap

在多線程環境,使用HashMap進行put操作會引起死循環,原因是多線程會導致HashMap的Entry鏈表形成環狀數據結構,進而Entry的next節點永不會空,產生死循環獲取Entry。

效率低下的HashTable

HashTable容器使用synchronized來保證線程安全,但在競爭激烈的情況下,當一個線程訪問HashTable的同步方法,其他線程嘗試訪問HashTable的同步方法時,會進入阻塞狀態。例如線程1使用put操作時,線程2不能使用put方法和get方法,因此效率低。

ConcurrentHashMap的鎖分段技術

HashTable容器效率低的原因是所有線程都必須爭奪同一個鎖。假如容器裏有多把鎖,將數據分稱一段一段的存儲,給每一段數據配一把鎖,那麼線程間就不會存在鎖競爭。這就是ConcurrentHashMap的鎖分段技術。

 

ConcurrentHashMap的結構

ConcurrentHashMap是由Segment數組結構和HashEntry數組結構組成。Segment是一種可重入鎖(ReentrantLock),它扮演鎖的角色;HashEntry則用於存儲鍵值對數據。

一個ConcurrentHashMap裏包含一個Segment數組。Segment是一種數組和鏈表結構。一個Segment裏包含一個HashEntry數組,每個HashEntry是一個鏈表結構的元素。當對HashEntry數組的數據進行修改時, 必須先獲得與它對應的Segment鎖,

 

ConcurrentHashMap相關參數的初始化

1.初始化segments數組

以下是初始化segments數組源代碼,segments數組長度ssize是通過concurrencyLevel計算得出的爲了能 通過按位與的散列算法來定位segments數組的索引,必須保證segments數組的長度是2的N次方,因此需要計算一個大於或等於concurrencyLevel的最小的2的N次方值 來作爲segments數組的長度。

if (concurrencyLevel > MAX_SEGMENTS)
   concurrencyLevel = MAX_SEGMENTS;
    int sshift = 0;
    int ssize = 1;
        while (ssize < concurrencyLevel) {
              ++sshift;
            ssize <<= 1;
        }
    segmentShift = 32 - sshift;
    segmentMask = ssize - 1;
    this.segments = Segment.newArray(ssize);

 

2.初始化段偏移量segmentShift和段掩碼segmentMask

這兩個變量需要在定位segment時的散列算法裏使用,sshift等於segments數組長度ssize從1向左移位的次數,比如默認情況下concurrencyLevel等於16,1需要向左移位移動4次,所以sshift等於4。

segmentShift用於定位參與散列運算的位數,segmentShift等於32減sshift,所以等於28,此處用32是因爲ConcurrentHashMap裏的hash()方法輸出的最大數是32位的。

segmentMask是散列運算的掩碼,等於ssize減1,即15。

3.初始化每個segment

輸入參數initialCapacity是ConcurrentHashMap的初始化容量,,loadfactor是每個segment的負載因子,在在構造方法裏需要通過這兩個參數來初始化數組中的每個segment。

下面代碼中的變量cap爲segment數組的HashEntry數組長度

if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = 1;
        while (cap < c)
            cap <<= 1;
        for (int i = 0; i < this.segments.length; ++i)
            this.segments[i] = new Segment<K,V>(cap, loadFactor);

 

定位Segment

在插入和獲取元素時,必須先通過散列算法定位到Segment。如下所示,ConcurrentHashMap會首先使用 Wang/Jenkins hash的變種算法對元素的hashCode進行一次再散列。再散列的原因是減少散列衝突,使元素能均勻分佈在不同的Segment中。

    private static int hash(int h) {
        h += (h << 15) ^ 0xffffcd7d;
        h ^= (h >>> 10);
        h += (h << 3);
        h ^= (h >>> 6);
        h += (h << 2) + (h << 14);
        return h ^ (h >>> 16);
    }

ConcurrentHashMap通過以下散列算法定位segment,默認情況下下segmentShift爲28,segmentMask爲15,再散列後的數最大是32位二進制數據。向右無符號移動28位(hash>>>segmentShift),意思是讓高4位參與到散列運算中。

final Segment<K,V> segmentFor(int hash) {
    return segments[(hash >>> segmentShift) & segmentMask];
}

 

ConcurrentHashMap的操作

get操作

如下是Segment的get操作。先經過一次再散列,然後使用這個散列值通過散列運算定位到Segment,再通過散列算法定位到元素

public V get(Object key) {
    int hash = hash(key.hashCode());
    return segmentFor(hash).get(key, hash);
}

可以看到,get方法是不加鎖的,因爲它將要使用的共享變量都定義成volatile類型,如用於統計當前 Segement大小的count字段和用於存儲值的HashEntry的value。它保證能被多線程同時讀,但不會讀到過期值,但只能被單線程寫(如果寫入的值不依賴於原值,則可以多線程寫)。在get操作裏,只需要讀而不寫共享變量count和value,因此可不用加鎖。

定位HashEntry和定位Segment的散列算法雖然一樣, 都與數組的長度減去1再相“與”,但是相“與”的值不一樣,定位Segment使用的是元素的 hashcode通過再散列後得到的值的高位,而定位HashEntry直接使用的是再散列後的值。其目的 是避免兩次散列後的值一樣。

hash >>> segmentShift) & segmentMask // 定位Segment所使用的hash算法
int index = hash & (tab.length - 1); // 定位HashEntry所使用的hash算法

 

put操作

put方法首先定位到Segment,然後在Segment裏進行插入操作。插入操作首先要判斷是否要對Segment裏的的HashEntry數組進行擴容;其次是定位添加元素的位 置,然後將其放在HashEntry數組裏。

(1)是否需要擴容

在插入元素前會先判斷Segment裏的HashEntry數組是否超過容量(threshold),如果超過,則對數組進行擴容。

(2)如何擴容

創建一個容量是原來容量兩倍的數組,然後將原數組裏的元素進 行再散列後插入到新的數組裏。,ConcurrentHashMap只對某個segment進行擴容。

 

size操作

統計元素的大小即統計所有Segment裏元素的大小count後求和。但考慮到累加前使用的count可能發生變化(機率非常小),所以ConcurrentHashMap的做法是先嚐試2次通過不鎖住Segment的方式來統計各個Segment大小,如果統計的過程中,容器的count發生了變化,則再採用加鎖的方式來統計所有Segment的大小。

ConcurrentHashMap通過modCount變量來判斷統計時容器是否發生變化:在put、remove和clean方法裏操作元素前都會將變量modCount進行加1,在統計size前後比較modCount是否發生變化即可得知。

 


ConcurrentLinkedQueue

併發編程有時需要使用線程安全的隊列,實現隊列的方式有兩種:

  • 阻塞算法:該隊列可以用一個鎖 (入隊和出隊用同一把鎖)或兩個鎖(入隊和出隊用不同的鎖)等方式來實現。
  • 非阻塞算法:循環CAS

ConcurrentLinkedQueue則屬於非阻塞方式,它是一個採用FIFO的無界線程安全隊列。

ConcurrentLinkedQueue的結構

隊列由head節點和tail節點,每個節點由節點元素和指向下一元素的引用組成。默認情況下head存儲元素爲空,tail節點等於head節點

private transient volatile Node<E> tail = head;

入隊列

入隊列過程

該過程分爲兩件事:

  1. 將入隊節點設置爲當前隊列尾節點的下一節點;
  2. 更新tail節點,若tail節點的next節點不爲空,則將入隊節點設置tail節點,否則將入隊節點設置爲tail的next節點。

如果是多線程同時進行入隊操作,則可能出現其他線程插隊的情況:例如一線程先獲取尾節點,正當它向設置尾節點時,另一個線程插隊,則隊列的尾節點就會發生變化。這是當前線程會需要暫停入隊操作,重新獲取尾節點。

    public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
// 入隊前,創建一個入隊節點
        Node<E> n = new Node<E>(e);
        retry:
// 死循環,入隊不成功反覆入隊。
        for (;;) {
// 創建一個指向tail節點的引用
            Node<E> t = tail;
// p用來表示隊列的尾節點,默認情況下等於tail節點。
            Node<E> p = t;
            for (int hops = 0; ; hops++) {
// 獲得p節點的下一個節點。
                Node<E> next = succ(p);
// next節點不爲空,說明p不是尾節點,需要更新p後在將它指向next節點
                if (next != null) {
// 循環了兩次及其以上,並且當前節點還是不等於尾節點
                    if (hops > HOPS && t != tail)
                        continue retry;
                    p = next;
                }
// 如果p是尾節點,則設置p節點的next節點爲入隊節點。
                else if (p.casNext(null, n)) {
/*如果tail節點有大於等於1個next節點,則將入隊節點設置成tail節點,
更新失敗了也沒關係,因爲失敗了表示有其他線程成功更新了tail節點*/
                    if (hops >= HOPS)
                        casTail(t, n); // 更新tail節點,允許失敗
                    return true;
                }
// p有next節點,表示p的next節點是尾節點,則重新設置p節點
                else {
                    p = succ(p);
                }
            }
        }
    }

 

定位尾節點

每次入隊都必須通過tail節點來找到尾節點,尾節點可能是tail節點,也可是tail節點的next節點。

獲取某節點p的next節點代碼如下,若p節點等於p的next節點,則只能是一種情況:p節點和p的next節點都爲空,表示該隊列剛初始化,正準備添加,因此需要返回head節點。

final Node<E> succ(Node<E> p) {
    Node<E> next = p.getNext();
    return (p == next) head : next;
}

 

設置入隊節點爲尾節點

p.casNext(null,n)方法(CAS算法)用於將入隊節點設置爲當前隊列尾節點的next節點,如果p是null, 表示p是當前隊列的尾節點,如果不爲null,表示有其他線程更新了尾節點,則需要重新獲取當前隊列的尾節點。

 

出隊列

該操作是從隊列裏返回一個節點元素,並清空該節點對元素的引用。

下圖是出隊列的例子,由圖可知,並不是每次出隊時都更新head節點,當head節點裏由元素時,直接彈出head節點裏的元素,而不會更新head節點。只有當head節點裏沒有元素時,出隊操作纔會更新head節點。

如下是出隊代碼。首先是獲取頭節點元素,判斷是否爲空。若爲空,則表示另外一個線程已進行一次出隊操作,將該節點元素拿走;若不爲空,則使用CAS方式將頭節點的引用設置爲nul,若CAS成功,則直接返回頭節點的元素;若不成功,表示另外一個線程已經 進行了一次出隊操作更新了head節點,導致元素髮生了變化,需要重新獲取頭節點。

    public E poll() {
        Node<E> h = head;
// p表示頭節點,需要出隊的節點
        Node<E> p = h;
        for (int hops = 0;; hops++) {
// 獲取p節點的元素
            E item = p.getItem();
// 如果p節點的元素不爲空,使用CAS設置p節點引用的元素爲null,
// 如果成功則返回p節點的元素。
            if (item != null && p.casItem(item, null)) {
                if (hops >= HOPS) {
// 將p節點下一個節點設置成head節點
                    Node<E> q = p.getNext();
                    updateHead(h, (q != null) q : p);
                }
                return item;
            }
// 如果頭節點的元素爲空或頭節點發生了變化,這說明頭節點已經被另外
// 一個線程修改了。那麼獲取p節點的下一個節點
            Node<E> next = succ(p);
// 如果p的下一個節點也爲空,說明這個隊列已經空了
            if (next == null) {
// 更新頭節點。
                updateHead(h, p);
                break;
            }
// 如果下一個元素不爲空,則將頭節點的下一個節點設置成頭節點
            p = next;
        }
        return null;
    }

 


阻塞隊列

(1)當隊列滿時,隊列會阻塞插入元素的線程,直到隊列不滿。

(2)在隊列爲空時,獲取元素的線程會等待隊列變爲非空。

當阻塞隊列不可用時,插入和移除操作提供了4種處理方式。

Java提供了7個阻塞隊列:

ArrayBlockingQueue

它一個由數組結構組成的有界阻塞隊列,按FIFO的原則對元素排序。

默認情況下不保證線程公平的訪問隊列,即不保證先阻塞線程先訪問隊列。如果希望保證公平(這會降低吞吐量),我們可以如下做:

ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);

其公平性是使用可重入鎖實現的

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
        this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull = lock.newCondition();
}

 

LinkedBlockingQueue

一個用鏈表實現的有界阻塞隊列。此隊列的默認和最大長度爲 Integer.MAX_VALUE。此隊列按照先進先出的原則對元素進行排序。

 

PriorityBlockingQueue

是一個支持優先級的無界阻塞隊列。默認情況下元素採取自然順序 升序排列。也可以自定義類實現compareTo()方法來指定元素排序規則,或者初始化 PriorityBlockingQueue時,指定構造參數Comparator來對元素進行排序。需要注意的是不能保證 同優先級元素的順序。

 

DelayQueue

一個支持延時獲取元素的無界阻塞隊列,在創建元素時可以指定多久才能從隊列中獲取當前元素,只有在延遲期滿時才能從隊列中提取元素。隊列使用PriorityQueue來實現。隊中的元素必須實現Delayed接口

(1)如何實現Delayed接口

參考ScheduledThreadPoolExecutor 裏ScheduledFutureTask類的實現。

第一步:在對象創建時,用time記錄當前對象延遲到什麼時候可以使用,使用sequenceNumber來標識元素在隊列中的先後順序。

ScheduledFutureTask(Runnable r, V result, long ns, long period) {
    super(r, result);
    this.time = ns;
    this.period = period;
    this.sequenceNumber = sequencer.getAndIncrement();
}

第二步:實現getDeley方法,該方法返回當前元素還需延時多長時間,單位爲納秒。

public long getDelay(TimeUnit unit) {
    return unit.convert(time - now(), TimeUnit.NANOSECONDS);
}

第三步:實現compareTo方法來指定元素的順序。例如,讓延時時間最長的放在隊列的末尾。

 public int compareTo(Delayed other) {
        if (other == this) // compare zero ONLY if same object
            return 0;
        if (other instanceof ScheduledFutureTask) {
            ScheduledFutureTask<> x = (ScheduledFutureTask<>)other;
            long diff = time - x.time;
            if (diff < 0)
                return -1;
            else if (diff > 0)
                return 1;
            else if (sequenceNumber < x.sequenceNumber)
                return -1;
            else
                return 1;
        }
        long d = (getDelay(TimeUnit.NANOSECONDS) -
                other.getDelay(TimeUnit.NANOSECONDS));
        return (d == 0) 0 : ((d < 0) -1 : 1);
    }

(2)如何實現延時阻塞隊列

當消費者從隊列裏獲取元素時,若元素沒有達到延時時間,則阻塞當前線程。

如下所示,leader變量是一個等待獲取隊列頭部元素的線程,若leader不爲空,表示已有線程在等待獲取隊列頭元素,使用await()方法讓當前線程等待;若爲空,則把當前線程設置爲leader,並用awaitNanos()方法讓當前線程等待接收信號或等待delay時間。

    long delay = first.getDelay(TimeUnit.NANOSECONDS);
    if (delay <= 0)
        return q.poll();
    else if (leader != null)
        available.await();
    else {
        Thread thisThread = Thread.currentThread();
        leader = thisThread;
        try {
            available.awaitNanos(delay);
        } finally {
        if (leader == thisThread)
            leader = null;
        }
    }

 

SynchronousQueue

一個不存儲元素的阻塞隊列。每一個put操作必須等待一個take操作, 否則不能繼續添加元素。隊列本身並不存儲任何元素,非常適合傳遞性場景。SynchronousQueue的吞吐量高於 LinkedBlockingQueue和ArrayBlockingQueue。

它支持公平訪問隊列。默認情況下線程非公平性訪問隊列。

 

LinkedTransferQueue

一個由鏈表結構組成的無界阻塞TransferQueue隊列。相對於其他阻塞隊列,LinkedTransferQueue多了tryTransfer和transfer方法。

(1)transfer方法

若當前有消費者正等待接受數據,該方法可以把生產者傳入的元素立即傳輸給消費者;若沒有等待的消費者,則將元素放在隊列的tail節點,並等到該元素被消費者消費了才返回。

該方法關鍵部分如下,第一行試圖把存放當前元素s作爲tail節點;第二行讓CPU自旋等待消費者消費元素。自旋一定的次數後使用Thread.yield()方法來暫停 當前正在執行的線程,並執行其他線程。

Node pred = tryAppend(s, haveData);
return awaitMatch(s, pred, e, (how == TIMED), nanos);

(2)tryTransfer方法

該方法用來試探生產者傳入的元素是否能直接傳給消費者。如果沒有消費者等 待接收元素,則返回false。

 

LinkedBlockingDeque

一個由鏈表組成的雙向阻塞隊列,所謂雙向隊列指的是可以 從隊列的兩端插入和移出元素。由於多了一個操作隊列的入口,在多線程同時入隊 時,也就減少了一半的競爭。

相比其他的阻塞隊列,LinkedBlockingDeque多了addFirst、 addLast、offerFirst、offerLast、peekFirst和peekLast等方法。

 

阻塞隊列的實現原理

(1)通知模式

 當生產者往往滿的隊列裏添加元素時會阻塞住生 產者,當消費者消費了一個隊列中的元素後,會通知生產者當前隊列可用。

下面是ArrayBlockingQueue的源碼,它使用了Condition來實現。

    private final Condition notFull;
    private final Condition notEmpty;

 public ArrayBlockingQueue(int capacity, boolean fair) {
// 省略其他代碼
        notEmpty = lock.newCondition();
        notFull = lock.newCondition();
    }
    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            insert(e);
        } finally {
            lock.unlock();
        }
    }
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return extract();
        } finally {
            lock.unlock();
        }
    }
    private void insert(E x) {
        items[putIndex] = x;
        putIndex = inc(putIndex);
        ++count;
        notEmpty.signal();
    }

await()方法主要通過LockSupport.park(this)實現,而繼續進入park()方法,會發現調用setBlocker先保存一下將要阻塞的線程,然後調用unsafe.park阻塞 當前線程。

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    unsafe.park(false, 0L);
    setBlocker(t, null);
}

unsafe.park是個native方法(由非java語言實現),這個方法會阻塞當前線程,只有如下4種情況的一種發生,該方法纔會返回:

  • 與park對應的unpark執行或已經執行時。“已經執行”是指unpark先執行,然後再執行park 的情況。
  • ·線程被中斷時。
  • ·等待完time參數指定的毫秒數時。
  • ·異常現象發生時,這個異常現象沒有任何原因。

 


Fork/Join框架 

 Fork/Join是一個用於並行執行任務的框架,是一個把大任務分割成若干 個小任務,最終彙總每個小任務結果後得到大任務結果的框架。

工作竊取算法 

假設我們需要做一個比較大的任務,可以將該任務分割爲若干互不依賴的子任務,爲了減少線程間競爭,將子任務放到不同隊列裏,併爲每個隊列創建一個單獨的線程來執行。但是,有的線程會先把隊列裏的任務做完,而其他線程對應的隊列裏還有任務等待處理。因此,幹完活的線程就會去其他線程的隊列裏竊取一個任務來執行。爲了減少竊取任務線程和被竊取線程之間的競爭,通常使用雙端隊列,被竊取的線程永遠從雙端隊列頭部拿任務執行,而竊取任務的線程永遠從雙端隊列的尾部拿任務執行。

缺點:在某些情況仍存在競爭,比如雙端隊列裏只有一個任務。並且該算法會消耗更多的系統資源,比如創建多個線程和多個雙端隊列。

 

Fork/Join框架的設計

Fork/Join使用兩個類來完成分割任務和執行任務併合並結果兩件事情:

ForkJoinTask:首先要創建一個ForkJoin任務,它提供任務中執行fork()和join()操作。通常我們需要繼承它的子類:

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

ForkJoinPool:ForkJoinTask需要通過ForkJoinPool來執行。任務分割的子任務會添加到當前工作線程維護的雙端隊列中,進入隊列的頭部。當一個工作線程的隊列裏暫時沒有任務時,它會隨機從其他工作線程的隊列的尾部獲取一個任 務。

 

使用Fork/Join框架

通過框架來計算1到4的累加和

public class Test {
    public static class CountTask extends RecursiveTask<Integer> {
        private static final int MAX_Num = 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) <= MAX_Num;
            if(canCompute) {
                for(int i = start; i <= end; i++) {
                    sum += i;
                }
            }else {                 //任務分割
                int mid = (start + end) / 2;
                CountTask leftTask = new CountTask(start, mid);
                CountTask rightTask = new CountTask(mid + 1, end);
//                執行子任務
                leftTask.fork();
                rightTask.fork();
                int leftRes = leftTask.join();
                int rightRes = rightTask.join();
                sum = leftRes + rightRes;
            }
            return sum;
        }
    }

    public static void main(String[] args) throws Exception{
        ForkJoinPool pool = new ForkJoinPool();
//        生成計算任務,計算1到4的累加和
        CountTask task = new CountTask(1, 4);
        Future<Integer> result = pool.submit(task);
        try {
            System.out.println(result.get());
        } catch (InterruptedException e) {
        } catch (ExecutionException e) {
        }

    }
}

 

Fork/Join框架的實現原理

ForkJoinPool由ForkJoinTask數組和ForkJoinWorkerThread數組組成,ForkJoinTask數組負責存放程序提交給ForkJoinPool的任務,而ForkJoinWorkerThread數組負責執行這些任務。

(1)ForkJoinTask的fork方法實現原理

當我們調用ForkJoinTask的fork方法時,程序會調用ForkJoinWorkerThread的pushTask方法 異步地執行這個任務,然後立即返回結果。代碼如下。

public final ForkJoinTask<V> fork() {
    ((ForkJoinWorkerThread) Thread.currentThread()).pushTask(this);
    return this;
}

pushTask方法把當前任務存放在ForkJoinTask數組隊列裏。然後再調用ForkJoinPool的 signalWork()方法喚醒或創建一個工作線程來執行任務。代碼如下。

final void pushTask(ForkJoinTask<> t) {
    ForkJoinTask<>[] q;
     int s, m;
    if ((q = queue) != null) { // ignore if queue removed
        long u = (((s = queueTop) & (m = q.length - 1)) << ASHIFT) + ABASE;
        UNSAFE.putOrderedObject(q, u, t);
        queueTop = s + 1; // or use putOrderedInt
        if ((s -= queueBase) <= 2)
            pool.signalWork();
        else if (s == m)
            growQueue();
    }
}

(2)ForkJoinTask的join方法實現原理

Join方法的主要作用是阻塞當前線程並等待獲取結果。

    public final V join() {
        if (doJoin() != NORMAL)
            return reportResult();
        else
            return getRawResult();
    }
    private V reportResult() {
        int s; Throwable ex;
        if ((s = status) == CANCELLED)
            throw new CancellationException();
        if (s == EXCEPTIONAL && (ex = getThrowableException()) != null)
            UNSAFE.throwException(ex);
        return getRawResult();
    }

它首先調用了doJoin()方法,通過doJoin()方法得到當前任務的狀態來判斷返回什麼結 果,任務狀態有4種:

已完成(NORMAL)、被取消(CANCELLED)、信號(SIGNAL)和出現異常 (EXCEPTIONAL)。

再看下doJoin()方法。該方法首先查看任務狀態,若執行完成則直接返回任務狀態;若沒有,則從任務數組裏取出任務並執行。。如果任務順利執行 完成,則設置任務狀態爲NORMAL,如果出現異常,則記錄異常,並將任務狀態設置爲 EXCEPTIONAL。

    private int doJoin() {
        Thread t; 
        ForkJoinWorkerThread w; 
        int s; 
        boolean completed;
        if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) {
            if ((s = status) < 0)
                return s;
            if ((w = (ForkJoinWorkerThread)t).unpushTask(this)) {
                try {
                    completed = exec();
                } catch (Throwable rex) {
                    return setExceptionalCompletion(rex);
                }
                if (completed)
                    return setCompletion(NORMAL);
            }
            return w.joinTask(this);
        }
        else
            return externalAwaitDone();
    }

 

Fork/Join框架的異常處理

ForkJoinTask在執行過程可能會拋出異常,ForkJoinTask提供了了isCompletedAbnormally()方法來檢查任務是否已經拋出異常或已經被 取消了,並且可以通過ForkJoinTask的getException方法獲取異常。

if(task.isCompletedAbnormally()) {
    System.out.println(task.getException());
}

getException方法返回Throwable對象,如果任務被取消了則返回CancellationException。如 果任務沒有完成或者沒有拋出異常則返回null

 

 

 

 

 

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