6-Java并發容器和框架

1.ConcurrentHashMap

①為什么要使用ConcurrentHashMap

1)線程不安全的HashMap

多線程環境下,使用HashMap進行put操作會引起死循環,導致CPU利用率接近100%。

以下代碼會引起死循環(1.8之前)

        final HashMap<String, String> map = new HashMap<>(2);
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            map.put(UUID.randomUUID().toString(), "");
                        }
                    }, "ftf" + i).start();
                }
            }
        }, "ftf");
        t.start();
        t.join();
        System.out.println("ok");

HashMap在并發執行put操作時會引起死循環,是因為多線程會導致HashMap的Entry鏈表形成環形數據結構,一單形成環形數據結構,Entry的next節點永遠不為空,就會產生死循環獲取Entry。

1.8之前HashMap在并發執行put操作時,需要擴容的時候會出現鏈表形成環形數據結構:1.7擴容代碼

    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;//這一步被掛起,就可能出現環
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

假設原來table的a處,是 k1→k2→null,假設所有的key計算出在新的table中的位置都是b

線程A:

e=k1;

next=k1.next=k2;

線程B:

e=k1;

next=k1.next=k2;k1.next=newTable[b]=null;newTable[b]=k1;e=k2;

next=k2.next=null;k2.next=newTable[b]=k1;newTable[b]=k2;e=null;

//現在順序是:k2→k1→null

線程A:

k1.next=newTable[b]=k2;newTable[b]=k1;e=k2;

//現在順序是:k1→k2→k1

next=k2.next=k1;k2.next=newTable[b]=k1;newTable[b]=k2;e=k1;

next=k1.next=k2;k1.next=newTable[b]=k2;newTable[b]=k1;e=k2;

next=k2.next=k1;k2.next=newTable[b]=k1;newTable[b]=k2;e=k1;

.......//形成死循環

猜測1.8不會形成死循環,這里并不會出現反向更改Node的next引用的情況

                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;//原位置
                        Node<K,V> hiHead = null, hiTail = null;//新位置
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {//不需要改變位置
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {//需要改變位置
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
2)效率低下的HashTable

HashTable容器使用synchronized來保證線程安全,在線程競爭激烈的情況下HashTable的效率非常低下。

3)ConcurrentHashMap的鎖分段技術(1.8之前)可有效提升并發訪問率

1.8之前:容器中有多把鎖,每一把鎖用于鎖容器其中一部分數據,那么當多線程訪問容器里不同數據段的數據時,線程間就不會存在鎖競爭,從而可以有效提高并發訪問率,這個就是鎖分段技術。首先將數據分成一段一段地存儲,然后給每一段數據配一把鎖,。

1.8:鎖粒度降低,如果形成鏈表,以鏈表第一個節點為synchronized的對象。

②ConcurrentHashMap的結構

在JDK1.7版本中,ConcurrentHashMap的數據結構是由一個Segment數組和多個HashEntry組成:

JDK1.8的實現已經摒棄了Segment的概念,而是直接用Node數組+鏈表+紅黑樹的數據結構實現,并發控制使用synchronized和CAS來操作,整個看起來就像是優化過且現場安全的HashMap,雖然在JDK1.8中還能看到Segment的數據結構,但是已經簡化了屬性,只是為了兼容舊版本。

常量設計:

