Java 基礎(十五)併發工具包 concurrent

diamond_lin發表於2017-11-13

本文目錄:

  • java.util.concurrent - Java 併發包簡介
  • 阻塞佇列 BlockingQueue
  • 陣列阻塞佇列 ArrayBlockingQueue
  • 延遲佇列 DelayQueue
  • 鏈阻塞佇列 LinkedBlockingQueue
  • 具有優先順序的阻塞佇列 PriorityBlockingQueue
  • 同步佇列 SynchronousQueue
  • 阻塞雙端佇列 BlockingDeque
  • 鏈阻塞雙端佇列 LinkedBlockingDeque
  • 併發 Map ConcurrentMap
  • 併發導航對映 ConcurrentNavigableMap
  • 閉鎖 ConutDownLatch
  • 柵欄 CyclicBarrier
  • 交換機 Exchanger
  • 訊號量 Semaphore
  • 執行器服務 ExecutorService
  • 執行緒池執行者 ThreadPoolExecutor
  • 定時執行者服務 ScheduledExecutorService
  • 使用 ForkJoinPool 進行分叉和合並
  • 鎖 Lock
  • 讀寫鎖 ReadWriteLock
  • 原子性布林 AtomicBoolean
  • 原子性整型 AtomicInteger
  • 原子性長整型 AtomicLong
  • 原子性引用型 AtomicReference

本章內容比較多,我自己也是邊學邊總結,所以拖到今天才出爐。另外,建議學習本文的小夥伴在學習的過程中,把程式碼 copy 出去run 一下,有助於理解。

1.java.util.concurrent Java 併發工具包

這是 Java5 新增的一個併發工具包。這個包包含了一系列能夠讓 Java 的併發程式設計變得更加簡單輕鬆的類。在這之前,你需要自己手動去實現相關的工具類。

本文將和大家一起學習 java.util.concurrent包裡的這些類,學完之後我們可以嘗試如何在專案中使用它們。

2.阻塞佇列 BlockingQueue

java.util.concurrent 包裡面的 BlockingQueue 介面表示一個執行緒安放如和提取例項的佇列。

這裡我們不會討論在 Java 中實現一個你自己的 BlockingQueue。

BlockingQueue 用法

BlockingQueue 通常用於一個執行緒生產物件,而另一個執行緒消費這些物件的場景。下圖是對這個原理的闡述:

一個執行緒往裡邊放,另外一個執行緒從裡邊取的一個 BlockingQueue.png
一個執行緒往裡邊放,另外一個執行緒從裡邊取的一個 BlockingQueue.png

一個執行緒將會持續生產新物件並將其插入到佇列之中,直到佇列達到它所能容納的臨界點。也就是說,它是有限的。如果該阻塞佇列到達了其臨界點,負責生產的執行緒將會在往裡面插入新物件時發生阻塞。它會一直處於阻塞之中,直到負責消費的執行緒從佇列中拿走一個物件。負責消費的執行緒將會一直從該阻塞佇列中拿出物件。如果消費執行緒嘗試去從一個空的佇列中提取物件的話,這個消費執行緒將會處於阻塞之中,直到一個生產執行緒把一個物件丟進佇列。

BlockingQueue 的方法

BlockingQueue 具有4組不同的方法用於插入、移除以及對佇列中的元素進行檢查。如果請求的操作不能得到立即執行的話,每個方法的表現也不同。這些方法如下:

~ 拋異常 特定值 阻塞 超時
插入 add(o) offer(o) put(o) offer(o,timeout,timeUnit)
移除 remove(o) poll(o) take(o) poll(timeout,timeunit)
檢查 element(o) peek(o) ~ ~

四組不同的行為方式解釋:

1.拋異常:如果試圖的操作無法立即執行,拋一個異常
2.特定值:如果試圖的操作無法立即執行,返回一個特定的值(一般是 true/false)
3.阻塞:如果試圖的操作無法立即執行,該方法將會發生阻塞,直到能執行
4.超時:如果試圖的操作無法立即執行,該方法呼叫將會發生阻塞,直到能夠執行,但等待時間不會超過給定值。返回一個特定的值以告知該操作是否成功。

無法向一個 BlockingQueue 中插入 null。如果你試圖插入 null,BlockingQueue 會丟擲一個 NullPointerException。

可以訪問到 BlockingQueue 中的所有元素,而不僅僅是開始和結束的元素。比如說你將一個物件放入佇列之中以等待處理,但你的應用想要將其取消掉,那麼你可以呼叫諸如remove(o)方法啦將佇列中的特定物件進行移除。但是這麼幹相率並不高,因此儘量不要用這一類方法,除非迫不得已。

BlockingQueue 的實現

BlockingQueue 是個藉口,你可以通過它的實現之一來使用 BlockingQueue。concurrent 包裡面有如下幾個類實現了 BlockingQueue:

  • ArrayBlockingQueue
  • DelayQueue
  • LinkedBlockingQueue
  • PriorityBlockingQueue
  • SynchronousQueue

Java 中使用 BlockingQueue 的例子

這是一個 Java 鍾使用 BlockingQueue 的示例,本示例使用的是 BlockingQueue 藉口的 ArrayBlockingQueue 實現。
首先,BlockingQueueExample 類分別在兩個獨立的執行緒中啟動了一個 Producer 和 Consumer 。Producer 向一個共享的 BlockingQueue 中注入字串,而 Consumer 則會從中把它們拿出來。

public class BlockingQueueExample {
    public static void main(String[] args) {
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(1024);
        Producer producer = new Producer(queue);
        Consumer consumer = new Consumer(queue);
        new Thread(producer).start();
        new Thread(consumer).start();
    }
}

class Producer implements Runnable {
    private BlockingQueue<String> queue = null;

    Producer(BlockingQueue<String> queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            queue.put("1");
            Thread.sleep(1000);
            queue.put("2");
            Thread.sleep(1000);
            queue.put("3");
        } catch (InterruptedException ignored) {
        }
    }
}

class Consumer implements Runnable {
    private BlockingQueue queue = null;

    Consumer(BlockingQueue queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            System.out.println(queue.take());
            System.out.println(queue.take());
            System.out.println(queue.take());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}複製程式碼

這個例子很簡單吶,我就不加文字描述了。

3.陣列阻塞佇列 ArrayBlockingQueue

ArrayBlockingQueue 類實現了 BlockingQueue 介面。

ArrayBlockingQueue 是衣蛾有界的阻塞佇列,其內部實現是將物件放到一個陣列裡。有界也就意味著,它不能夠儲存無限多數量的原色。它有一個同一時間儲存元素數量的上線。你可以在對其初始化的時候設定這個上限,但之後就無法對這個上限進行修改了。

ArrayBlockingQueue 內部以 FIFO(先進先出)的順序對元素進行儲存。佇列中的頭元素在所有元素之中是放入時間最久的那個,而尾元素則是最短的那個。

以下是在使用 ArrayBlockingQueue 的時候對其初始化的一個示例:

BlockingQueue queue = new ArrayBlockingQueue(1024);
try {
    queue.put("1");
    Object object = queue.take();
} catch (InterruptedException e) {
    e.printStackTrace();
}複製程式碼

4.延遲佇列 DelayQueue

DelayQueue 實現了 BlockingQueue 介面
DelayQueue 對元素進行持有知道一個特定的延遲到期。注入其中的元素必須實現 concurrent.Delay 介面,該介面定義:

public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit var1);
}複製程式碼

DelayQueue 將會在每個元素的 getDelay()方法返回的值的時間段之後才釋放掉該元素。如果返回的是 0 或者負值,延遲將被認為過期,該元素將會在 DelayQueue 的下一次 take 被呼叫的時候被釋放掉。

