JDK容器與並發—Map—ConcurrentSkipListMap

來源:互聯網
上載者:User

JDK容器與並發—Map—ConcurrentSkipListMap
概述

基於跳錶實現的ConcurrentNavigableMap。

1)containsKey、get、put、remove等操作的平均時間複雜度為log(n);size非固定時間操作,因非同步特性,需要遍曆所有節點才能確定size,且可能不是正確的值如果遍曆過程中有修改;大量操作:putAll、equals、toArray、containsValue、clear非原子性。

2)增刪改查操作並發安全執行緒;

3)迭代器是弱一致性的:map建立時或之後某個時間點的,不拋出ConcurrentModificationException,可能與其他動作並發執行。升序視圖及迭代器比降序的要快;

資料結構

基於鏈表,有三種類型節點Node、Index、HeadIndex,底層為Node單鏈表有序層(升序),其他層為基於Node層的Index層即索引層,可能有多個索引層。

相對於單鏈表,跳錶尋找節點很快是在於:通過HeadIndex維護索引層次,通過Index從最上層開始往下尋找元素,一步步縮小尋找範圍,到了最底層Node單鏈表層,就只需要比較很少的元素就可以找到待尋找的元素節點。

// Node節點,擁有key-value對// 單鏈表,有序,第一個節點為head.node標記節點,中間可能會穿插些刪除標記節點(即marker節點)static final class Node {final K key;volatile Object value; // value為Object類型,便於區分刪除標記節點、head.node標記節點volatile Node next;/** * Creates a new regular node. */Node(K key, Object value, Node next) {this.key = key;this.value = value;this.next = next;}// 建立一個刪除標記節點// 刪除標記節點的value為this,以區分其他Node節點;// key為null,其他場合會用到,但不能用作區分,因為head.node的key也為nullNode(Node next) {this.key = null;this.value = this;this.next = next;}// CAS value屬性boolean casValue(Object cmp, Object val) {return UNSAFE.compareAndSwapObject(this, valueOffset, cmp, val);}// CAS next屬性boolean casNext(Node cmp, Node val) {return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);}// 是否為刪除標記節點boolean isMarker() {return value == this;}// 是否為header node節點boolean isBaseHeader() {return value == BASE_HEADER;}// 在當前Node節點後增加刪除標記節點(採用CAS next方式實現)boolean appendMarker(Node f) {return casNext(f, new Node(f));} // 推進刪除Node節點 // 一般在遍曆過程中,如果遇到當前Node的value為null,會調用該方法void helpDelete(Node b, Node f) {// 首先檢查當前的連結是否為b——>this——>f;// 再進行推進刪除,過程分兩步,每一步都採用CAS實現// 每次只推進一步,以減小推進線程間的幹擾if (f == next && this == b.next) { // 檢測是否為b——>this——>f,其中b為前驅節點,f為後繼節點if (f == null || f.value != f) // 待刪除節點未進行標記appendMarker(f);// 連結刪除標記節點elseb.casNext(this, f.next); // 刪除當前節點及其刪除標記節點,完成刪除}}// 擷取節點的有效valueV getValidValue() {Object v = value;if (v == this || v == BASE_HEADER) // 若為刪除標記節點、header node節點,則返回nullreturn null;return (V)v;}// 對有效索引值對封裝到不可變的EntryAbstractMap.SimpleImmutableEntry createSnapshot() {V v = getValidValue();if (v == null)return null;return new AbstractMap.SimpleImmutableEntry(key, v);}// UNSAFE mechanicsprivate static final sun.misc.Unsafe UNSAFE;private static final long valueOffset;private static final long nextOffset;static {try {UNSAFE = sun.misc.Unsafe.getUnsafe();Class k = Node.class;valueOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("value"));nextOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("next"));} catch (Exception e) {throw new Error(e);}}}// 索引節點,用於從最上層往下縮小尋找範圍// 邏輯上的雙鏈表,非前後連結,而是上下連結static class Index {final Node node; // 索引節點是基於Node節點的final Index down;// down為final的,簡化並發volatile Index right;/** * Creates index node with given values. */Index(Node node, Index down, Index right) {this.node = node;this.down = down;this.right = right;}// CAS right屬性final boolean casRight(Index cmp, Index val) {return UNSAFE.compareAndSwapObject(this, rightOffset, cmp, val);}// 其Node節點是否刪除final boolean indexesDeletedNode() {return node.value == null;}// 連結Index節點// 如果連結過程中,其Node節點已刪除,則不連結,以減小與解連結的CAS競爭/* * @param succ the expected current successor * @param newSucc the new successor * @return true if successful */final boolean link(Index succ, Index newSucc) {Node n = node;newSucc.right = succ; // 將newSucc連結進來return n.value != null && casRight(succ, newSucc); // CAS right}// 解連結index節點,如果其Node已刪除,則解連結失敗/** * @param succ the expected current successor * @return true if successful */final boolean unlink(Index succ) {return !indexesDeletedNode() && casRight(succ, succ.right);// CAS right}// Unsafe mechanicsprivate static final sun.misc.Unsafe UNSAFE;private static final long rightOffset;static {try {UNSAFE = sun.misc.Unsafe.getUnsafe();Class k = Index.class;rightOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("right"));} catch (Exception e) {throw new Error(e);}}}// HeadIndex節點,跟蹤索引層次static final class HeadIndex extends Index {final int level;// 索引層,從1開始,Node單鏈表層為0HeadIndex(Node node, Index down, Index right, int level) {super(node, down, right);this.level = level;}}