// node數組最大容量:2^30=1073741824
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默認初始值,必須是2的幕數
private static final int DEFAULT_CAPACITY = 16;
//數組可能最大值,需要與toArray()相關方法關聯
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//并發級別,遺留下來的,為兼容以前的版本
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 負載因子
private static final float LOAD_FACTOR = 0.75f;
// 鏈表轉紅黑樹閥值,> 8 鏈表轉換為紅黑樹
static final int TREEIFY_THRESHOLD = 8;
//樹轉鏈表閥值,小于等于6(tranfer時,lc、hc=0兩個計數器分別++記錄原bin、新binTreeNode數量,<=UNTREEIFY_THRESHOLD 則untreeify(lo))
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
private static final int MIN_TRANSFER_STRIDE = 16;
private static int RESIZE_STAMP_BITS = 16;
// 2^15-1,help resize的最大線程數
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 32-16=16,sizeCtl中記錄size大小的偏移量
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// forwarding nodes的hash值
static final int MOVED     = -1;
// 樹根節點的hash值
static final int TREEBIN   = -2;
// ReservationNode的hash值
static final int RESERVED  = -3;
// 可用處理器數量
static final int NCPU = Runtime.getRuntime().availableProcessors();
//存放node的數組
transient volatile Node<K,V>[] table;
/*控制標識符,用來控制table的初始化和擴容的操作,不同的值有不同的含義
 *當為負數時:-1代表正在初始化,-N代表有N-1個線程正在 進行擴容
 *當為0時:代表當時的table還沒有被初始化
 *當為正數時:表示初始化或者下一次進行擴容的大小
private transient volatile int sizeCtl;

內部一些數據結構:

Node是ConcurrentHashMap存儲結構的基本單元,用于存儲數據,數據結構就是一個鏈表,但只允許對數據進行查找,不允許進行修改:

static class Node<K,V> implements Map.Entry<K,V> {
    //鏈表的數據結構
    final int hash;
    final K key;
    //val和next都會在擴容時發生變化,所以加上volatile來保持可見性和禁止重排序
    volatile V val;
    volatile Node<K,V> next;
    Node(int hash, K key, V val, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.val = val;
        this.next = next;
    }
    public final K getKey()       { return key; }
    public final V getValue()     { return val; }
    public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
    public final String toString(){ return key + "=" + val; }
    //不允許更新value 
    public final V setValue(V value) {
        throw new UnsupportedOperationException();
    }
    public final boolean equals(Object o) {
        Object k, v, u; Map.Entry<?,?> e;
        return ((o instanceof Map.Entry) &&
                (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                (v = e.getValue()) != null &&
                (k == key || k.equals(key)) &&
                (v == (u = val) || v.equals(u)));
    }
    //用于map中的get()方法,子類重寫
    Node<K,V> find(int h, Object k) {
        Node<K,V> e = this;
        if (k != null) {
            do {
                K ek;
                if (e.hash == h &&
                    ((ek = e.key) == k || (ek != null && k.equals(ek))))
                    return e;
            } while ((e = e.next) != null);
        }
        return null;
    }
}

TreeNode繼承與Node,但是數據結構換成了二叉樹結構,它是紅黑樹的數據的存儲結構,用于紅黑樹中存儲數據,當鏈表節點數大于8時會轉換成紅黑樹的結構,他就是通過TreeNode作為存儲結構代替Node來轉換成紅黑樹。

static final class TreeNode<K,V> extends Node<K,V> {
    //樹形結構的屬性定義
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red; //標志紅黑樹的紅節點
    TreeNode(int hash, K key, V val, Node<K,V> next,
             TreeNode<K,V> parent) {
        super(hash, key, val, next);
        this.parent = parent;
    }
    Node<K,V> find(int h, Object k) {
        return findTreeNode(h, k, null);
    }
    //根據key查找 從根節點開始找出相應的TreeNode,
    final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {
        if (k != null) {
            TreeNode<K,V> p = this;
            do  {
                int ph, dir; K pk; TreeNode<K,V> q;
                TreeNode<K,V> pl = p.left, pr = p.right;
                if ((ph = p.hash) > h)
                    p = pl;
                else if (ph < h)
                    p = pr;
                else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
                    return p;
                else if (pl == null)
                    p = pr;
                else if (pr == null)
                    p = pl;
                else if ((kc != null ||
                          (kc = comparableClassFor(k)) != null) &&
                         (dir = compareComparables(kc, k, pk)) != 0)
                    p = (dir < 0) ? pl : pr;
                else if ((q = pr.findTreeNode(h, k, kc)) != null)
                    return q;
                else
                    p = pl;
            } while (p != null);
        }
        return null;
    }
}

TreeBin存儲樹形結構的容器,樹形結構就是指TreeNode,所以TreeBin就是封裝TreeNode的容器,它提供轉換黑紅樹的一些條件和鎖的控制。

static final class TreeBin<K,V> extends Node<K,V> {
    //指向TreeNode列表和根節點
    TreeNode<K,V> root;
    volatile TreeNode<K,V> first;
    volatile Thread waiter;
    volatile int lockState;
    // 讀寫鎖狀態
    static final int WRITER = 1; // 獲取寫鎖的狀態
    static final int WAITER = 2; // 等待寫鎖的狀態
    static final int READER = 4; // 增加數據時讀鎖的狀態
    /**
     * 初始化紅黑樹
     */
    TreeBin(TreeNode<K,V> b) {
        super(TREEBIN, null, null, null);
        this.first = b;
        TreeNode<K,V> r = null;
        for (TreeNode<K,V> x = b, next; x != null; x = next) {
            next = (TreeNode<K,V>)x.next;
            x.left = x.right = null;
            if (r == null) {
                x.parent = null;
                x.red = false;
                r = x;
            }
            else {
                K k = x.key;
                int h = x.hash;
                Class<?> kc = null;
                for (TreeNode<K,V> p = r;;) {
                    int dir, ph;
                    K pk = p.key;
                    if ((ph = p.hash) > h)
                        dir = -1;
                    else if (ph < h)
                        dir = 1;
                    else if ((kc == null &&
                              (kc = comparableClassFor(k)) == null) ||
                             (dir = compareComparables(kc, k, pk)) == 0)
                        dir = tieBreakOrder(k, pk);
                        TreeNode<K,V> xp = p;
                    if ((p = (dir <= 0) ? p.left : p.right) == null) {
                        x.parent = xp;
                        if (dir <= 0)
                            xp.left = x;
                        else
                            xp.right = x;
                        r = balanceInsertion(r, x);
                        break;
                    }
                }
            }
        }
        this.root = r;
        assert checkInvariants(root);
    }
    ......
}