傳遞給 getDelay 方法的 getDelay 例項是一個列舉型,它表明了將要延遲的時間段。

TimeUnit 列舉的取值單位都能顧名思義,這裡就帶過了。

上面我們可以看到 Delayed 介面繼承了 Comparable 介面,這也就意味著 Delayed 物件之間可以進行對比。這個可能在對 DelayeQueue 佇列中的元素進行排序時有用,因此它們可以根據過期時間進行有序釋放。

以下是使用 DelayQueue 的例子:

public class DelayQueueExample {
    public static void main(String[] args) throws InterruptedException {
        DelayQueue<DelayedElement> queue = new DelayQueue<>();
        DelayedElement element1 = new DelayedElement(1000);
        DelayedElement element2 = new DelayedElement(0);
        DelayedElement element3 = new DelayedElement(400);
        queue.put(element1);
        queue.put(element2);
        queue.put(element3);
        DelayedElement e = queue.take();
        System.out.println("e1:" + e.delayTime);
        DelayedElement e2 = queue.take();
        System.out.println("e2:" + e2.delayTime);
        DelayedElement e3 = queue.take();
        System.out.println("e3:" + e3.delayTime);
    }
}

class DelayedElement implements Delayed {
    long delayTime;
    long tamp;

    DelayedElement(long delay) {
        delayTime = delay;
        tamp = delay + System.currentTimeMillis();
    }

    @Override
    public long getDelay(@NonNull TimeUnit unit) {
        return tamp - System.currentTimeMillis();
//        return -1;
    }

    @Override
    public int compareTo(@NonNull Delayed o) {
        return tamp - ((DelayedElement) o).tamp > 0 ? 1 : -1;
    }
}複製程式碼

執行結果:

e1:0
e2:400
e3:1000複製程式碼

在 take 取出 e2的時候,會阻塞。
compareTo 決定佇列中的取出順序
getDelay 決定是否能取出元素,如果無法取出則阻塞執行緒。

具體玩法,大家可以自行思考,我看了一下別人用DelayQueue,能玩出很多花樣,在某些特定的需求很方便。

5.鏈阻塞佇列 LinkedBlockingQueue

LinkedBlockingQueue 類也實現了 BlockingQueue介面。
LinkedBlockingQueue 內部以一個鏈式結構對其元素進行儲存。如果需要的話,這一鏈式結構可以選擇一個上線。如果沒有定義上線,將使用 Ingeter.MAX_VALUE 作為上線。

LinkedBlockingQueue 內部以 FIFO(先進先出)的順序對元素進行儲存。佇列中的頭元素在所有元素之中是放入時間最久的那個。

使用方式同 ArrayBlockingQueue。

6.具有優先順序的阻塞佇列 PriorityBlockingQueue

PriorityBlockingQueue 類也實現了 BlockingQueue 介面。

PriorityBlockingQueue 是一個無界的併發佇列。它使用了和 PriorityQueue 一樣的排序規則。你無法向這個佇列中插入 null 值。PriorityQueue 的程式碼分析在集合中講了,感興趣的小夥伴可以回頭去閱讀。

所有插入到 PriorityBlockingQueue 的元素必須實現 Comparable 介面或者在構造方法中傳入Comparator。

注意:PriorityBlockingQueue 對於具有相等優先順序的元素並不強制任何特定的行為。

同時注意:如果你從一個 PriorityBlockingQueue 獲得一個 Iterator 的話,該 Iterator並不能保證它對元素的遍歷是按照優先順序的。原理在之前的文章中分析過~

使用方法同上。

7.同步佇列 SynchronousQueue

SynchronousQueue 類實現了 BlockingQueue 介面。
SynchronousQueue 是一個特殊的佇列,它的內部同時只能容納單個元素。如果該佇列已有一個元素的話,試圖向佇列中插入一個新元素的執行緒將會阻塞,知道另一個執行緒將該元素從佇列中抽走。同樣,如果該佇列為空,試圖向佇列中抽取一個元素的執行緒將會阻塞,直到另一個執行緒向佇列中插入了一條新的元素。
據此,把這個類稱作一個佇列顯然是誇大其詞,它更多像是一個匯合點。

使用方法和 ArrayBlockingQueue 一樣吧,區別就是 SynchronousQueue 只能儲存一個元素。

可以理解成這個,哈哈哈new ArrayBlockingQueue<>(1);

8.阻塞雙端佇列 BlockingDeque

BlockingDeque 介面在 concurrent 包裡,表示一個執行緒安放入和提取例項的雙端佇列。

BlockingDeque 類是一個雙端佇列,在不能夠插入元素時,它將阻塞住試圖插入元素的執行緒;在不能夠抽取元素時,它將阻塞住試圖抽取的執行緒。

deque 是“Double Ended Queue”的縮寫。因此,雙端佇列是一個你可以從任意一段插入或者抽取元素的佇列。

BlockingDeque 的使用

線上程既是生產者又是這個佇列的消費者的時候可以用到 BlockingDeque。如果生產者執行緒需要在佇列的兩端都可以插入資料,消費者執行緒需要在佇列的兩端都可以移除資料,這時候也可以用 BlockingDeque。BlockingDeque 圖解:

一個執行緒生產元素,並把它們插入到佇列的任意一段。如果雙端佇列已滿,插入執行緒將被阻塞,知道一個移除執行緒從佇列中移除了一個元素。

BlockingDeque 的方法

BlockingDeque 具有4組不同的方法用於插入、移除以及對雙端佇列中的元素進行檢查。如果請求的操作不能得到立即執行的話,每個方法的表現也不同。這些方法如下:

~ 拋異常 特定值 阻塞 超時
插入 addFirst(o) offerFirst(o) putFirst(o) offerFirst(o,timeout,timeUnit)
移除 removeFirst(o) pollFirst(o) takeFirst(o) pollFirst(timeout,timeunit)
檢查 getFirst(o) peekFirst(o) ~ ~
~ 拋異常 特定值 阻塞 超時
插入 addLast(o) offerLast(o) putLast(o) offerLast(o,timeout,timeUnit)
移除 removeLast(o) pollLast(o) takeLast(o) pollLast(timeout,timeunit)
檢查 getLast(o) peekLast(o) ~ ~

1.拋異常:如果試圖的操作無法立即執行,拋一個異常
2.特定值:如果試圖的操作無法立即執行,返回一個特定的值(一般是 true/false)
3.阻塞:如果試圖的操作無法立即執行,該方法將會發生阻塞,直到能執行
4.超時:如果試圖的操作無法立即執行,該方法呼叫將會發生阻塞,直到能夠執行,但等待時間不會超過給定值。返回一個特定的值以告知該操作是否成功。

這一段文字有沒有感覺特別眼熟,hahah~其實它和 BlockingQueue 一樣。

BlockingDeque 繼承自 BlockingQueue

BlockingDeque 介面繼承自 BlockingQueue 介面。這就意味著你可以像使用一個 BlockingQueue 那樣使用 BlockingDeque。如果你這麼幹的話,各種插入方法將會把新元素新增到雙端佇列的尾端,而移除方法將會把雙端佇列的首端元素移除。正如 BlockingQueue 介面的插入和移除方法一樣。

BlockingDeque 的實現

既然 BlockingDeque 是一個介面,那麼肯定有實現類,它的實現類不多,就一個:

  • LinkedBlockingDeque

BlockingDeque 程式碼示例

這個真沒什麼好說的。。。

BlockingDeque<String> deque = new LinkedBlockingDeque<String>();
deque.addFirst("1");
deque.addLast("2");
String two = deque.takeLast();
String one = deque.takeFirst();複製程式碼

9.鏈阻塞雙端佇列LinkedBlockingDeque

LinkedBlockingDeque 類實現了 BlockingDeque 介面。

不想寫描述了,跳過了昂~