JDK中一個執行個體結構圖:


WIKI上關於跳錶的解釋圖:<喎?http://www.bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vcD4KPHA+PGltZyBzcmM9"http://www.2cto.com/uploadfile/Collfiles/20160421/201604210924001082.gif" alt="\">

構造器
無參構造,空Mappublic ConcurrentSkipListMap() {this.comparator = null; // 用Key的Comparable介面排序initialize();}// 帶comparator參數構造public ConcurrentSkipListMap(Comparator comparator) {this.comparator = comparator;initialize();}// 帶Map參數構造,採用Key的Comparable介面排序public ConcurrentSkipListMap(Map m) {this.comparator = null;initialize();putAll(m);}// 帶SortedMap參數構造,採用SortedMap的comparator排序public ConcurrentSkipListMap(SortedMap m) {this.comparator = m.comparator();initialize();buildFromSorted(m);}
增刪改查初始化
final void initialize() {keySet = null;entrySet = null;values = null;descendingMap = null;randomSeed = seedGenerator.nextInt() | 0x0100; // ensure nonzerohead = new HeadIndex(new Node(null, BASE_HEADER, null),  null, null, 1); // 初始化head節點,空Map}
增、改

步驟:
1)尋找比key小的前驅節點,尋找過程中刪除待刪除Node節點的索引節點;
2)從前驅節點開始,遍曆底層Node單鏈表,若已存在相關的key-value對,則CAS替換新的value,返回舊value;若不存在,則確定插入位置;遍曆過程中,推進刪除待刪除的Node節點;
3)用key、value建立新的Node節點,用CAS next方式連結進來;
4)給新的Node節點隨機產生一個索引層次,若層次大於0,則給其增加索引節點,返回null。