③ConcurrentHashMap的初始化

JDK1.7 ConcurrentHashMap的初始化會通過位與運算來初始化Segment的大小,用ssize來標識,如下:

        int sshift = 0;
        int ssize = 1;
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }

concurrencyLevel的最大值是65535,這意味著segments數組的長度最大為65536,對應的二進制是16位。

④ConcurrentHashMap的操作

參考:https://www.cnblogs.com/study-everyday/p/6430462.html

2.ConcurrentLinkedQueue

并發編程中,有時候需要使用線程安全的隊列。如果要實現一個線程安全的隊列有兩種方式:一種是使用阻塞算法,另一種是使用非阻塞算法。

使用阻塞算法的隊列,可以用一個鎖(入隊和出隊用同一把鎖)或兩個鎖(入隊和出隊用不同的鎖)等方式來實現。

非阻塞的實現方式則可以使用循環CAS的方式來實現。

ConcurrentLinkedQueue是使用非阻塞的方式來實現線程安全隊列的。它是一個基于鏈接節點的無界線程安全隊列,采用先進先出的規則對節點進行排序,當我們添加一個元素的時候,它會添加到隊列的尾部;當我們獲取一個元素時,它會返回隊列頭部的元素。

①ConcurrentLinkedQueue的結構

②入隊列

入隊列的過程

    public boolean offer(E e) {
        checkNotNull(e);
        //入隊前,創建一個入隊節點
        final Node<E> newNode = new Node<E>(e);
        //死循環,入隊不成功反復入隊
        for (Node<E> t = tail, p = t;;) {
        //創建一個指向tail節點的引用,用p來標識隊列的尾節點,默認情況下等于tail節點。
            Node<E> q = p.next;//獲取p節點的下一個節點
            if (q == null) {//p節點是尾節點
                if (p.casNext(null, newNode)) {//設置p節點的next節點為入隊節點
                    if (p != t) // t已經不是尾節點了,t=tail,p經過循環后已經改變。
                        casTail(t, newNode);  // 更新tail節點,允許失敗
                    return true;
                }
                //另一個線程CAS成功了,重新讀取下一個
            }
            else if (p == q)
                //我們已經脫離了隊列。如果tail沒有變化,它也已經脫離了隊列,在這種情況下,我們需要跳到頭部,否則跳到尾部
                p = (t != (t = tail)) ? t : head;
            else
                //檢查tail更新
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

入隊列就是將入隊節點添加到隊列的尾部。tail節點不總是尾節點。入隊方法永遠返回true,所以不要通過返回值判斷入隊是否成功。

在一個隊列中依次插入4個節點,示例:

  • 添加元素1。隊列更新head節點的next節點為元素1節點。又因為tail節點默認情況下等于head節點,所以它們的next節點都指向元素1節點。
  • 添加元素2。隊列首先設置元素1節點的next節點為元素2節點,然后更新tail節點指向元素2節點。(會跳過1節點)。
  • 添加元素3。設置tail節點的next節點為元素3節點。
  • 添加元素4。設置元素3的next節點為元素4節點,然后將tail節點指向元素4節點。

③出隊列

    public E poll() {
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                E item = p.item;//獲取p節點的元素
                //p節點的元素不為空,使用CAS設置p節點引用的元素為null,如果CAS成功,返回p節點的元素
                if (item != null && p.casItem(item, null)) {
                    if (p != h) // 一次跳躍兩個節點
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    return item;
                }
                else if ((q = p.next) == null) {//如果p的下一個節點也為空,說明這個隊列已經空了。
                    updateHead(h, p);
                    return null;
                }
                else if (p == q)
                    continue restartFromHead;
                else
                    p = q;
            }
        }
    }

