【Java多執行緒】執行緒安全的集合
執行緒安全的集合
Vector
Vector集合是對ArrayList集合執行緒安全的實現,它們兩者在方法的實現上沒有什麼太大的區別,最大的區別就是,Vector在方法前面加上了synchronized關鍵字,用於保證執行緒安全。
Vector存在的問題:
- 1、它的add()和get()方法都能夠獲取當前Vector物件的物件鎖,但是有可能會發生讀讀互斥。
- 2、當threadA在1下標處新增一個元素,threadB在2下標處修改一個元素時,同樣有可能會發生互斥現象。
Vector v = new Vector();
thread1: v.add(100, 1);
thread2: v.set(50, 2);
因此,我們可以看出Vector所存在的鎖的粒度是非常大的,這也就會導致在多執行緒情況下,程式執行的效率有可能會十分低下。
HashTable
HashTable集合是對HashMap集合執行緒安全的實現,它們兩者在方法的實現上沒有什麼太大的區別,最大的區別就是,HashTable在方法前面加上了synchronized關鍵字,用於保證執行緒安全。
HashTable存在的問題:
- 由於HashTable和Vector在本質上都是在方法前面加上synchronized關鍵字,因此,它們兩個存在的問題也是同樣相同的,均有可能發生互斥現象。
- 由此可知,HashTable所存在的鎖的粒度也是非常大的,也同樣會導致在多執行緒情況下,程式執行的效率有可能會十分低下。
為了解決Vector集合和HashTable集合效率低下的問題,我們在選取執行緒安全的集合時一般會選擇CopyOnWriteArrayList
集合和ConcurrentHashMap
集合,它的鎖的粒度相較於Vector和HashTable更小,因此能夠高效率的解決Vector和HashTable所存在的問題。
ConcurrentHashMap
ConcurrentHashMap是Java中的一個執行緒安全且高效的HashMap實現。平時涉及高併發如果要用map結構,那第一時間想到的就是它。
我們從以下幾個方面來了解一下ConcurrentHashMap:
- 1、ConcurrentHashMap在JDK8裡的結構。
- 2、ConcurrentHashMap的put方法、szie方法等。
- 3、ConcurrentHashMap的擴容。
- 4、HashMap、Hashtable、ConccurentHashMap三者的區別。
- 5、ConcurrentHashMap在JDK7和JDK8的區別。
相關連結:
ConcurrentHashMap在JDK8裡結構
CurrentHashMap與HashMap的底層結構一致,都是基於陣列+連結串列+紅黑樹進行實現。
那麼它是如何保證執行緒安全的呢?
- 答案:其中拋棄了原有JDK1.7的Segment分段鎖,而採用了 CAS + synchronized 來保證併發安全性。
現在我們來解決另一個問題,為什麼HashMap不是執行緒安全的。
- 因為HashMap在多執行緒環境中很有可能產生死迴圈(注意不是死鎖)。
在連續的put時,會發生擴容,當擴容時就會產生讀取節點和移動節點,再次期間,並沒有對訪問進行一個控制,所以每一次在擴容時遍歷的節點,可能完全不相同,那麼這樣很有可能產生一個 A -> B -> A的情況,這樣就導致了死迴圈。
ConcurrentHashMap的重要變數以及方法
1、table
/**
* The array of bins. Lazily initialized upon first insertion.
* Size is always a power of two. Accessed directly by iterators.
*/
transient volatile Node<K,V>[] table;
裝載 Node 的陣列,作為 ConcurrentHashMap的資料容器,採用懶載入的方式,直到第一次插入資料的時候才會進行初始化操作,陣列的大小總是為 2 的冪次方。
2、nextTable
/**
* The next table to use; non-null only while resizing.
*/
private transient volatile Node<K,V>[] nextTable;
擴容時新生成的陣列,大小為原陣列的2倍。平時為null,只有在擴容的時候才為非null。
3、sizeCtl
/**
* Table initialization and resizing control. When negative, the
* table is being initialized or resized: -1 for initialization,
* else -(1 + the number of active resizing threads). Otherwise,
* when table is null, holds the initial table size to use upon
* creation, or 0 for default. After initialization, holds the
* next element count value upon which to resize the table.
*/
private transient volatile int sizeCtl;
該屬性用來控制 table 陣列的大小,根據是否初始化和是否正在擴容有幾種情況:
- 當值為負數時:如果為 -1 表示正在初始化 ,如果為 -N 則表示當前正有 N-1 個執行緒進行擴容操作。
- 當值為正數時:如果當前陣列為 null 的話表示 table 在初始化過程中,sizeCtl 表示為需要新建陣列的長度。
- 若已經初始化了,表示當前資料容器(table 陣列)可用容量也可以理解成臨界值(插入節點數超過了該臨界值就需要擴容),具體指為陣列的長度
n
乘以 載入因子loadFactor
; - 預設值為0,當table被初始化後,sizeCtl的值為下一次要擴容時元素個數。
sun.misc.Unsafe U
在 ConcurrentHashMapde 的實現中可以看到大量的U.compareAndSwapXXXX 的方法去修改 ConcurrentHashMap 的一些屬性。這些方法實際上是利用了 CAS 演算法保證了執行緒安全性,這是一種樂觀策略,假設每一次操作都不會產生衝突,當且僅當衝突發生的時候再去嘗試。而 CAS 操作依賴於現代處理器指令集,通過底層CMPXCHG指令實現。
CAS(V,O,N)核心思想為:若當前變數實際值 V 與期望的舊值 O 相同,則表明該變數沒被其他執行緒進行修改,因此可以安全的將新值 N 賦值給變數;若當前變數實際值 V 與期望的舊值 O 不相同,則表明該變數已經被其他執行緒做了處理,此時將新值 N 賦給變數操作就是不安全的,在進行重試。而在大量的同步元件和併發容器的實現中使用 CAS 是通過sun.misc.Unsafe類實現的,該類提供了一些可以直接操控記憶體和執行緒的底層操作,可以理解為 java 中的“指標”。該成員變數的獲取是在靜態程式碼塊中:
static {
try {
U = sun.misc.Unsafe.getUnsafe();
.......
} catch (Exception e) {
throw new Error(e);
}
}
- 也就是說通過Unsafe獲取的值,都是從主記憶體去獲取的。
4、Node
Node 類實現了 Map.Entry 介面,主要存放 key-value
對,並且具有 next 域。
/**
* Key-value entry. This class is never exported out as a
* user-mutable Map.Entry (i.e., one supporting setValue; see
* MapEntry below), but can be used for read-only traversals used
* in bulk tasks. Subclasses of Node with a negative hash field
* are special, and contain null keys and values (but are never
* exported). Otherwise, keys and vals are never null.
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
......
}
5、TreeNode
樹節點,用於儲存紅黑樹節點,繼承於承載資料的 Node 類。而紅黑樹的操作是針對 TreeBin 類的,從該類的註釋也可以看出,也就是TreeBin 會將 TreeNode 進行再一次封裝。
/**
* Nodes for use in TreeBins
*/
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;
......
}
6、TreeBin
這個類並不負責包裝使用者的 key、value 資訊,而是包裝的很多 TreeNode 節點。實際的 ConcurrentHashMap“陣列”中,存放的是 TreeBin 物件,而不是 TreeNode 物件。
/**
* TreeNodes used at the heads of bins. TreeBins do not hold user
* keys or values, but instead point to list of TreeNodes and
* their root. They also maintain a parasitic read-write lock
* forcing writers (who hold bin lock) to wait for readers (who do
* not) to complete before tree restructuring operations.
*/
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> first;
volatile Thread waiter;
volatile int lockState;
// values for lockState
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4; // increment value for setting read lock
......
}
7、ForwardingNode
在擴容時才會出現的特殊節點,其 key,value,hash 全部為 null。並擁有 nextTable 指標引用新的 table 陣列。 可以把此節點看成標記節點,如果,table的某一個節點被標記為ForwardingNode,那麼此節點正在被一個執行緒執行擴容操作。
CAS操作:
//該方法用來獲取 table 陣列中索引為 i 的 Node 元素。
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
//利用 CAS 操作設定 table 陣列中索引為 i 的元素
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
//該方法用來設定 table 陣列中索引為 i 的元素,非CAS操作
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
ConcurrentHashMap的常用方法剖析
新增方法 put()
首先我們先看一張put方法的圖解。
相信看完這篇圖解,再讀原始碼就會由一個大致的瞭解了。
原始碼,以及解釋:
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//1. 計算key的hash值
//spread(就是擾動函式),讓hashcode右移32位進行異或操作,來減少hash衝突
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//2. 如果當前table還沒有初始化先呼叫initTable方法將tab進行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//3. tab中索引為i的位置的元素為null,則直接使用CAS將值插入即可
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//4. 當前正在擴容
else if ((fh = f.hash) == MOVED)
//當前執行緒去輔助擴容。
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
//5. 當前為連結串列,在連結串列中插入新的鍵值對
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 6.當前為紅黑樹,將新的鍵值對插入到紅黑樹中
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// 7.插入完鍵值對後再根據實際大小看是否需要轉換成紅黑樹
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//8.對當前容量大小進行檢查,如果超過了臨界值(實際大小*載入因子)就需要擴容
addCount(1L, binCount);
return null;
}
初始化方法 initTable()
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
// 1.sizeCtl < 0表示其他執行緒也正在初始化,
//保證只有一個執行緒正在進行初始化操作,所以讓出時間片
Thread.yield(); // lost initialization race; just spin
//沒有其他執行緒進行操作,那麼就直接將sizeCtl置為-1。
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
// 2. 得出陣列的大小
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
// 3. 這裡才真正的初始化陣列
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// 4. 計算陣列中可用的大小:實際大小n*0.75(載入因子)
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
ConcurrentHashMap的擴容
通過判斷該節點的hash值是不是等於-1(MOVED)
,程式碼為(fh = f.hash) == MOVED
,說明 Map 正在擴容。那麼就幫助 Map 進行擴容。以加快速度。
helpTransfer(Node<K,V>[] tab, Node<K,V> f)
就是協助擴容的方法。這裡我們就能看出ConcurrentHashMap
設計的精妙之處了,執行緒不僅可以進行增刪改查,甚至可以去協助擴容,來減少擴容時移動資料的大量操作對阻塞時間的影響。讓多個執行緒一起完成擴容,使得擴容速度非常的快,不僅僅減少了擴容需要的時間,還合理的利用了執行緒資源。這種想法屬實太強了。
首先我們來看一下作為擴容的入口點,也就是什麼時候擴容呢?
- 就是當節點的個數等於 SizeCtl 的時候擴容,擴容依舊是2倍擴容。那麼統計節點個數的方法就是擴容方法的入口點。也就是addCount()。
addCount()方法
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//通過CAS更新baseCount,table的數量,counterCells表示元素個數的變化
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
//如果多個執行緒都在執行,則CAS失敗,執行fullAddCount,全部加入count
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
//check>=0表示需要進行擴容操作
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
//這裡才開始進入擴容。
transfer(tab, nt);
}
//當前執行緒發起操作,nextTable=null
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
實際上addCount的原理,很簡單,統計並更新所有節點個數,更新時使用的是CAS操作。然後進行檢查,檢視當前是否需要擴容,如果需要擴容,進入transfer()方法中。
transfer()方法
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
//1. 新建Node陣列,容量為之前的兩倍
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
//2. 新建forwardingNode引用,在之後會用到
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 3. 確定遍歷中的索引i
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
//4.將原陣列中的元素複製到新陣列中去
//4.5 for迴圈退出,擴容結束脩改sizeCtl屬性
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
//4.1 當前陣列中第i個元素為null,用CAS設定成特殊節點forwardingNode(可以理解成佔位符)
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//4.2 如果遍歷到ForwardingNode節點 說明這個點已經被處理過了 直接跳過 這裡是控制併發擴容的核心
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {
//4.3 處理當前節點為連結串列的頭結點的情況,構造兩個連結串列,一個是原連結串列 另一個是原連結串列的反序排列
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
//在nextTable的i位置上插入一個連結串列
setTabAt(nextTab, i, ln);
//在nextTable的i+n的位置上插入另一個連結串列
setTabAt(nextTab, i + n, hn);
//在table的i位置上插入forwardNode節點 表示已經處理過該節點
setTabAt(tab, i, fwd);
//設定advance為true 返回到上面的while迴圈中 就可以執行i--操作
advance = true;
}
//4.4 處理當前節點是TreeBin時的情況,操作和上面的類似
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
程式碼邏輯請看註釋,整個擴容操作分為兩個部分:
- 第一部分:構建一個 nextTable,它的容量是原來的兩倍,這個操作是單執行緒完成的。新建 table 陣列的程式碼為:
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]
,在原容量大小的基礎上右移一位。 - 第二個部分:就是將原來 table 中的元素複製到 nextTable 中,主要是遍歷複製的過程。
根據運算得到當前遍歷的陣列的位置 i,然後利用 tabAt 方法獲得 i 位置的元素再進行判斷:
- 1、如果這個位置為空,就在原 table 中的 i 位置放入 forwardNode 節點,這個也是觸發併發擴容的關鍵點。
- 2、如果這個位置是 Node 節點(fh>=0),如果它是一個連結串列的頭節點,就構造一個反序連結串列,把他們分別放在 nextTable 的 i 和 i+n 的位置上。
- 3、如果這個位置是 TreeBin 節點(fh<0),也做一個反序處理,並且判斷是否需要 untreefi,把處理的結果分別放在 nextTable 的 i 和 i+n 的位置上。
- 4、遍歷過所有的節點以後就完成了複製工作,這時讓 nextTable 作為新的 table,並且更新 sizeCtl 為新容量的 0.75 倍 ,完成擴容。
設定為新容量的 0.75 倍程式碼為 sizeCtl = (n << 1) - (n >>> 1),仔細體會下是不是很巧妙,n<<1 相當於 n 左移一位表示 n 的兩倍即 2n,n>>>1,n 右移相當於 n 除以 2 即 0.5n,然後兩者相減為 2n-0.5n=1.5n,是不是剛好等於新容量的 0.75 倍即 2n*0.75=1.5n。
HashMap、Hashtable、ConccurentHashMap三者的區別
- HashMap:非執行緒安全,允許NULL值與NULL鍵。預設大小為16,擴容為2倍擴容。
- HashTable:執行緒安全,不允許NULL值與NULL鍵,預設大小為11,擴容為2倍+1擴容。HashTable的執行緒安全實現依靠Synchronized。
- ConcurrentHashMap:執行緒安全,不允許NULL值與NULL鍵,預設大小為16,擴容為2倍擴容。ConcurrentHashMap的執行緒安全實現依靠於Synchronized + CAS 。
HashMap不應用於併發場景,會產生死迴圈,HashTable於ConcurrentHashMap運用於併發場景,但是兩者有效能差距。當資料量足夠大時,我們會發現ConcurrentHashMap的效率實際上比HashTable要低下一些,但是關於讀操作,ConcurrentHashMap比HashTable快不止一個量級。
所以適用於什麼場景關鍵還是需要進行壓力測試,才可以斷言需要用什麼樣的容器。
- (Collections.synchronizedMap(new HashMap()); 可以將普通的hashMap變為執行緒安全的HashMap。)
ConcurrentHashMap在JDK7和JDK8的區別
ConcurrentHashMap在JDK7版本中實現的是Segment分段鎖,一個Segment鎖上一個或者幾個table節點。當要對指定的節點上的資料進行操作時,先獲取對應的Segement的鎖才可以,而這種鎖的粒度相對較大,並且採用 ReetrantLock 的方式去獲取與釋放鎖。
JDK8版本中實現拋棄了原來的Segment分端鎖,轉而用鎖table的節點,也就是鎖連結串列頭或者 樹的根節點。這種轉變直接將鎖的粒度變小,使得執行緒的衝突變少,並且支援多執行緒協助擴容,使用3個CAS操作來確保 node 的一些操作的原子性,這種方式代替了鎖。
相關文章
- Java 多執行緒基礎(四)執行緒安全Java執行緒
- Java多執行緒-執行緒中止Java執行緒
- java各種集合的執行緒安全Java執行緒
- 多執行緒系列之 執行緒安全執行緒
- iOS 多執行緒之執行緒安全iOS執行緒
- Java多執行緒-執行緒池的使用Java執行緒
- Java多執行緒中執行緒安全與鎖問題Java執行緒
- Java多執行緒-執行緒狀態Java執行緒
- Java多執行緒-執行緒通訊Java執行緒
- java多執行緒9:執行緒池Java執行緒
- java 多執行緒守護執行緒Java執行緒
- Java多執行緒(2)執行緒鎖Java執行緒
- 【java多執行緒】(二)執行緒停止Java執行緒
- Java多執行緒之執行緒中止Java執行緒
- 【多執行緒總結(二)-執行緒安全與執行緒同步】執行緒
- java多執行緒之執行緒的基本使用Java執行緒
- 【Java】【多執行緒】執行緒的生命週期Java執行緒
- Java併發-執行緒安全的集合類Java執行緒
- 【Java】【多執行緒】執行緒池簡述Java執行緒
- 【Java多執行緒】輕鬆搞定Java多執行緒(二)Java執行緒
- iOS多執行緒安全-13種執行緒鎖?iOS執行緒
- Java多執行緒(一)多執行緒入門篇Java執行緒
- Java執行緒(一):執行緒安全與不安全Java執行緒
- java多執行緒Java執行緒
- Java - 多執行緒Java執行緒
- java——多執行緒Java執行緒
- java 多執行緒Java執行緒
- Java執行緒安全Java執行緒
- java多執行緒5:執行緒間的通訊Java執行緒
- .Net 執行緒安全集合執行緒
- 執行緒與多執行緒執行緒
- 多執行緒【執行緒池】執行緒
- 多執行緒--執行緒管理執行緒
- 多執行緒------執行緒與程式/執行緒排程/建立執行緒執行緒
- Java多執行緒學習(一)Java多執行緒入門Java執行緒
- Java多執行緒學習——執行緒通訊Java執行緒
- Java多執行緒學習(2)執行緒控制Java執行緒
- 最全java多執行緒總結3——瞭解阻塞佇列和執行緒安全集合不Java執行緒佇列