10.併發 Map(對映)ConcurrentMap

ConcurrentMap 介面表示了一個能夠對別人的訪問(插入和提取)進行併發處理的 Map。
ConcurrentMap 除了從其父介面 java.util.Map 繼承來的方法之外還有一些額外的原子性方法。

ConcurrentMap 的實現

concurrent 包裡面就一個類實現了 ConcurrentMap 介面

  • ConcurrentHashMap

ConcurrentHashMap

ConcurrentHashMap 和 HashTable 類很相似,但 ConcurrentHashMap 能提供比 HashTable 更好的併發效能。在你從中讀取物件的時候,ConcurrentHashMap 並不會把整個 Map 鎖住。此外,在你向其寫入物件的時候,ConcurrentHashMap 也不會鎖住整個 Map,它的內部只是把 Map 中正在被寫入的部分鎖定。
其實就是把 synchronized 同步整個方法改為了同步方法裡面的部分程式碼。

另外一個不同點是,在被遍歷的時候,即使是 ConcurrentHashMap 被改動,它也不會拋 ConcurrentModificationException。儘管 Iterator 的設計不是為多個執行緒同時使用。

使用例子:

public class ConcurrentHashMapExample {

    public static void main(String[] args) {
//        HashMap<String, String> map = new HashMap<>();
        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
        map.put("1", "a");
        map.put("2", "b");
        map.put("3", "c");
        map.put("4", "d");
        map.put("5", "e");
        map.put("6", "f");
        map.put("7", "g");
        map.put("8", "h");
        new Thread1(map).start();
        new Thread2(map).start();

    }

}

class Thread1 extends Thread {

    private final Map map;

    Thread1(Map map) {
        this.map = map;
    }

    @Override
    public void run() {
        super.run();
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        map.remove("6");
    }
}

class Thread2 extends Thread {

    private final Map map;

    Thread2(Map map) {
        this.map = map;
    }

    @Override
    public void run() {
        super.run();
        Set set = map.keySet();
        for (Object next : set) {
            System.out.println(next + ":" + map.get(next));
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}複製程式碼

列印結果:

1:a
2:b
3:c
4:d
5:e
7:g
8:h複製程式碼

思考題:用這個 Map 行不行?Map map = Collections.synchronizedMap(new HashMap());

哈哈哈~答案很簡單,思考一下就行了。

11.併發導航對映 ConcurrentNaviagbleMap

ConcurrentNavigableMap 是一個支援併發的 NavigableMap,它還能讓他的子 Map 具備併發訪問的能力,所謂的“子 map”指的是諸如 headMap(),subMap(),tailMap()之類的方法返回的 map.

NavigableMap

NavigableMap 這個介面之前集合中遺漏了。
這裡稍微補充一下吧,首先繼承關係是 NavigableMap繼承自 SortedMap 繼承自 Map。

SortedMap從名字就可以看出,在Map的基礎上增加了排序的功能。它要求key與key之間是可以相互比較的,從而達到排序的目的。

而NavigableMap是繼承於SortedMap,目前只有TreeMap和ConcurrentNavigableMap兩種實現方式。它本質上新增了搜尋選項到介面,主要為紅黑樹服務。先來了解下它新增的幾個方法

主要方法

  • lowerEntry(K key)返回小於 key 的最大值的節點
  • lowerKey(K key)返回小於 key 的最大值節點的 key
  • floorEntry(K key)返回小於等於 key 的最大值節點
  • floorKey(K key)返回小於等於 key 的最大值節點 key
  • ceilingEntry(K key)返回大於等於 key 的最小節點
  • ceilingkey(K key)返回大於等於 key 的最小節點的 key
  • higherEntry(K key)返回大於 key 的最小節點
  • higherKey(K key)返回大於 key 的最小節點 key
  • firstEntry()返回最小key 節點
  • lastEntry()返回最大 key 節點
  • descendingMap()獲取反序的 map
  • navigableKeySet()獲取升序迭代器
  • decendingKeySet()獲取降序的迭代器
  • subMap(K from,K to)擷取 map
  • headMap(K toKey)擷取小於等於 toKey 的 map
  • tailMao(K fromKey)擷取大於等於 key 的 map

額,講完了。。。。。就不舉?了吧~

12.閉鎖 CountDownLatch

java.util.concurrent.CountDownLatch 是一個併發構造,它允許一個或多個執行緒等待一系列指定操作的完成。

CountDownLatch 以一個給定的數量初始化。countDown()每被呼叫一次,這一數量就建議。通過呼叫 await()方法之一,執行緒可以阻塞等待這一數量到達零。

下面是一個簡單的示例,Decrementer 三次呼叫 countDown()之後,等待中的 Waiter 才會從 await()呼叫中釋放出來。

public class CountDownLatchExample {

    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(3);

        Waiter waiter = new Waiter(latch);
        Decrementer decrementer = new Decrementer(latch);

        new Thread(waiter).start();
        new Thread(decrementer).start();

    }

}

class Waiter implements Runnable {

    CountDownLatch latch = null;

    public Waiter(CountDownLatch latch) {
        this.latch = latch;
    }

    public void run() {
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Waiter Released");
    }
}

class Decrementer implements Runnable {

    CountDownLatch latch = null;

    Decrementer(CountDownLatch latch) {
        this.latch = latch;
    }