public V put(K key, V value) {if (value == null)throw new NullPointerException();return doPut(key, value, false);}private V doPut(K kkey, V value, boolean onlyIfAbsent) {Comparable key = comparable(kkey);for (;;) {Node b = findPredecessor(key); // 找到前驅節點後,接下來就是在Node單鏈表層精確找到插入位置Node n = b.next;for (;;) {// 遍曆清除Node節點操作同findNodeif (n != null) {Node f = n.next;if (n != b.next) break;Object v = n.value;if (v == null) { n.helpDelete(b, f); break;}if (v == n || b.value == null)break;int c = key.compareTo(n.key);if (c > 0) { // key大於n,繼續往後找b = n;n = f;continue;}if (c == 0) { // 已有相關的key-value對if (onlyIfAbsent || n.casValue(v, value))return (V)v;elsebreak; // CAS value失敗,則重新開始,失敗原因可能是n變成了待刪除節點或有其他修改線程修改過}// else c < 0; fall through:說明新增的key-value對需要插到b和n之間}Node z = new Node(kkey, value, n);if (!b.casNext(n, z)) // 將新節點z插入b、n之間break;         // 失敗了,原因同n != b.next,重來int level = randomLevel(); // 給新增的Node節點隨機產生一個索引層次if (level > 0)insertIndex(z, level); // 給z增加索引節點return null;}}}// 尋找比key小的前驅節點,若沒有,則返回head.node// 一些操作依賴該方法刪除索引節點private Node findPredecessor(Comparable key) {if (key == null)throw new NullPointerException(); // don't postpone errorsfor (;;) {Index q = head;Index r = q.right;// 從索引層最上層開始,往右往下,// 一直找到最下層索引層(即第一層),從而確定尋找範圍,以在底層Node單鏈表遍曆精確找到for (;;) {if (r != null) { // 在索引層,往右找Node n = r.node;K k = n.key;if (n.value == null) { // 遍曆到待刪除節點n的索引節點if (!q.unlink(r))// 刪除其索引節點(採用CAS right屬性)//刪除失敗原因:q被標記為待刪除節點或在q後增加新索引節點或已刪除了其right節點break;   // 重新開始r = q.right;// 若刪除成功,則擷取新的right索引節點,繼續找continue;}if (key.compareTo(k) > 0) { // 若key大,說明可能還有小於key的更大的,繼續找q = r;r = r.right;continue;}}Index d = q.down;// 當層索引層沒有,則往下一層找,進一步縮小尋找範圍if (d != null) {// 在下一層索引層,繼續找q = d;r = d.right;} elsereturn q.node;// 確定前驅節點,如果沒有則為head.node標記節點}}}// 給新增的Node節點隨機產生一個索引層次/**     * Returns a random level for inserting a new node.     * Hardwired to k=1, p=0.5, max 31 (see above and     * Pugh's "Skip List Cookbook", sec 3.4).     *     * This uses the simplest of the generators described in George     * Marsaglia's "Xorshift RNGs" paper.  This is not a high-quality     * generator but is acceptable here.     */private int randomLevel() {int x = randomSeed;x ^= x << 13;x ^= x >>> 17;randomSeed = x ^= x << 5;if ((x & 0x80000001) != 0) // test highest and lowest bitsreturn 0;int level = 1;while (((x >>>= 1) & 1) != 0) ++level;return level;}// 為Node節點添加索引節點/**     * @param z the node     * @param level the level of the index     */private void insertIndex(Node z, int level) {HeadIndex h = head;int max = h.level; // head的索引層次是最大的if (level <= max) { // 待添加索引節點的索引層次在head索引層次內,建立索引節點添加進來即可Index idx = null;for (int i = 1; i <= level; ++i)idx = new Index(z, idx, null);// 索引節點,從下往上連結addIndex(idx, h, level);               // 將索引節點連結進來} else { // 增加一層索引層,需要new新層次的HeadIndex/* * To reduce interference by other threads checking for * empty levels in tryReduceLevel, new levels are added * with initialized right pointers. Which in turn requires * keeping levels in an array to access them while * creating new head index nodes from the opposite * direction. */level = max + 1;Index[] idxs = (Index[])new Index[level+1];Index idx = null;for (int i = 1; i <= level; ++i)idxs[i] = idx = new Index(z, idx, null);HeadIndex oldh;int k;for (;;) {oldh = head;int oldLevel = oldh.level;if (level <= oldLevel) { // 其他線程增加過索引層k = level;break;               // 同上面的level <= max情況處理}HeadIndex newh = oldh;Node oldbase = oldh.node;for (int j = oldLevel+1; j <= level; ++j) // 有可能其他線程刪除過索引層,所以從oldLevel至level增加HeadIndexnewh = new HeadIndex(oldbase, newh, idxs[j], j); // 建立新層次的HeadIndex且將索引節點idxs相應層次連結進來if (casHead(oldh, newh)) { // CAS head HeadIndex節點k = oldLevel;break;}}addIndex(idxs[k], oldh, k);                  // 需要將idxs的舊oldLevel層次及下面的索引連結進來}}/** * 從第indexLevel層往下到第1層,將索引節點連結進來 * @param idx the topmost index node being inserted * @param h the value of head to use to insert. This must be * snapshotted by callers to provide correct insertion level * @param indexLevel the level of the index */private void addIndex(Index idx, HeadIndex h, int indexLevel) {// Track next level to insert in case of retriesint insertionLevel = indexLevel;Comparable key = comparable(idx.node.key);if (key == null) throw new NullPointerException();// 過程與findPredecessor類似, 只是多了增加索引節點for (;;) {int j = h.level;Index q = h;Index r = q.right;Index t = idx;for (;;) {if (r != null) {// 在索引層,往右遍曆Node n = r.node;// compare before deletion check avoids needing recheckint c = key.compareTo(n.key);if (n.value == null) { if (!q.unlink(r))break;r = q.right;continue;}if (c > 0) {q = r;r = r.right;continue;}}if (j == insertionLevel) {        // 可以連結索引節點idxif (t.indexesDeletedNode()) { // 索引節點的Node節點被標記為待刪除節點findNode(key);            // 推進刪除索引節點及其Node節點return;                   // 不用增加索引節點了}if (!q.link(r, t))            // 將第insertionLevel層索引節點連結進來//刪除失敗原因:同findPredecessor種的q.unlink(r)break;    // 連結失敗,重新開始if (--insertionLevel == 0) {  // 準備連結索引節點idx的下一層索引if (t.indexesDeletedNode()) // 返回前再次檢查索引節點t是否被標記為待刪除節點,以進行清理工作findNode(key);return;                   // insertionLevel==0表明已經完成索引節點idx的連結}}if (--j >= insertionLevel && j < indexLevel) // 已連結過索引節點idx的第insertionLevel+1層t = t.down;                              // 準備連結索引節點idx的第insertionLevel層q = q.down;  // 準備連結索引節點idx的下一層索引r = q.right;}}}// 尋找相關key的Node,沒有則返回null。// 遍曆Node單鏈表中,清除待刪除節點// 在doPut、doRemove、findNear等都包含這樣的遍曆清除操作// 不能共用這樣的清除代碼,因為增刪改查需要擷取Node鏈表順序的快照暫存到自身的局部變數,用於並發// 一些操作依賴此方法刪除Node節點private Node findNode(Comparable key) {for (;;) {Node b = findPredecessor(key); // 擷取key的前驅節點Node n = b.next;for (;;) {if (n == null)return null;Node f = n.next;// 不是連續的b——>n——>f快照,不能進行後續解連結待刪除節點//變化情況:在b後增加了新節點或刪除了其next節點或增加了刪除標記節點以刪除b,if (n != b.next)              break; // 重新開始Object v = n.value;if (v == null) {           // n為待刪除節點n.helpDelete(b, f);    // 推進刪除節點nbreak;                 // 重新開始}// 返回的前驅節點b為待刪除節點// 這裡不能直接刪除b,因為不知道b的前驅節點,只能重新開始,調用findPredecessor返回更前的節點if (v == n || b.value == null)  // b is deletedbreak;                 // 重新開始int c = key.compareTo(n.key);if (c == 0)return n;if (c < 0)return null;b = n;n = f;}}}

步驟:
1)尋找比key小的前驅節點;
2)從前驅節點開始,遍曆底層Node單鏈表,若不存在相關的key-value對,則返回null;否則確定Node節點位置;假設確定刪除的節點為n,b為其前驅節點,f為其後繼節點:


3)用CAS方式將其value置為null;

作用:

a)其他增刪改查線程遍曆到該節點時,都知其為待刪除節點;

b)其他增刪改查線程可通過CAS修改n的next,推進n的刪除。

該步失敗,只需要重試即可。
4)在其後添加刪除標記節點marker;


作用:

a)新增節點不能插入到n的後面;

b)基於CAS方式的刪除,可以避免刪除上的錯誤。

5)刪除該節點及其刪除標記節點;


第4)5)步可能會失敗,原因在於其他動作線程在遍曆過程中知n的value為null後,會協助推進刪除n,這些協助操作可以保證沒有一個線程因為刪除線程的刪除操作而阻塞。

另外,刪除需要確保b——>n——>marker——>f連結關係才能進行。

6)用findPredecessor刪除其索引節點;
7)若最上層索引層無Node索引節點,則嘗試降低索引層次。
8)返回其舊value

public V remove(Object key) {return doRemove(key, null);}// 主要的刪除Node節點及其索引節點方法final V doRemove(Object okey, Object value) {Comparable key = comparable(okey);for (;;) {Node b = findPredecessor(key);Node n = b.next;for (;;) {if (n == null)return null;Node f = n.next;if (n != b.next)                    // inconsistent readbreak;Object v = n.value;if (v == null) {                    // n is deletedn.helpDelete(b, f);break;}if (v == n || b.value == null)      // b is deletedbreak;int c = key.compareTo(n.key);if (c < 0)return null;if (c > 0) {b = n;n = f;continue;}if (value != null && !value.equals(v))return null;if (!n.casValue(v, null))           // 將value置為nullbreak;if (!n.appendMarker(f) || !b.casNext(n, f)) // 添加刪除標記節點,刪除該節點與其刪除標記節點findNode(key);                  // 失敗則用findNode繼續刪除else {findPredecessor(key);           // 用findPredecessor刪除其索引節點if (head.right == null)tryReduceLevel();}return (V)v;}}}// 當最上三層索引層無Node索引節點,則將最上層索引層去掉。// 採用CAS方式去掉後,如果其又擁有Node索引節點,則嘗試將其恢複。private void tryReduceLevel() {HeadIndex h = head;HeadIndex d;HeadIndex e;if (h.level > 3 &&(d = (HeadIndex)h.down) != null &&(e = (HeadIndex)d.down) != null &&e.right == null &&d.right == null &&h.right == null &&casHead(h, d) && // try to seth.right != null) // recheckcasHead(d, h);   // try to backout}

