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