    public void run() {

        try {
            Thread.sleep(1000);
            this.latch.countDown();

            Thread.sleep(1000);
            this.latch.countDown();

            Thread.sleep(1000);
            this.latch.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}複製程式碼

執行結果

Waiter Released複製程式碼

嗯,用法大概就是醬紫。我再給大家舉個實際的例子吧~

有時候會有這樣的需求,多個執行緒同時工作,然後其中幾個可以隨意併發執行,但有一個執行緒需要等其他執行緒工作結束後,才能開始。舉個例子,開啟多個執行緒分塊下載一個大檔案,每個執行緒只下載固定的一截,最後由另外一個執行緒來拼接所有的分段,那麼這時候我們可以考慮使用CountDownLatch來控制併發。

13.柵欄 CyclicBarrier

CyclicBarrier 類是一種同步機制,它能對處理一些演算法的執行緒實現同步。換句話說,它就是一個所有執行緒必須等待的一個柵欄,直到所有執行緒都到達這裡,然後所有執行緒才可以繼續做其他事情。

這個文字很好理解吧,沒理解的把上面這段話再讀一遍。

圖示如下:

兩個執行緒在柵欄旁等待對方
兩個執行緒在柵欄旁等待對方

通過呼叫 CuclicBarrier 物件的 await()方法,兩個執行緒可以實現互相等待。一旦 N 個執行緒在等待 CyclicBarrier 達成,所有執行緒將被釋放掉去繼續執行。

建立一個 CyclicBarrier

在建立一個 CyclicBarrier 的時候你需要定義有多少執行緒在被釋放之前等待柵欄。建立 CyclicBarrier 示例:

CyclicBarrier barrier = new CyclicBarrier(2);複製程式碼

等待一個 CyclicBarrier

以下演示瞭如何讓一個執行緒等待一個 CyclicBarrier:
barrier.await();
當然,你也可以為等待執行緒設定一個超時時間。等待超過了超時時間之後,即便還沒有達成 N 個執行緒等待 CyclicBarrier 的條件,該執行緒也會被釋放出來。以下是定義超時時間示例:
barrier.await(10,TimeUnit.SECONDS);

當然,滿足以下條件也可以讓等待 CyclicBarrier 的執行緒釋放:

  • 最後一個執行緒也到達 CyclicBarrier(呼叫 await()方法)
  • 當前執行緒被其他執行緒打斷(其他執行緒呼叫了這個執行緒的 interrupt()方法)
  • 其他等待柵欄的執行緒被打斷
  • 其他等待柵欄的執行緒因超時而被釋放
  • 外部執行緒呼叫了柵欄的 CyclicBarrier.reset()方法

CyclicBarrier 行動

CyclicBarrier 支援一個柵欄行動,柵欄行動是一個 Runnable 例項,一旦最後等待柵欄的執行緒抵達,該例項將被執行。你可以在 CyclicBarrier 的構造方法中將 Runnable 柵欄行動傳給它:

CyclicBarrier barrier = new CyclicBarrier(2,barrierAction);

CyclicBarrier 示例程式碼

public class CyclicBarrierExample {

    public static void main(String[] args) {
        Runnable barrier1Action = new Runnable() {
            public void run() {
                System.out.println("BarrierAction 1 executed ");
            }
        };
        Runnable barrier2Action = new Runnable() {
            public void run() {
                System.out.println("BarrierAction 2 executed ");
            }
        };

        CyclicBarrier barrier1 = new CyclicBarrier(2, barrier1Action);
        CyclicBarrier barrier2 = new CyclicBarrier(2, barrier2Action);

        CyclicBarrierRunnable barrierRunnable1 =
                new CyclicBarrierRunnable(barrier1, barrier2);

        new Thread(barrierRunnable1).start();
        new Thread(barrierRunnable1).start();


    }

}

class CyclicBarrierRunnable implements Runnable {

    CyclicBarrier barrier1 = null;
    CyclicBarrier barrier2 = null;

    CyclicBarrierRunnable(
            CyclicBarrier barrier1,
            CyclicBarrier barrier2) {

        this.barrier1 = barrier1;
        this.barrier2 = barrier2;
    }

    public void run() {
        try {
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() +
                    " waiting at barrier 1");
            this.barrier1.await();

            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() +
                    " waiting at barrier 2");
            this.barrier2.await();

            System.out.println(Thread.currentThread().getName() +
                    " done!");

        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}複製程式碼

思考一下程式的執行結果~

Thread-0 waiting at barrier 1
Thread-1 waiting at barrier 1
BarrierAction 1 executed 
Thread-1 waiting at barrier 2
Thread-0 waiting at barrier 2
BarrierAction 2 executed 
Thread-0 done!
Thread-1 done!複製程式碼

14.交換機 Exchanger

Exchanger 類表示一種兩個執行緒可以進行互相交換物件的會和點。這種機制圖如下:

兩個執行緒通過一個 Exchanger 交換物件
兩個執行緒通過一個 Exchanger 交換物件

交換物件的動作由Exchanger 的兩個 exchange()方法中的其中一個完成。以下是一個示例:

public class ExchangerExample {

    public static void main(String[]args){
        Exchanger exchanger = new Exchanger();

        ExchangerRunnable exchangerRunnable1 =
                new ExchangerRunnable(exchanger, "Thread-0資料");

        ExchangerRunnable exchangerRunnable2 =
                new ExchangerRunnable(exchanger, "Thread-1資料");

        new Thread(exchangerRunnable1).start();
        new Thread(exchangerRunnable2).start();

    }

}
 class ExchangerRunnable implements Runnable{

    Exchanger exchanger = null;
    Object    object    = null;

    ExchangerRunnable(Exchanger exchanger, Object object) {
        this.exchanger = exchanger;
        this.object = object;
    }

    public void run() {
        try {
            Object previous = this.object;

            this.object = exchanger.exchange(this.object);

            System.out.println(
                    Thread.currentThread().getName() +
                            " exchanged " + previous + " for " + this.object
            );
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}複製程式碼

輸出結果:

Thread-1 exchanged Thread-1資料 for Thread-0資料
Thread-0 exchanged Thread-0資料 for Thread-1資料複製程式碼

當一個執行緒到達exchange呼叫點時,如果它的夥伴執行緒此前已經呼叫了此方法,那麼它的夥伴會被排程喚醒並與之進行物件交換,然後各自返回。

在常見的 生產者-消費者 模型中用於同步資料。

15.訊號量 Semaphore

Semaphore類是一個計數訊號量。這就意味著它具備兩個主要方法:

  • acquire()獲得
  • release()釋放

計數訊號量由一個指定數量的“許可”初始化。每呼叫一次 acquire(),一個許可會被呼叫執行緒取走。沒呼叫一次 release(),一個許可會被還給訊號量。因此,在沒有任何 release()呼叫時,最多有 N 個執行緒能夠通過 acquire()方法,N 是該訊號量初始化時的許可的指定數量。這些許可只是一個簡單的計數器。沒有啥奇特的地方。

Semaphore 用途

訊號量主要有兩種用途:

1.保護一個重要(程式碼)部分防止一次超過 N 個執行緒進入
2.在兩個執行緒之間傳送訊號

Semaphore 用法

如果你將訊號量用於保護一個重要部分,試圖進入這一部分的程式碼通常會首先嚐試獲得一個許可,然後才能進入重要部分(程式碼塊),執行完之後,再把許可釋放掉:

Semaphore semaphore = new Semaphore(1);
semaphore.acquire();
//重要部分程式碼塊
semaphore.release();複製程式碼

線上程之間發生訊號

如果你將一個訊號量用於在兩個執行緒之間傳送訊號,通常你應該用一個執行緒呼叫 acquire()方法,而另一個執行緒呼叫 release()方法。

如果沒有可用的許可,acquire()呼叫將會阻塞,知道一個許可被另一個執行緒釋放出來。同理,如果無法往訊號量釋放更多許可時,一個 release()方法呼叫也會阻塞。

通過這個可以對多個執行緒進行協調。比如,如果執行緒1將一個物件插入到了一個共享列表(list)之後呼叫了 acquire(),而執行緒2則從該列表中獲取一個物件之前呼叫了release(),這時你其實已經建立了一個阻塞佇列。訊號量中可用的許可的數量也就等同於該則是佇列能夠持有的元素個數。

公平

沒有辦法保證執行緒能夠公平地從訊號量中獲得許可。也就是說,無法擔保第一個呼叫 acquire()的執行緒會是第一個獲得許可的執行緒。如果第一個執行緒在等待一個許可時發生阻塞,而第二個執行緒來索要一個許可的時候剛好有一個許可被釋放出來,那麼它就可能在第一個執行緒之前獲得許可。
如果需要強制公平,Semaphore 類有一個具有一個布林型別的引數的構造子,通過這個引數以告知 Semaphore 是否要強制公平。強制公平會影響到併發效能,所以除非你確實需要它,否則不要啟動它。

以下是如何在公平模式建立一個 Semaphore 的示例:

Semaphore semaphore = new Semaphore(1,ture);

更多方法

  • acquire()獲取一個許可
  • availablePermits()返回當前可用許可數
  • drainPermits()獲取並返回立即可用的所有許可
  • getQueueThreads()返回一個集合,包含可能等待獲取的數量
  • hasQueueThreads()返回正在等待獲取的執行緒的估計數目
  • isFair()如果此訊號量的公平設定為 true,則返回 true
  • reducePermits(int)根據指定的縮減量減小可用許可的數量
  • relaese()釋放一個許可

具體參考 JDK 文件吧

使用案例

public class SemaphoreExample {

    public static void main(String[]args){
        Semaphore semaphore = new Semaphore(3);

        new Thread(new ThreadSemaphore(semaphore)).start();
        new Thread(new ThreadSemaphore(semaphore)).start();
        new Thread(new ThreadSemaphore(semaphore)).start();
        new Thread(new ThreadSemaphore(semaphore)).start();
        new Thread(new ThreadSemaphore(semaphore)).start();

    }

}

 class ThreadSemaphore implements Runnable{

    private final Semaphore semaphore;

     ThreadSemaphore(Semaphore semaphore){
        this.semaphore = semaphore;
    }

    @Override
    public void run() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName()+"獲取到鎖進來");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        semaphore.release();

    }
}複製程式碼

列印結果是先列印三條執行緒,1秒後再列印兩條執行緒。

腦補一下,我們當年搶第一代紅米手機的時候,搶購介面是不是一直在排隊,是不是用訊號量可以實現呢,10w 臺手機,同時只允許1000個使用者購買(Semaphore 的許可為1000個),然後運氣好突然排隊進去了(有人購買成功,釋放了許可,運氣好分配給了你),然後紅米搶購到手。。。

16.執行器服務 ExecutorService

ExecutorService 介面表示一個非同步執行機制,使我們能夠在後臺執行任務。因此一個 ExecutorService 很類似一個執行緒池。實際上,存在於 concurrent 包裡的 ExecutorService 實現就是一個執行緒池實現。

ExecutorService 例子

以下是一個簡單的 ExecutorService 例子:

ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(new Runnable() {
    public void run() {
        System.out.println("Asynchronous task");
    }
});
executorService.shutdown();複製程式碼

首先使用 newFixedThreadPool 工廠方法建立一個 ExecutorService。這裡建立了一個是個執行緒執行任務的執行緒池。
然後,將一個 Runnable 介面的匿名實現類傳給 execute()方法。這將導致 ExecutorService 中的某個執行緒執行該 Runnable。

任務委派

下圖說明了一個執行緒是如歌將一個任務委託給一個 ExecutorService 去非同步執行的:

一個執行緒將一個任務委派給一個 ExecutorService 去非同步執行
一個執行緒將一個任務委派給一個 ExecutorService 去非同步執行

一旦該執行緒將任務委派給 ExecutorService,該執行緒將繼續它自己的執行,獨立於該任務的執行。

ExecutorService 實現

既然 ExecutorService 是個介面,如果你想用它的話,還得去使用它的實現類。

  • ThreadPoolExecutor
  • ScheduledThreadPoolExecutor

建立一個 ExecutorService

ExecutorService 的建立依賴於你使用的具體實現。但是你也可以使用 Executors 工廠類來建立 ExecutorService 例項。以下是幾個建立 ExecutorService 例項的例子:

ExecutorService executorService1 = Executors.newSingleThreadExecutor();
ExecutorService executorService2 = Executors.newFixedThreadPool(10);
ExecutorService executorService3 = Executors.newScheduledThreadPool(10);複製程式碼

ExecutorService 使用

有幾種不同的方式來將任務委託給 ExecutorService 去執行:

  • execute(Runnable)
  • submit(Runnable)
  • submit(Callable)
  • invokeAny(Collection)
  • invokeAll(Collection)

接下來我們來一個一個看這些方法

  • execute(Runnable)

execute 方法要求一個 Runnable 物件,然後對它進行非同步執行。以下是使用 ExecutorService 執行一個 Runnable 的示例:

ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(new Runnable() {
    @Override
    public void run() {
        System.out.println("Asynchronous task");
    }
});
System.out.println("Asynchronous task");
executorService.shutdown();複製程式碼

沒有辦法得知被執行的 Runnable 的執行結果。如果需要的話,得使用 Callable

  • submit(Runnable)

sunmit 方法也要求一個 Runnable 實現類,但它返回一個 Future 物件。這個 Future 物件可以用來檢查 Runnable 是否已經執行完畢。

以下是 ExecutorService submit 示例:

ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(new Runnable() {
    public void run() {
        System.out.println("Asynchronous task");
    }
});
future.get(); //returns null if the task has finished correctly.複製程式碼
  • submit(Callable)
    submit(Callable)方法類似於 submit(Runnable)方法,除了它所要求的引數型別之外。Callable 例項除了它的 call()方法能夠返回一個結果之外和一個 Runnable 很像。Runnable.run()不能返回一個結果。

Callable 的結果可以通過 submit(Callable)方法返回的 Future 物件進行獲取。以下是一個 ExecutorService Callable 示例:

ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Object> future = executorService.submit(new Callable<Object>() {
    @Override
    public Object call() throws Exception {
        System.out.println("Asynchronous Callable");
        return "Callable Result";
        }
    });
System.out.println("future.get() = " + future.get());複製程式碼

以上程式碼輸出:

Asynchronous Callable
future.get() = Callable Result複製程式碼

注意:future.get()會阻塞執行緒直到 Callable 執行結束。你可以把這個當成是一個有返回值的執行緒。

  • invokeAny()

invokeAny()方法要求一系列的 Callable 或者其子介面的例項物件。呼叫這個方法並不會返回一個 Future,但它返回其中一個 Callable 物件的結果。無法保證返回的是哪個 Callable 的結果,只能表明其中一個已經執行結束。

ExecutorService executorService = Executors.newSingleThreadExecutor();
Set<Callable<String>> set = new HashSet<>();
set.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 1";
    }
});