步驟:
1)尋找比key小的前驅節點;
2)從前驅節點開始,遍曆底層Node單鏈表,若不存在相關的key-value對,則返回null;否則擷取Node節點;
3)判斷其value是否為null,如果不為null,則直接返回value;否則重試,原因是其被標記為待刪除節點。

public V get(Object key) {return doGet(key);}private V doGet(Object okey) {Comparable key = comparable(okey);/* * Loop needed here and elsewhere in case value field goes * null just as it is about to be returned, in which case we * lost a race with a deletion, so must retry. */for (;;) {Node n = findNode(key);if (n == null)return null;Object v = n.value;if (v != null)return (V)v;}}

迭代器

基礎迭代器為Iter,從first節點開始遍曆Node單鏈表層:

abstract class Iter implements Iterator {/** the last node returned by next() */Node lastReturned;/** the next node to return from next(); */Node next;/** Cache of next value field to maintain weak consistency */V nextValue;/** Initializes ascending iterator for entire range. */Iter() {for (;;) {next = findFirst();if (next == null)break;Object x = next.value;if (x != null && x != next) {nextValue = (V) x;break;}}}public final boolean hasNext() {return next != null;}/** Advances next to higher entry. */final void advance() {if (next == null)throw new NoSuchElementException();lastReturned = next;for (;;) {next = next.next; // nextif (next == null)break;Object x = next.value;if (x != null && x != next) {nextValue = (V) x;break;}}}public void remove() {Node l = lastReturned;if (l == null)throw new IllegalStateException();// It would not be worth all of the overhead to directly// unlink from here. Using remove is fast enough.ConcurrentSkipListMap.this.remove(l.key);lastReturned = null;}}

特性

如何?增刪改查並發安全執行緒?

1. 採用無鎖並發方式;

2.基於final、volatile、CAS方式助於並發;

3. 刪除Node節點方式:將該節點的value置為null,且在其後插入一個刪除標記節點,即:b——>n——>marker——>f(假設n為待刪除Node節點,marker為其刪除標記節點,b為n的前驅節點,f為n的刪除前的後繼節點)這種方式解決了4個問題:

1)與Node插入可並發進行,因為n後為marker標記節點,肯定不會在n後插入新的Node節點;

2)與Node修改可並發進行,因為n的value為null,修改線程對n的CAS修改肯定是失敗的;

3)與Node讀可並發進行,因為n的value為null,即使讀線程匹配到n,也返回的value為null,而在Map中返回null即代表key-value對不存在,n正在刪除,所以也表明不存在,儘管不是嚴格意義上的;

4)所有操作在遍曆Node單鏈表時,可根據以上連結關係,推進刪除n和marker。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.