出隊列的就是從隊列里返回一個幾點元素,并清空該節點對元素的引用。

3.Java中的阻塞隊列

①什么是阻塞隊列

阻塞隊列(BlockingQueue)是一個支持兩個附加操作的隊列。這兩個附加的操作支持阻塞的插入和移除方法。

1)支持阻塞的插入方法:意思是當隊列滿時,隊列會阻塞插入元素的線程,知道隊列不滿。

2)支持阻塞的移除方法:意思是在隊列為空時,獲取元素的線程會等待隊列變為非空。

阻塞隊列常用于生產者和消費者的場景,生產者是向隊列里添加元素的線程,消費者是從隊列里取元素的線程。阻塞隊列就是生產者用來存放元素、消費者用來獲取元素的容器。

在阻塞隊列不可用時,這兩個附加操作提供了4種處理方式:

  • 拋出異常:當隊列滿時,如果再往隊列里插入元素,會拋出IllegalStateException(“Queue full”)異常。當隊列空時,從隊列里獲取元素會拋出NoSuchElementException異常。(AbstractQueue)
  • 返回特殊值:往隊列里插入元素時,成功返回true。移除方法取出一個元素,如果沒有則返回null。
  • 一直阻塞:當阻塞隊列滿時,如果生產者線程往隊列里put元素,隊列會一直阻塞生產者線程,知道隊列可用或者響應中斷退出。當隊列空時,如果消費者線程從隊列里take元素,隊列會阻塞住消費者線程,直到隊列不為空。
  • 超時退出:當阻塞隊列滿時,如果生產者線程往隊列里插入元素,隊列會阻塞生產者線程一點時間,如果超過了指定的時間,生產者線程就會退出。

注意:無界阻塞隊列,隊列不可能出現滿的情況。

②Java里的阻塞隊列

  • ArrayBlockingQueue:一個由數組結構組成的有界阻塞隊列。
  • LinkedBlockingQueue:一個由鏈表結構組成的有界阻塞隊列。
  • PriorityBlockingQueue:一個支持優先級排序的無界阻塞隊列。
  • DelayQueue:一個使用優先級隊列實現的支持延時獲取元素的無界阻塞隊列。
  • SynchronousQueue:一個不存儲元素的阻塞隊列。
  • LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。
  • LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。
1)ArrayBlockingQueue

是一個用數組實現的有界阻塞隊列。此隊列按照先進先出(FIFO)的原則對元素進行排序。用重入鎖ReentrantLock來實現線程訪問隊列的公平性。

    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();
    }
2)LinkedBlockingQueue

一個用來鏈表實現的有界阻塞隊列。此隊列的默認和最大長度為Integer.MAX_VALUE。

3)PriorityBlockingQueue

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

4)DelayQueue

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

適用于以下場景:

  • 緩存系統的設計:可以保存緩存元素的有效期,使用一個線程循環查詢DelayQueue,一旦能從DelayQueue中獲取元素時,表示緩存有效期到了。
  • 定時任務調度:使用DelayQueue保存當天將會執行的任務和執行時間,一旦從DelayQueue中獲取到任務就開始執行,比如TimerQueue就是使用DelayQueue實現的。

a.如何實現Delayed接口