set.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 2";
    }
});
set.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 3";
    }
});
String result = executorService.invokeAny(set);
System.out.println("result = " + result);
executorService.shutdown();複製程式碼

執行結果就不看了,自行測試吧

ExecutorService executorService = Executors.newSingleThreadExecutor();
Set<Callable<String>> set = new HashSet<>();
set.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 1";
    }
});

set.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 2";
    }
});
set.add(new Callable<String>() {
    public String call() throws Exception {
        return "Task 3";
    }
});
List<Future<String>> list = executorService.invokeAll(set);
for (Future<String> future : list)
    System.out.println("result = " + future.get());
executorService.shutdown();複製程式碼

執行結果自行測試。。。

ExecutorService 關閉

使用完 ExecutorService 之後,應該將其關閉,以使其中的執行緒不再允許。
比如,如果你的應用是通過一個 main 方法啟動的,之後 main 方法退出了你的應用,如果你的應用有一個活動的 ExecutorService,它將還會保持執行。ExecutorService 裡的活動執行緒阻止了 JVM 的關閉。
要終止 ExecutorService 裡的執行緒,你需要呼叫 ExecutorService 的 shutdown 方法。

ExecutorService 並不會立即關閉,但它將不再接受新的任務,而且一旦所有執行緒都完成了當前任務的時候,ExecutorService 將會關閉。在 shutdown 被呼叫之前所有提交給ExecutorService 的任務都被執行。
如果你想立即關閉 ExecutorService,你可以呼叫 shutdownNow 方法,這樣會立即嘗試停止所有執行中的任務,並忽略掉那些已提交但尚未開始處理的任務。無法保證執行任務的正確執行。可能它們被停止了,也可能已經執行結束。

17.執行緒池執行者 ThreadPoolExecutor

ThreadPoolExecutor 是 ExecutorService 介面的一個實現。
ThreadPoolExecutor 使用其內部池中的執行緒執行給定任務(Callable 或者 Runnable)。ThreadPoolExecutor 包含的執行緒池能夠包含不同數量的執行緒,池中執行緒的數量由以下變數決定:

  • corePoolSize
  • maximumPoolSize
    當一個任務委託給執行緒池時,如果池中執行緒數量低於 corePoolSize,一個新的執行緒將被建立,即使池中可能尚未有空閒執行緒。
    如果內部任務佇列已滿,而且有至少 corePoolSize 正在執行,但是執行執行緒的數量低於 maximumPoolSize,一個新的執行緒將被建立去執行該任務。

ThreadPoolExecutor 圖解:

一個ThreadPoolExecutor
一個ThreadPoolExecutor

建立一個 ThreadPoolExecutor

ThreadPoolExecutor 有若干個可用構造方法。比如:

int corePoolSize = 5;
int maxPoolSize = 10;
long keepAliveTime = 5000;
ExecutorService threadPoolExecutor = new ThreadPoolExecutor(corePoolSize,
            maxPoolSize,
            keepAliveTime,
            TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>());複製程式碼

