JDK 1.8 ConccurrenthashMap拡大実現原理
7689 ワード
1.8のconcurrenthashMap拡張メカニズムは、マルチスレッドをサポートしながら、古いハッシュ配列の拡張を行うロックなしに併発される。マルチスレッドシーンでスレッドの安全をどう保証しますか?具体的な分析:
n=tab.length=16;nextN=nextTab.length=32;stride=16transferIndex=16;
初めてのwhile: nextIndex=16、next Bound=0; --->bound=next Bound=0 ---> i=nextIndex-1=15;(16-1) 二番目: --i==14,bound=0(16-2) advance=false; 3回目: --i=13,bound=0;(16-3) advance=false; ... 16回目: --i=0、bound=0 advance=false;
つまり、スレッドは、インデックス0まで古いハッシュ配列の最後のインデックスから巡回し始めます。シングルスレッドでもマルチスレッドでも実行される順序はこのようになります。また、resizeプロセスでは、oldtabが設定されている頭の結点はForwardingNodeであり、マルチスレッド環境でも、他のスレッドが再び巡回しても、このインデックス位置がすでにForwadingNode(ノードのハッシュ値は−1)であると簡単に判断すれば、他のスレッドはインデックス位置をスキップします。次のインデックス位置を取得し、同じプロセスを実行して巡回します。
resizeプロセスでは、スレッドがいくつあっても、あるインデックス位置にノードがあれば、この位置はスレッドだけでrehashされます。インデックス位置要素がrehashにある場合、スレッドはまず、ヘッドノードのモニタロックを取得し、取得に成功してこそ、rehashを行うことができます。
1.8 hashmapのインデックスポジショニング原理と似ていますが、具体的には「hashMap実現原理」を参照してください。rehashの後、元の位置または元のハッシュテーブルの長さですが、rehashの過程では、各インデックスの要素位置は古いテーブルと逆です。ConccurrenthashMap拡張操作要素は逆さまです。
以上、1.8のCurrennthashMapの拡大プロセスです。
private final void transfer(Node[] tab, Node[] nextTab) {
// stride, CPU , 16
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// , 2
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
Node[] nt = (Node[])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;
// rehash , ForwardingNode
// rehash, ,
//
ForwardingNode fwd = new ForwardingNode(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
Node f; int fh;
//while , tab.length-1... 0.
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;
}
}
// ,rehash
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
}
}
// null, fwd
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
// rehash, ,
synchronized (f) {
if (tabAt(tab, i) == f) {
Node ln, hn;
if (fh >= 0) {
int runBit = fh & n;
Node lastRun = f;
for (Node 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 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(ph, pk, pv, ln);
else
hn = new Node(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
else if (f instanceof TreeBin) {
TreeBin t = (TreeBin)f;
TreeNode lo = null, loTail = null;
TreeNode hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode p = new TreeNode
(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(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
単一スレッドシナリオでは、ハッシュ配列はデフォルトの初期サイズが16であると仮定し、最初に拡張をトリガし、拡張プロセスを参照してください。n=tab.length=16;nextN=nextTab.length=32;stride=16transferIndex=16;
初めてのwhile: nextIndex=16、next Bound=0; --->bound=next Bound=0 ---> i=nextIndex-1=15;(16-1) 二番目: --i==14,bound=0(16-2) advance=false; 3回目: --i=13,bound=0;(16-3) advance=false; ... 16回目: --i=0、bound=0 advance=false;
つまり、スレッドは、インデックス0まで古いハッシュ配列の最後のインデックスから巡回し始めます。シングルスレッドでもマルチスレッドでも実行される順序はこのようになります。また、resizeプロセスでは、oldtabが設定されている頭の結点はForwardingNodeであり、マルチスレッド環境でも、他のスレッドが再び巡回しても、このインデックス位置がすでにForwadingNode(ノードのハッシュ値は−1)であると簡単に判断すれば、他のスレッドはインデックス位置をスキップします。次のインデックス位置を取得し、同じプロセスを実行して巡回します。
resizeプロセスでは、スレッドがいくつあっても、あるインデックス位置にノードがあれば、この位置はスレッドだけでrehashされます。インデックス位置要素がrehashにある場合、スレッドはまず、ヘッドノードのモニタロックを取得し、取得に成功してこそ、rehashを行うことができます。
1.8 hashmapのインデックスポジショニング原理と似ていますが、具体的には「hashMap実現原理」を参照してください。rehashの後、元の位置または元のハッシュテーブルの長さですが、rehashの過程では、各インデックスの要素位置は古いテーブルと逆です。ConccurrenthashMap拡張操作要素は逆さまです。
以上、1.8のCurrennthashMapの拡大プロセスです。