參考java.util.concurrent.ScheduledThreadPoolExecutor.ScheduledFutureTask類的實現,一共有三步:

  • 在對象創建的時候,初始化基本數據。使用time記錄當前對象延遲到什么時候可以使用,使用sequenceNumber來標識元素在隊列中的先后順序。

            private static final AtomicLong sequencer = new AtomicLong(0);
            ScheduledFutureTask(Runnable r, V result, long ns, long period) {
                super(r, result);
                this.time = ns;
                this.period = period;
                this.sequenceNumber = sequencer.getAndIncrement();
            }
    

    ?

  • 實現getDelay方法,該方法返回當前元素還需要延時多長時間,單位是納秒。當time小于當前時間時,getDelay會返回負數。

            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);
            }
    

b.如何實現延時阻塞隊列

當消費者從隊列里獲取元素時,如果元素沒有達到延時時間,就阻塞當前線程。

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            for (;;) {
                E first = q.peek();
                if (first == null)
                    available.await();
                else {
                    long delay = first.getDelay(TimeUnit.NANOSECONDS);
                    if (delay <= 0) //到時間了
                        return q.poll();
                    else if (leader != null) //leader是等待獲取隊列頭部元素的線程不為空,表示已經有線程在等待獲取隊列的頭元素。
                        available.await();
                    else {
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;//將當前線程設置成leader
                        try {
                            available.awaitNanos(delay);//等待delay時間
                        } finally {
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
            if (leader == null && q.peek() != null)
                available.signal();
            lock.unlock();
        }
    }
5)SynchronousQueue

一個不存儲元素的阻塞隊列。每一個put操作必須等待一個take操作,否則不能繼續添加元素。默認采用非公平性策略訪問隊列。

    public SynchronousQueue(boolean fair) {//true則等待的線程會采用先進先出的順序訪問隊列
        transferer = fair ? new TransferQueue() : new TransferStack();
    }

SynchronousQueue負責把生產者線程處理的數據直接傳遞給消費者線程。隊列本身不存儲元素,適合傳遞性場景。吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue。

6)LinkedTransferQueue

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

a.transfer方法

  • 當前有消費者正在等待接收元素(消費者使用take方法或帶時間限制的poll方法時),transfer方法可以把生產者傳入的元素liketransfer(傳輸)給消費者。
  • 沒有消費者在等待接收元素,transfer方法會將元素采暖費在隊列的tail節點,并等到該元素被消費者消費了才返回。

關鍵代碼如下:

Node pred = tryAppend(s, haveData);//試圖把存放當前元素的s節點作為tail節點。
return awaitMatch(s, pred, e, (how == TIMED), nanos);//讓CPU自旋等待消費者消費元素。因為自旋會消耗CPU,所以自旋一定次數后使用Thread.yield()方法來暫停當前正在執行的線程,并執行其他線程。

b.tryTransfer方法

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

和transfer方法的區別是無論消費者是否接收,方法立即返回。

tryTransfer(E e, long timeout, TimeUnit unit),試圖把生產者傳入的元素直接傳遞給消費者。但如果沒有消費者消費該元素,則等待指定的時間再返回,如果超時還沒有消費元素,返回false,超時時間內消費了元素,返回true。

7)LinkedBlockingDeque

一個由鏈表結構組成的雙向阻塞隊列??梢詮年犃械膬啥瞬迦牒鸵瞥鲈?,多線程同時入隊時,減少了一半訂單競爭。相比其他阻塞隊列,多了addFirst、addLast、offerFirst、offerLast、peekFirst、peekLast等方法。

以first結尾的方法,表示插入、獲?。╬eek)或移除雙端隊列的第一個元素。以last結尾的方法表示插入、獲?。╬eek)或移除雙端隊列的最后一個元素。

另外,插入方法add等同于addLast,remove等效于removeFirst。

初始化時可以設置容量防止其過度膨脹。另外,雙向阻塞隊列可以運用在“工作竊取”模式中。

③阻塞隊列的實現原理

1)使用通知模式實現。

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

    private final Condition notEmpty;
    private final Condition notFull;
    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();
    }
    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
    private void enqueue(E x) {
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal();
    }

4.Fork/Join框架

①什么是Fork/Join框架

Fork/Join框架是Java 7 提供的一個用于并行執行任務的框架,是一個把大任務分割成若干個小任務,最終匯總每個小任務結果后得到大任務結果的框架。

②工作竊取算法

工作竊?。╳ork-stealing)算法是指某個線程從其他隊列里竊取任務來執行。