但是,除非你確實需要顯式為 ThreadPoolExecutor 定義所有引數,使用 Executors 類中的額工廠方法之一會更加方便。

18.定時執行者服務 ScheduleExecutorService

ScheduleExecutorService 是一個 ExecutorService,它能夠將任務延後執行,或者間隔固定時間多次執行。任務由一個工作者執行緒非同步執行,而不是由提交任務給 ScheduleExecutorService 的那個執行緒執行。

ScheduleExecutorService 例子

以下是一個簡單的 ScheduleExecutorService 示例:

ScheduledExecutorService scheduledExecutorService =
                Executors.newScheduledThreadPool(5);
scheduledExecutorService.schedule(new Callable<Object>() {
    @Override
    public Object call() throws Exception {
        System.out.println("Executed!");
        return "Called!";
    }
}, 5, TimeUnit.SECONDS);
scheduledExecutorService.shutdown();複製程式碼

首先一個內建5個執行緒的 ScheduleExecutorService 被建立,之後一個 Callable 介面的匿名類示例被建立然後傳遞給 schedule()方法。後邊的兩引數定義了 Callable 將在5秒鐘之後被執行。

ScheduleExecutorService 實現

既然是一個介面,要使用它的話就得使用 concurrent 包下的實現類

  • ScheduleThreadPoolExecutor

建立一個 ScheduleExecutorService

如何建立一個 ScheduleExecutorService,取決於你採用它的實現類。但是你也可以使用 Executors 工廠類來建立一個 ScheduleExecutorService 例項。比如:

ScheduledExecutorService scheduledExecutorService =
                Executors.newScheduledThreadPool(5);複製程式碼

ScheduleExecutorService 使用

一旦你建立了一個 ScheduleExecutorService,你可以通過呼叫它的以下方法:

  • shcedule(Callable task,long delay,TimeUnit timeunit)
  • shcedule(Runnable task,long delay,TimeUnit timeunit)
  • shceduleAtFixedRate(Runnable task,long initialDelay,long period,TimeUtil timeutil)
  • shceduleWithFixedDelay(Runnable task,long initialDelay,long period,TimeUtil timeutil)

下面我們就簡單看一下這些方法。

  • schedule(Callable task,long delay,TimeUnit timeUnit)
    這個方法計劃指定的 Callable 在給定的延遲之後執行。
    這個方法返回一個 ScheduleFuture,通過它你可以在它被執行前對它進行取消,或者在它執行之後獲取結果。

以下是一個示例:

ScheduledExecutorService scheduledExecutorService =
                Executors.newScheduledThreadPool(5);
ScheduledFuture<Object> schedule = scheduledExecutorService.schedule(new Callable<Object>() {
    @Override
    public Object call() throws Exception {
        System.out.println("Executed!");
        return "Called!";
    }
}, 5, TimeUnit.SECONDS);
System.out.println(schedule.get());
scheduledExecutorService.shutdown();複製程式碼

輸出結果:

Executed!
Called!複製程式碼
  • shcedule(Runnable task,long delay,TimeUnit timeUnit)

除了 Runnable 無法返回一個結果之外,這一方法工作起來就像一個 Callable 作為一個引數的那個版本的方法一樣,因此 ScheduleFuture.get()在任務執行結束之後返回 null。

  • scheduleAtFixedRate(Runnable,long initialDelay,long period,TimeUnit tomeUnit)
    這一方法規劃一個任務將被定期執行。該任務將會在某個 initialDelay 之後得到執行,然後每個 period 時間之後重複執行。
    如果給的任務的執行丟擲了異常,該任務將不再執行。如果沒有任何異常的話,這個任務將會持續迴圈執行到 ScheduleExecutorService 被關閉。
    如果一個任務佔用了比計劃的時間間隔更長的時候,下一次執行將在當前執行結束執行才開始。計劃任務在同一時間不會有多個執行緒同時執行。

  • scheduleWithFixedDelay(Runnable r,long initalDelay,long period,TimeUnit timeUnit)

除了 period 有不同的解釋之外這個方法和 scheduleAtFixedRate()非常像。
scheduleAtFixedRate()方法中,period 被解釋為前一個執行的開始和下一個執行的開始之間的間隔時間。
而在本方法中,period 則被解釋為前一個執行的結束和下一個執行開始之間的間隔。

ShceduleExecutorService 關閉

正如 ExecutorService,在你使用結束之後,你需要吧 ScheduleExecutorService 關閉掉。否則他將導致 JVM 繼續執行,即使所有其他執行緒已經全部被關閉。
你可以從 ExecutorService 介面繼承來的 shutdown()或 shutdownNow()方法將 ScheduleExecutorService 關閉。

19.使用 ForkJoinPool 進行分叉和合並

ForkJoinPool 在 Java7中被引入。它和 ExecutorService 很相似,除了一點不同。
ForkJoinPool 讓我們可以很方便把任務分裂成幾個更小的任務,這些分裂出來的任務也將會提交給 ForkJoinPool。任務可以繼續分割成更小的子任務,只要它還能分割。可能聽起來有點抽象,因此本節中我們將會解釋 ForkJoinPool 是如何工作的,還有任務分割是如何進行的。

分叉和合並解釋

在我們開始看 ForkJoinPool 之前,我們先來簡要解釋一下分叉和合並的原理。
分叉和合並原理包含兩個遞迴進行的步驟。兩個步驟分別是分叉步驟和合並步驟。

  • 分叉

一個使用了分叉和合並原理的任務可以將自己分叉(分割)為更小的子任務,這些子任務可以被併發執行。如下圖所示:

分叉
分叉

通過把自己分割成多個子任務,每個子任務可以由不同的 CPU 併發執行,或者被同一個 CPU 上的不同執行緒執行。

只有當給的任務過大,把它分割成幾個子任務才有意義。把任務分割成子任務有一點的開銷,因此對於小型任務,這個分割的消耗可能比每個子任務併發執行的消耗還要大。

什麼時候把一個任務分割成子任務是有意義的,這個界限也稱作一個閾值。折腰看每個任務對有意義閾值的決定。很大程度取決於它要做的工作的種類。

  • 合併

當一個任務將自己分割成若干子任務之後,該任務將進入等待所有子任務的結束之中。
一旦子任務執行結束,該任務可以把所有結果合併到同一結果。圖示如下:

合併
合併

當然,並非所有型別的任務都會返回一個結果。如果這個任務並不返還一個結果,它只需等待所有子執行緒執行完畢。也就不需要結果合併。

ForkJoinPool

ForkJoinPool 是一個特殊的執行緒池,她的設計是為了更好的配合 分叉-合併 任務分割的工作。ForkJoinPool 也在 concurrent 包中。

可以通過其構造方法建立一個 ForkJoinPool。 ForkJoinPool 建構函式的引數定義了 ForkJoinPool 的並行級別,並行級別表示分叉的執行緒或 CPU 數量。

建立示例:
ForkJoinPool forkJoinPool = new ForkJoinPool(4);

提交任務到 ForkJoinPool

就像提交任務到 ExecutorService那樣,把任務提交到 ForkJoinPool。你可以提交兩種型別的任務。一種是沒有任何返回值的,另一種是有返回值的。這兩週任務分別有 RecursiveAction 和 RecursiveTask 表示。接下來介紹如何使用這兩種型別的任務,以及如何對它們進行提交。

RecursiveAction

RecursiveAction 是一種沒有返回值的任務。它只是做一些工作,比如寫資料到磁碟,然後就退出了。
一個 RecursiveAction 可以把自己的工作分割成更小的幾塊,這樣它們可以由獨立的執行緒或者 CPU 執行。
你可以通過整合來實現一個 RecursiveAction。示例如下:

public class RecursiveActionExample {
    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool(40);
        MyRecursiveAction myRecursiveAction = new MyRecursiveAction(240);
        forkJoinPool.invoke(myRecursiveAction);

    }
}

class MyRecursiveAction extends RecursiveAction {
    private long workLoad = 0;

    public MyRecursiveAction(long workLoad) {
        this.workLoad = workLoad;
    }

    @Override
    protected void compute() {
        //if work is above threshold, break tasks up into smaller tasks
        if (this.workLoad > 16) {
            System.out.println("Splitting workLoad : " + this.workLoad);
            List<MyRecursiveAction> subtasks =
                    new ArrayList<>();
            subtasks.addAll(createSubtasks());
            for (RecursiveAction subtask : subtasks) {
                subtask.fork();
            }
        } else {
            System.out.println("Doing workLoad myself: " + this.workLoad);
        }
    }

    private List<MyRecursiveAction> createSubtasks() {
        List<MyRecursiveAction> subtasks =
                new ArrayList<>();
        MyRecursiveAction subtask1 = new MyRecursiveAction(this.workLoad / 2);
        MyRecursiveAction subtask2 = new MyRecursiveAction(this.workLoad / 2);
        subtasks.add(subtask1);
        subtasks.add(subtask2);
        return subtasks;
    }
}複製程式碼

例子跟簡單。MyRecursiveAction 將一個虛構的 workLoad 作為引數傳給自己的構造方法。如果 wrokLoad 高於一個特定的閾值,該工作將分割為幾個子工作,子工作繼續分割。如果 workLoad 高於一個特定閾值,該工作將被分割為幾個子工作,子工作繼續分割。如果 workLoad 低於特定閾值,該工作將有 MyRecursiveAction 自己執行。

執行結果:

Splitting workLoad : 240
Splitting workLoad : 120
Splitting workLoad : 120
Splitting workLoad : 60
Splitting workLoad : 60
Splitting workLoad : 60
Splitting workLoad : 30
Splitting workLoad : 30
Splitting workLoad : 30
Splitting workLoad : 30
Doing workLoad myself: 15
Doing workLoad myself: 15
Doing workLoad myself: 15
Doing workLoad myself: 15
Doing workLoad myself: 15
Splitting workLoad : 30
Doing workLoad myself: 15
Doing workLoad myself: 15
Doing workLoad myself: 15
Splitting workLoad : 30
Doing workLoad myself: 15
Doing workLoad myself: 15
Doing workLoad myself: 15
Splitting workLoad : 60
Splitting workLoad : 30
Doing workLoad myself: 15複製程式碼

RecursiveTask

RecursiveTask 是一種會返回結果的任務。它可以將自己的工作分割為若干更小任務,並將這些子任務的執行結果合併到一個集體結果。可以有幾個水平的分割和合並。以下是一個 RecursiveTask 示例:

public class RecursiveTaskExample {
    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool(40);
        MyRecursiveTask myRecursiveAction = new MyRecursiveTask(240);
        Object invoke = forkJoinPool.invoke(myRecursiveAction);
        System.out.println("mergedResult = " + invoke);
    }
}

class MyRecursiveTask extends RecursiveTask<Long> {

    private long workLoad = 0;

    public MyRecursiveTask(long workLoad) {
        this.workLoad = workLoad;
    }

    protected Long compute() {

        //if work is above threshold, break tasks up into smaller tasks
        if (this.workLoad > 16) {
            System.out.println("Splitting workLoad : " + this.workLoad);

            List<MyRecursiveTask> subtasks =
                    new ArrayList<>();
            subtasks.addAll(createSubtasks());

            for (MyRecursiveTask subtask : subtasks) {
                subtask.fork();
            }

            long result = 0;
            for (MyRecursiveTask subtask : subtasks) {
                result += subtask.join();
            }
            return result;

        } else {
            System.out.println("Doing workLoad myself: " + this.workLoad);
            return workLoad * 3;
        }
    }

    private List<MyRecursiveTask> createSubtasks() {
        List<MyRecursiveTask> subtasks =
                new ArrayList<>();

        MyRecursiveTask subtask1 = new MyRecursiveTask(this.workLoad / 2);
        MyRecursiveTask subtask2 = new MyRecursiveTask(this.workLoad / 2);

        subtasks.add(subtask1);
        subtasks.add(subtask2);

        return subtasks;
    }
}複製程式碼

注意是如何通過 ForkJoinPool.invoke()方法的呼叫來獲取最終執行結果的。

執行結果:

Splitting workLoad : 240
Splitting workLoad : 120
Splitting workLoad : 120
Splitting workLoad : 60
Splitting workLoad : 30
Doing workLoad myself: 15
Splitting workLoad : 60
Splitting workLoad : 30
Doing workLoad myself: 15
Splitting workLoad : 30
Splitting workLoad : 60
Splitting workLoad : 30
Splitting workLoad : 60
Doing workLoad myself: 15
Splitting workLoad : 30
Splitting workLoad : 30
Doing workLoad myself: 15
Doing workLoad myself: 15
Doing workLoad myself: 15
Doing workLoad myself: 15
Doing workLoad myself: 15
Doing workLoad myself: 15
Splitting workLoad : 30
Doing workLoad myself: 15
Doing workLoad myself: 15
Doing workLoad myself: 15
Doing workLoad myself: 15
Splitting workLoad : 30
Doing workLoad myself: 15
Doing workLoad myself: 15
Doing workLoad myself: 15
mergedResult = 720複製程式碼

ForkJoinPool 評論

貌似並非每個人都對 Java7裡面的 ForkJoinPool 滿意,也就是說,這裡面會有坑,在你計劃在自己的專案裡使用 ForkJoinPool 之前最好閱讀一下這篇文章《一個 Java 分叉-合併 帶來的災禍》

haha...文章是英文版本的,可以用瀏覽器外掛翻譯,或者自行百度吧。

20.鎖 Lock

Lock 是一個類似於 Synchronized 塊的執行緒同步機制。但是 Lock 比 Synchronized 塊更加靈活、精細。

Lock 例子

既然 Lock 是一個介面,在程式中總需要使用它的實現類之一來使用它。以下是一個簡單示例:

Lock lock = new ReentrantLock();
lock.lock();
//同步程式碼
lock.unLock();複製程式碼

首先建立了一個 Lock 物件。之後呼叫了它的 lock()方法。這時候這個 lock 例項就被鎖住啦。任何其他再過來呼叫 lock()方法的執行緒將會被鎖阻塞住,直到鎖定 lock 執行緒的例項的執行緒呼叫了 unlock()方法。最後 unlock()被呼叫了,lock 物件解鎖了,其他執行緒可以對它進行鎖定了。

Lock 實現

concurrent 包下 Lock 的實現類如下:

  • ReentrantLock

Lock 和 Synchronized 程式碼塊的主要不同點

  • Synchronized 程式碼塊不能夠保證進入訪問等待的執行緒的先後順序
  • 你不能傳遞任何引數給一個 Synchronized 程式碼塊的入口。因此,對於 Synchronized 程式碼塊的訪問等待設定超時時間是不可能的事情。
  • Synchronized 塊必須被完整地包含在單個方法裡。而一個 Lock 物件可以把它的 lock()和 unLock()方法的呼叫放在不同的方法裡。

Lock 的方法

Lock 介面主要有以下幾個方法

  • lock()
  • lockInterruptibly()
  • tryLock()
  • tryLock(long timeout,TimeUnit unit)
  • unLock()

lock()將 Lock 例項鎖定。如果該 Lock 例項已被鎖定,呼叫lock()方法的執行緒將會被阻塞,直到 Lock 例項解鎖。
lockInterruptibly()方法將會被呼叫執行緒鎖定,除非該執行緒將被打斷。此外,如果一個執行緒在通過這個方法來鎖定 Lock 物件時進入阻塞等待,而它被打斷了的話,該執行緒將會退出這個方法呼叫。
tryLock()方法檢視立即鎖定 Lock 例項。如果鎖定成功,它將返回 true,如果 Lock 例項已經被鎖定,則返回 false。這一方法用不阻塞。
tryLock(long timeout,TimeUnit unit)的工作類似於 tryLock()方法,除了它在放棄鎖定 Lock 之前等待一個給定的超時時間之外。
unlock()方法對 Lock 例項解鎖。一個 Lock 實現將只允許鎖定了該物件的執行緒來呼叫此方法。其他執行緒對 unlock()方法呼叫將會丟擲異常。

21.讀寫鎖 ReadWriteLock

讀寫鎖是一種先進的執行緒鎖機制。它能夠允許多個執行緒在同一時間對某特定資源進行讀取,但同一時間內只能有一個執行緒對其進行寫入。
讀寫鎖的理念在於多個執行緒能夠對一個共享資源進行讀取,而不會導致併發問題。併發問題的發生場景在於對一個共享資源的讀和寫操作的同時進行,或者多個讀寫操作併發進行。

ReadWrite Lock 鎖規則

一個執行緒在對受保護資源在讀或者寫之前對 ReadWriteLock 鎖定的規則如下:

  • 讀鎖:如果沒有任何寫操作執行緒鎖定 ReadWriteLock,並且沒有任何寫操作執行緒要求一個寫鎖(但還沒有獲得該鎖)。因此,可以有多個讀操作執行緒對該鎖進行鎖定。
  • 寫鎖:如果沒有任何讀操作或者寫操作。因此,在寫操作的時候,只能有一個執行緒對該鎖進行鎖定。

ReadWriteLock 實現

ReadWriteLock 是個介面,如果你想使用它的話就得去使用它的實現類之一。concurrent 包提供了一個實現類:

  • ReentrantReadWriteLock

ReadWriteLock 程式碼示例
以下是 ReadWriteLock 的建立以及如何使用它進行讀、寫鎖定的簡單示例程式碼:

ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
readWriteLock.readLock().locl();
//乾點事情
readWriteLock.readLock().unlock();

ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
readWriteLock.writeLock().locl();
//乾點事情
readWriteLock.writeLock().unlock();複製程式碼

注意如何使用 ReadWriteLock 對兩種鎖示例的持有。一個對讀訪問進行保護,一個對寫訪問進行保護。

當然,這裡的“讀寫”你可以根據需求靈活變化。

22.原子性布林 AtomicBoolean

AtomicBoolean 類為我們提供了一個可以用原子方式進行讀和諧的布林值,它還擁有一些先進的原子性操作,比如 compareAndSet()。AtomicBoolean 類位於 concurrent.atomic 包。

建立一個 AtomicBoolean

你可以這樣建立一個 AtomicBoolean。
AtomicBoolean atomicBoolean = new AtomicBoolean();
以上示例新建了一個預設值為 false 的 AtomicBoolean。
如果你想要為 AtomicBoolean 示例設定一個顯示的初始值,那麼你可以將初始值傳給 AtomicBoolean 的構造引數。
AtomicBoolean atomicBoolean = new AtomicBoolean(true);

獲取 AtomicBoolean 的值

你可以通過使用 get()方法來獲取一個 AtomicBoolean 的值。示例如下:

AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean value = atomicBoolean.get();複製程式碼

以上程式碼執行後 value 的值為 true。

設定 AtomicBoolean 的值

你可以通過 set() 方法來設定 AtomicBoolean 的值:

AtomicBoolean atomicBoolean = new AtomicBoolean(true);
atomicBoolean.set(false);複製程式碼

互動 AtomicBoolean 的值

你可以通過 getAndSet()方法來交換一個 AtomicBoolean 例項的值。getAndSet()方法將返回 AtomicBoolean 當前的值,並將為 AtomicBoolean 設定一個新值,示例如下:

AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean oldValue = atomicBoolean.getAndSet(false);複製程式碼

以上程式碼執行後 oldValue 變數的值為 true,atomicBoolean 例項將持有 false 值,程式碼成功將 AtomicBoolean 當前值 true 交換為 false。

比較並設定 AtomicBoolean 的值

compareAndSet()方法允許你對 AtomicBoolean 的當前值與與一個期望值進行比較,如果當前值等於期望值的話,將會對 AtomicBoolean 設定一個新值。compareAndSet()方法是原子性質的,因此在同一時間之內有耽擱執行緒執行她。因此 compareAndSet()方法可被用於一些類似於鎖的同步的簡單實現。
以下是一個 compareAndSet()示例:

AtomicBoolean atomicBoolean = new AtomicBoolean(true);
boolean expectedValue = true;
boolean newVaule = false;
boolean wasNewValueSet = atomicBoolean.compareAndSet(expectedValue,newValue);複製程式碼

本示例針對 AtomicBoolean 的當前值與 true 值進行比較,如果相等,將 AtomicBoolean 的值更新為 false。

有什麼用?

可能有些小夥伴到這裡還是有點懵逼,這個原子布林到底有什麼用,給大家看一個示例程式碼:

class XxxService {

    private static AtomicBoolean initState = new AtomicBoolean(false);
    private static AtomicBoolean initFinish = new AtomicBoolean(false);
    private static XxxService instance;

    private XxxService() {
    }

    public static XxxService getInstance() {
        if (initState.compareAndSet(false, true)) {
            //TODO 寫初始化程式碼
            initFinish.set(true);
        }
        while(!initFinish.get()){
            Thread.yield();
        }

        return instance;
    }
}複製程式碼

假如程式需要在多執行緒的情況下初始化一個類,並且保證只初始化一次,完美解決併發問題。

23.原子性整形 AtomicIngteger

同22,略

24.原子性長整型 AtomicBooleanLong

同22,略

25.原子性引用型 AtomicReference

AtomicReference 提供了一個可以被原子性讀和寫的物件引用變數。原子性的意思是多個想要改變同一個 AtomicReference 的執行緒不會導致 AtomicReference 處於不一致的狀態。AtomicReference 還有一個 compareAndSet()方法,通過它你可以將當前引用於一個期望值(引用)進行比較,如果相等,在該 AtomicReference 物件內部設定一個新的引用。

建立一個 AtomicReference

建立 AtomicReference 如下:
AtomicReference atomicReference = new AtomicReference();
如果你需要使用一個指定引用建立 AtomicReference,可以:
String initialReference = "the initialyl reference string"; AtomicReference atomicReference = new AtomicReference(initialReference);

建立泛型 AtomicReference

你可以使用 Java 泛型來建立一個泛型 AtomicReference。示例:
AtomicReference<String> atomicReference = new AtomicReference();
你也可以為泛型 AtomicReference 設定一個初始值。示例:
String initialReference = "the initialyl reference string"; AtomicReference<String> atomicReference = new AtomicReference<>(initialReference);

獲取 AtomicReference 引用

你可以通過 AtomicReference 的 get()方法來獲取儲存在 AtomicReference 裡的引用.

設定 AtomicReference 引用

AtomicReference.set(V newValue);

比較並設定 AtomicReference 引用

使用 compareAndSet()

和 volatile 關鍵字的區別

敲黑板!!!

Atomic 和 volatile的區別很簡單,Atomic 保證讀寫操作同步,但是 volatile 只保證寫的操作,並沒有保證讀的操作同步。

具體原理牽涉到虛擬機器的層次了,感興趣的小夥伴可自行學習。

參考資料

本文主要參考了Java併發工具包java.util.concurrent使用者指南中英文對照閱讀版.pdf, 點選可下載資源。

相關文章