每個線程一個隊列,當前線程將任務執行完,而其他線程對應的隊列里還有任務等待處理。干完活的線程與其等著,不如去幫其他線程干活。為了減少竊取任務線程和被竊取任務線程之間的競爭,通常會使用雙端隊列,被竊取任務線程永遠從雙端隊列的頭部拿任務執行,而竊取任務的線程永遠從雙端隊列的尾部拿任務執行。

工作竊取算法的優點:充分利用線程進行并行計算,減少了線程間的競爭。

工作竊取算法的缺點:在某些情況下還是存在競爭,比如雙端隊列里只有一個任務時。并且該算法會消耗更多的系統資源,比如創建多個線程和多個雙端隊列。

③Fork/Join框架的設計

  • 分割任務。需要有一個fork類來把大任務分割成子任務,有可能子任務還是很大,所以還需要不停地分割,直到分割出的子任務足夠小。
  • 執行任務并合并結果。分割的子任務分別放在雙端隊列里,然后幾個啟動線程分別從雙端隊列里獲取任務執行。子任務執行完的結果都統一放在一個隊列里,啟動一個線程從隊列里拿數據,然后合并這些數據。

Fork/Join框架使用兩個類來完成以上兩件事情:

1)ForkJoinTask:使用Fork/Join框架,先創建一個ForkJoin任務。它提供在任務中執行fork和join操作的機制。一般我們不需要直接繼承ForkJoinTask類,只需要繼承它的子類,Fork/Join框架提供了以下子類:

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

2)ForkJoinPool:ForkJoinTask需要通過ForkJoinPool來執行。

任務分割出的子任務會添加到當前工作線程所維護的雙端隊列中,進入隊列的頭部。當一個工作線程的隊列里暫時沒有任務時,它會隨機從其他工作線程的隊列的尾部獲取一個任務。

④使用Fork/Join框架

示例(計算1+2+3+4):

public class CountTask extends RecursiveTask<Integer> {
    private 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();//執行fork方法時,又會進入compute方法
            rightTask.fork();
            //等待子任務執行完,并得到其結果
            Integer leftResult = leftTask.join();//join方法會等待子任務執行完并得到其結果
            Integer rightResult = rightTask.join();
            //合并子任務
            sum = leftResult + rightResult;
        }
        return sum;
    }

    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        //生成一個計算任務,負責計算
        CountTask task = new CountTask(1,4);
        //執行一個任務
        ForkJoinTask<Integer> result = forkJoinPool.submit(task);
        try {
            System.out.println(result.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

⑤Fork/Join框架的異常處理

ForkJoinTask在執行的時候可能會拋出異常,但我們在主線程里沒辦法直接捕獲異常,所以ForkJoinTask提供了isCompletedAbnormally()方法來檢查任務是否已經拋出異常或已經被取消了,并且可以通過ForkJoinTask的getException()方法獲取異常。

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

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

⑥Fork/Join框架的實現原理(1.7)

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

1)ForkJoinTask的fork方法實現原理

    public final ForkJoinTask<V> fork() {
        ((ForkJoinWorkerThread) Thread.currentThread())
            .pushTask(this);//將當前任務存放在ForkJoinTask數組隊列里。
        return this;
    }
    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() {
        //通過doJoin方法得到當前任務的狀態
        //任務狀態有4種:已完成(NORMAL)、被取消(CANCELLED)、信號(SIGNAL)、出現異常(EXCEPTIONAL)
        if (doJoin() != NORMAL) 
            return reportResult();
        else
            return getRawResult(); //任務狀態是已完成,直接返回任務結果
    }
    private V reportResult() {
        int s; Throwable ex;
        if ((s = status) == CANCELLED)  //任務狀態是被取消,拋出CancellationException
            throw new CancellationException();
        if (s == EXCEPTIONAL && (ex = getThrowableException()) != null) //任務狀態是拋出異常,則直接拋出對應的異常
            UNSAFE.throwException(ex);
        return getRawResult();
    }
    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);//執行出現異常,記錄異常,并將任務狀態設置為EXCEPTIONAL
                }
                if (completed)
                    return setCompletion(NORMAL);//任務順利執行完成,設置任務狀態為NORMAL
            }
            return w.joinTask(this);
        }
        else
            return externalAwaitDone();
    }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。