死磕 java集合之ConcurrentSkipListMap源码分析——发现个bug
前情提要
點擊鏈接查看“跳表”詳細介紹。
拜托,面試別再問我跳表了!
簡介
跳表是一個隨機化的數據結構,實質就是一種可以進行二分查找的有序鏈表。
跳表在原有的有序鏈表上面增加了多級索引,通過索引來實現快速查找。
跳表不僅能提高搜索性能,同時也可以提高插入和刪除操作的性能。
存儲結構
跳表在原有的有序鏈表上面增加了多級索引,通過索引來實現快速查找。
源碼分析
主要內部類
內部類跟存儲結構結合著來看,大概能預測到代碼的組織方式。
// 數據節點,典型的單鏈表結構 static final class Node<K,V> {final K key;// 注意:這里value的類型是Object,而不是V// 在刪除元素的時候value會指向當前元素本身volatile Object value;volatile Node<K,V> next;Node(K key, Object value, Node<K,V> next) {this.key = key;this.value = value;this.next = next;}Node(Node<K,V> next) {this.key = null;this.value = this; // 當前元素本身(marker)this.next = next;} }// 索引節點,存儲著對應的node值,及向下和向右的索引指針 static class Index<K,V> {final Node<K,V> node;final Index<K,V> down;volatile Index<K,V> right;Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) {this.node = node;this.down = down;this.right = right;} }// 頭索引節點,繼承自Index,并擴展一個level字段,用于記錄索引的層級 static final class HeadIndex<K,V> extends Index<K,V> {final int level;HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {super(node, down, right);this.level = level;} }(1)Node,數據節點,存儲數據的節點,典型的單鏈表結構;
(2)Index,索引節點,存儲著對應的node值,及向下和向右的索引指針;
(3)HeadIndex,頭索引節點,繼承自Index,并擴展一個level字段,用于記錄索引的層級;
構造方法
public ConcurrentSkipListMap() {this.comparator = null;initialize(); }public ConcurrentSkipListMap(Comparator<? super K> comparator) {this.comparator = comparator;initialize(); }public ConcurrentSkipListMap(Map<? extends K, ? extends V> m) {this.comparator = null;initialize();putAll(m); }public ConcurrentSkipListMap(SortedMap<K, ? extends V> m) {this.comparator = m.comparator();initialize();buildFromSorted(m); }四個構造方法里面都調用了initialize()這個方法,那么,這個方法里面有什么呢?
private static final Object BASE_HEADER = new Object();private void initialize() {keySet = null;entrySet = null;values = null;descendingMap = null;// Node(K key, Object value, Node<K,V> next)// HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level)head = new HeadIndex<K,V>(new Node<K,V>(null, BASE_HEADER, null),null, null, 1); }可以看到,這里初始化了一些屬性,并創建了一個頭索引節點,里面存儲著一個數據節點,這個數據節點的值是空對象,且它的層級是1。
所以,初始化的時候,跳表中只有一個頭索引節點,層級是1,數據節點是一個空對象,down和right都是null。
通過內部類的結構我們知道,一個頭索引指針包含node, down, right三個指針,為了便于理解,我們把指向node的指針用虛線表示,其它兩個用實線表示,也就是虛線不是表明方向的。
添加元素
通過【拜托,面試別再問我跳表了!】中的分析,我們知道跳表插入元素的時候會通過拋硬幣的方式決定出它需要的層級,然后找到各層鏈中它所在的位置,最后通過單鏈表插入的方式把節點及索引插入進去來實現的。
那么,ConcurrentSkipList中是這么做的嗎?讓我們一起來探個究竟:
public V put(K key, V value) {// 不能存儲value為null的元素// 因為value為null標記該元素被刪除(后面會看到)if (value == null)throw new NullPointerException();// 調用doPut()方法添加元素return doPut(key, value, false); }private V doPut(K key, V value, boolean onlyIfAbsent) {// 添加元素后存儲在z中Node<K,V> z; // added node// key也不能為nullif (key == null)throw new NullPointerException();Comparator<? super K> cmp = comparator;// Part I:找到目標節點的位置并插入// 這里的目標節點是數據節點,也就是最底層的那條鏈// 自旋outer: for (;;) {// 尋找目標節點之前最近的一個索引對應的數據節點,存儲在b中,b=before// 并把b的下一個數據節點存儲在n中,n=next// 為了便于描述,我這里把b叫做當前節點,n叫做下一個節點for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {// 如果下一個節點不為空// 就拿其key與目標節點的key比較,找到目標節點應該插入的位置if (n != null) {// v=value,存儲節點value值// c=compare,存儲兩個節點比較的大小Object v; int c;// n的下一個數據節點,也就是b的下一個節點的下一個節點(孫子節點)Node<K,V> f = n.next;// 如果n不為b的下一個節點// 說明有其它線程修改了數據,則跳出內層循環// 也就是回到了外層循環自旋的位置,從頭來過if (n != b.next) // inconsistent readbreak;// 如果n的value值為空,說明該節點已刪除,協助刪除節點if ((v = n.value) == null) { // n is deleted// todo 這里為啥會協助刪除?后面講n.helpDelete(b, f);break;}// 如果b的值為空或者v等于n,說明b已被刪除// 這時候n就是marker節點,那b就是被刪除的那個if (b.value == null || v == n) // b is deletedbreak;// 如果目標key與下一個節點的key大// 說明目標元素所在的位置還在下一個節點的后面if ((c = cpr(cmp, key, n.key)) > 0) {// 就把當前節點往后移一位// 同樣的下一個節點也往后移一位// 再重新檢查新n是否為空,它與目標key的關系b = n;n = f;continue;}// 如果比較時發現下一個節點的key與目標key相同// 說明鏈表中本身就存在目標節點if (c == 0) {// 則用新值替換舊值,并返回舊值(onlyIfAbsent=false)if (onlyIfAbsent || n.casValue(v, value)) {@SuppressWarnings("unchecked") V vv = (V)v;return vv;}// 如果替換舊值時失敗,說明其它線程先一步修改了值,從頭來過break; // restart if lost race to replace value}// 如果c<0,就往下走,也就是找到了目標節點的位置// else c < 0; fall through}// 有兩種情況會到這里// 一是到鏈表尾部了,也就是n為null了// 二是找到了目標節點的位置,也就是上面的c<0// 新建目標節點,并賦值給z// 這里把n作為新節點的next// 如果到鏈表尾部了,n為null,這毫無疑問// 如果c<0,則n的key比目標key大,相妝于在b和n之間插入目標節點zz = new Node<K,V>(key, value, n);// 原子更新b的下一個節點為目標節點zif (!b.casNext(n, z))// 如果更新失敗,說明其它線程先一步修改了值,從頭來過break; // restart if lost race to append to b// 如果更新成功,跳出自旋狀態break outer;}}// 經過Part I,目標節點已經插入到有序鏈表中了// Part II:隨機決定是否需要建立索引及其層次,如果需要則建立自上而下的索引// 取個隨機數int rnd = ThreadLocalRandom.nextSecondarySeed();// 0x80000001展開為二進制為10000000000000000000000000000001// 只有兩頭是1// 這里(rnd & 0x80000001) == 0// 相當于排除了負數(負數最高位是1),排除了奇數(奇數最低位是1)// 只有最高位最低位都不為1的數跟0x80000001做&操作才會為0// 也就是正偶數if ((rnd & 0x80000001) == 0) { // test highest and lowest bits// 默認level為1,也就是只要到這里了就會至少建立一層索引int level = 1, max;// 隨機數從最低位的第二位開始,有幾個連續的1則level就加幾// 因為最低位肯定是0,正偶數嘛// 比如,1100110,level就加2while (((rnd >>>= 1) & 1) != 0)++level;// 用于記錄目標節點建立的最高的那層索引節點Index<K,V> idx = null;// 取頭索引節點(這是最高層的頭索引節點)HeadIndex<K,V> h = head;// 如果生成的層數小于等于當前最高層的層級// 也就是跳表的高度不會超過現有高度if (level <= (max = h.level)) {// 從第一層開始建立一條豎直的索引鏈表// 這條鏈表使用down指針連接起來// 每個索引節點里面都存儲著目標節點這個數據節點// 最后idx存儲的是這條索引鏈表的最高層節點for (int i = 1; i <= level; ++i)idx = new Index<K,V>(z, idx, null);}else { // try to grow by one level// 如果新的層數超過了現有跳表的高度// 則最多只增加一層// 比如現在只有一層索引,那下一次最多增加到兩層索引,增加多了也沒有意義level = max + 1; // hold in array and later pick the one to use// idxs用于存儲目標節點建立的豎起索引的所有索引節點// 其實這里直接使用idx這個最高節點也是可以完成的// 只是用一個數組存儲所有節點要方便一些// 注意,這里數組0號位是沒有使用的@SuppressWarnings("unchecked")Index<K,V>[] idxs =(Index<K,V>[])new Index<?,?>[level+1];// 從第一層開始建立一條豎的索引鏈表(跟上面一樣,只是這里順便把索引節點放到數組里面了)for (int i = 1; i <= level; ++i)idxs[i] = idx = new Index<K,V>(z, idx, null);// 自旋for (;;) {// 舊的最高層頭索引節點h = head;// 舊的最高層級int oldLevel = h.level;// 再次檢查,如果舊的最高層級已經不比新層級矮了// 說明有其它線程先一步修改了值,從頭來過if (level <= oldLevel) // lost race to add levelbreak;// 新的最高層頭索引節點HeadIndex<K,V> newh = h;// 頭節點指向的數據節點Node<K,V> oldbase = h.node;// 超出的部分建立新的頭索引節點for (int j = oldLevel+1; j <= level; ++j)newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);// 原子更新頭索引節點if (casHead(h, newh)) {// h指向新的最高層頭索引節點h = newh;// 把level賦值為舊的最高層級的// idx指向的不是最高的索引節點了// 而是與舊最高層平齊的索引節點idx = idxs[level = oldLevel];break;}}}// 經過上面的步驟,有兩種情況// 一是沒有超出高度,新建一條目標節點的索引節點鏈// 二是超出了高度,新建一條目標節點的索引節點鏈,同時最高層頭索引節點同樣往上長// Part III:將新建的索引節點(包含頭索引節點)與其它索引節點通過右指針連接在一起// 這時level是等于舊的最高層級的,自旋splice: for (int insertionLevel = level;;) {// h為最高頭索引節點int j = h.level;// 從頭索引節點開始遍歷// 為了方便,這里叫q為當前節點,r為右節點,d為下節點,t為目標節點相應層級的索引for (Index<K,V> q = h, r = q.right, t = idx;;) {// 如果遍歷到了最右邊,或者最下邊,// 也就是遍歷到頭了,則退出外層循環if (q == null || t == null)break splice;// 如果右節點不為空if (r != null) {// n是右節點的數據節點,為了方便,這里直接叫右節點的值Node<K,V> n = r.node;// 比較目標key與右節點的值int c = cpr(cmp, key, n.key);// 如果右節點的值為空了,則表示此節點已刪除if (n.value == null) {// 則把右節點刪除if (!q.unlink(r))// 如果刪除失敗,說明有其它線程先一步修改了,從頭來過break;// 刪除成功后重新取右節點r = q.right;continue;}// 如果比較c>0,表示目標節點還要往右if (c > 0) {// 則把當前節點和右節點分別右移q = r;r = r.right;continue;}}// 到這里說明已經到當前層級的最右邊了// 這里實際是會先走第二個if// 第一個if// j與insertionLevel相等了// 實際是先走的第二個if,j自減后應該與insertionLevel相等if (j == insertionLevel) {// 這里是真正連右指針的地方if (!q.link(r, t))// 連接失敗,從頭來過break; // restart// t節點的值為空,可能是其它線程刪除了這個元素if (t.node.value == null) {// 這里會去協助刪除元素findNode(key);break splice;}// 當前層級右指針連接完畢,向下移一層繼續連接// 如果移到了最下面一層,則說明都連接完成了,退出外層循環if (--insertionLevel == 0)break splice;}// 第二個if// j先自減1,再與兩個level比較// j、insertionLevel和t(idx)三者是對應的,都是還未把右指針連好的那個層級if (--j >= insertionLevel && j < level)// t往下移t = t.down;// 當前層級到最右邊了// 那只能往下一層級去走了// 當前節點下移// 再取相應的右節點q = q.down;r = q.right;}}}return null; }// 尋找目標節點之前最近的一個索引對應的數據節點 private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {// key不能為空if (key == null)throw new NullPointerException(); // don't postpone errors// 自旋for (;;) {// 從最高層頭索引節點開始查找,先向右,再向下// 直到找到目標位置之前的那個索引for (Index<K,V> q = head, r = q.right, d;;) {// 如果右節點不為空if (r != null) {// 右節點對應的數據節點,為了方便,我們叫右節點的值Node<K,V> n = r.node;K k = n.key;// 如果右節點的value為空// 說明其它線程把這個節點標記為刪除了// 則協助刪除if (n.value == null) {if (!q.unlink(r))// 如果刪除失敗// 說明其它線程先刪除了,從頭來過break; // restart// 刪除之后重新讀取右節點r = q.right; // reread rcontinue;}// 如果目標key比右節點還大,繼續向右尋找if (cpr(cmp, key, k) > 0) {// 往右移q = r;// 重新取右節點r = r.right;continue;}// 如果c<0,說明不能再往右了}// 到這里說明當前層級已經到最右了// 兩種情況:一是r==null,二是c<0// 再從下一級開始找// 如果沒有下一級了,就返回這個索引對應的數據節點if ((d = q.down) == null)return q.node;// 往下移q = d;// 重新取右節點r = d.right;}} }// Node.class中的方法,協助刪除元素 void helpDelete(Node<K,V> b, Node<K,V> f) {/** Rechecking links and then doing only one of the* help-out stages per call tends to minimize CAS* interference among helping threads.*/// 這里的調用者this==n,三者關系是b->n->fif (f == next && this == b.next) {// 將n的值設置為null后,會先把n的下個節點設置為marker節點// 這個marker節點的值是它自己// 這里如果不是它自己說明marker失敗了,重新markerif (f == null || f.value != f) // not already markedcasNext(f, new Node<K,V>(f));else// marker過了,就把b的下個節點指向marker的下個節點b.casNext(this, f.next);} }// Index.class中的方法,刪除succ節點 final boolean unlink(Index<K,V> succ) {// 原子更新當前節點指向下一個節點的下一個節點// 也就是刪除下一個節點return node.value != null && casRight(succ, succ.right); }// Index.class中的方法,在當前節點與succ之間插入newSucc節點 final boolean link(Index<K,V> succ, Index<K,V> newSucc) {// 在當前節點與下一個節點中間插入一個節點Node<K,V> n = node;// 新節點指向當前節點的下一個節點newSucc.right = succ;// 原子更新當前節點的下一個節點指向新節點return n.value != null && casRight(succ, newSucc); }我們這里把整個插入過程分成三個部分:
Part I:找到目標節點的位置并插入
(1)這里的目標節點是數據節點,也就是最底層的那條鏈;
(2)尋找目標節點之前最近的一個索引對應的數據節點(數據節點都是在最底層的鏈表上);
(3)從這個數據節點開始往后遍歷,直到找到目標節點應該插入的位置;
(4)如果這個位置有元素,就更新其值(onlyIfAbsent=false);
(5)如果這個位置沒有元素,就把目標節點插入;
(6)至此,目標節點已經插入到最底層的數據節點鏈表中了;
Part II:隨機決定是否需要建立索引及其層次,如果需要則建立自上而下的索引
(1)取個隨機數rnd,計算(rnd & 0x80000001);
(2)如果不等于0,結束插入過程,也就是不需要創建索引,返回;
(3)如果等于0,才進入創建索引的過程(只要正偶數才會等于0);
(4)計算while (((rnd >>>= 1) & 1) != 0),決定層級數,level從1開始;
(5)如果算出來的層級不高于現有最高層級,則直接建立一條豎直的索引鏈表(只有down有值),并結束Part II;
(6)如果算出來的層級高于現有最高層級,則新的層級只能比現有最高層級多1;
(7)同樣建立一條豎直的索引鏈表(只有down有值);
(8)將頭索引也向上增加到相應的高度,結束Part II;
(9)也就是說,如果層級不超過現有高度,只建立一條索引鏈,否則還要額外增加頭索引鏈的高度(腦補一下,后面舉例說明);
Part III:將新建的索引節點(包含頭索引節點)與其它索引節點通過右指針連接在一起(補上right指針)
(1)從最高層級的頭索引節點開始,向右遍歷,找到目標索引節點的位置;
(2)如果當前層有目標索引,則把目標索引插入到這個位置,并把目標索引前一個索引向下移一個層級;
(3)如果當前層沒有目標索引,則把目標索引位置前一個索引向下移一個層級;
(4)同樣地,再向右遍歷,尋找新的層級中目標索引的位置,回到第(2)步;
(5)依次循環找到所有層級目標索引的位置并把它們插入到橫向的索引鏈表中;
總結起來,一共就是三大步:
(1)插入目標節點到數據節點鏈表中;
(2)建立豎直的down鏈表;
(3)建立橫向的right鏈表;
添加元素舉例
假設初始鏈表是這樣:
假如,我們現在要插入一個元素9。
(1)尋找目標節點之前最近的一個索引對應的數據節點,在這里也就是找到了5這個數據節點;
(2)從5開始向后遍歷,找到目標節點的位置,也就是在8和12之間;
(3)插入9這個元素,Part I 結束;
然后,計算其索引層級,假如是3,也就是level=3。
(1)建立豎直的down索引鏈表;
(2)超過了現有高度2,還要再增加head索引鏈的高度;
(3)至此,Part II 結束;
最后,把right指針補齊。
(1)從第3層的head往右找當前層級目標索引的位置;
(2)找到就把目標索引和它前面索引的right指針連上,這里前一個正好是head;
(3)然后前一個索引向下移,這里就是head下移;
(4)再往右找目標索引的位置;
(5)找到了就把right指針連上,這里前一個是3的索引;
(6)然后3的索引下移;
(7)再往右找目標索引的位置;
(8)找到了就把right指針連上,這里前一個是5的索引;
(9)然后5下移,到底了,Part III 結束,整個插入過程結束;
是不是很簡單^^
刪除元素
刪除元素,就是把各層級中對應的元素刪除即可,真的這么簡單嗎?來讓我們上代碼:
public V remove(Object key) {return doRemove(key, null); }final V doRemove(Object key, Object value) {// key不為空if (key == null)throw new NullPointerException();Comparator<? super K> cmp = comparator;// 自旋outer: for (;;) {// 尋找目標節點之前的最近的索引節點對應的數據節點// 為了方便,這里叫b為當前節點,n為下一個節點,f為下下個節點for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {Object v; int c;// 整個鏈表都遍歷完了也沒找到目標節點,退出外層循環if (n == null)break outer;// 下下個節點Node<K,V> f = n.next;// 再次檢查// 如果n不是b的下一個節點了// 說明有其它線程先一步修改了,從頭來過if (n != b.next) // inconsistent readbreak;// 如果下個節點的值奕為null了// 說明有其它線程標記該元素為刪除狀態了if ((v = n.value) == null) { // n is deleted// 協助刪除n.helpDelete(b, f);break;}// 如果b的值為空或者v等于n,說明b已被刪除// 這時候n就是marker節點,那b就是被刪除的那個if (b.value == null || v == n) // b is deletedbreak;// 如果c<0,說明沒找到元素,退出外層循環if ((c = cpr(cmp, key, n.key)) < 0)break outer;// 如果c>0,說明還沒找到,繼續向右找if (c > 0) {// 當前節點往后移b = n;// 下一個節點往后移n = f;continue;}// c=0,說明n就是要找的元素// 如果value不為空且不等于找到元素的value,不需要刪除,退出外層循環if (value != null && !value.equals(v))break outer;// 如果value為空,或者相等// 原子標記n的value值為空if (!n.casValue(v, null))// 如果刪除失敗,說明其它線程先一步修改了,從頭來過break;// P.S.到了這里n的值肯定是設置成null了// 關鍵!!!!// 讓n的下一個節點指向一個market節點// 這個market節點的key為null,value為marker自己,next為n的下個節點f// 或者讓b的下一個節點指向下下個節點// 注意:這里是或者||,因為兩個CAS不能保證都成功,只能一個一個去嘗試// 這里有兩層意思:// 一是如果標記market成功,再嘗試將b的下個節點指向下下個節點,如果第二步失敗了,進入條件,如果成功了就不用進入條件了// 二是如果標記market失敗了,直接進入條件if (!n.appendMarker(f) || !b.casNext(n, f))// 通過findNode()重試刪除(里面有個helpDelete()方法)findNode(key); // retry via findNodeelse {// 上面兩步操作都成功了,才會進入這里,不太好理解,上面兩個條件都有非"!"操作// 說明節點已經刪除了,通過findPredecessor()方法刪除索引節點// findPredecessor()里面有unlink()操作findPredecessor(key, cmp); // clean index// 如果最高層頭索引節點沒有右節點,則跳表的高度降級if (head.right == null)tryReduceLevel();}// 返回刪除的元素值@SuppressWarnings("unchecked") V vv = (V)v;return vv;}}return null; }(1)尋找目標節點之前最近的一個索引對應的數據節點(數據節點都是在最底層的鏈表上);
(2)從這個數據節點開始往后遍歷,直到找到目標節點的位置;
(3)如果這個位置沒有元素,直接返回null,表示沒有要刪除的元素;
(4)如果這個位置有元素,先通過n.casValue(v, null)原子更新把其value設置為null;
(5)通過n.appendMarker(f)在當前元素后面添加一個marker元素標記當前元素是要刪除的元素;
(6)通過b.casNext(n, f)嘗試刪除元素;
(7)如果上面兩步中的任意一步失敗了都通過findNode(key)中的n.helpDelete(b, f)再去不斷嘗試刪除;
(8)如果上面兩步都成功了,再通過findPredecessor(key, cmp)中的q.unlink(r)刪除索引節點;
(9)如果head的right指針指向了null,則跳表高度降級;
刪除元素舉例
假如初始跳表如下圖所示,我們要刪除9這個元素。
(1)找到9這個數據節點;
(2)把9這個節點的value值設置為null;
(3)在9后面添加一個marker節點,標記9已經刪除了;
(4)讓8指向12;
(5)把索引節點與它前一個索引的right斷開聯系;
(6)跳表高度降級;
至于,為什么要有(2)(3)(4)這么多步驟呢,因為多線程下如果直接讓8指向12,可以其它線程先一步在9和12間插入了一個元素10呢,這時候就不對了。
所以這里搞了三步來保證多線程下操作的正確性。
如果第(2)步失敗了,則直接重試;
如果第(3)或(4)步失敗了,因為第(2)步是成功的,則通過helpDelete()不斷重試去刪除;
其實helpDelete()里面也是不斷地重試(3)和(4);
只有這三步都正確完成了,才能說明這個元素徹底被刪除了。
這一塊結合上面圖中的紅綠藍色好好理解一下,一定要想在并發環境中會怎么樣。
查找元素
經過上面的插入和刪除,查找元素就比較簡單了,直接上代碼:
public V get(Object key) {return doGet(key); }private V doGet(Object key) {// key不為空if (key == null)throw new NullPointerException();Comparator<? super K> cmp = comparator;// 自旋outer: for (;;) {// 尋找目標節點之前最近的索引對應的數據節點// 為了方便,這里叫b為當前節點,n為下個節點,f為下下個節點for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {Object v; int c;// 如果鏈表到頭還沒找到元素,則跳出外層循環if (n == null)break outer;// 下下個節點Node<K,V> f = n.next;// 如果不一致讀,從頭來過if (n != b.next) // inconsistent readbreak;// 如果n的值為空,說明節點已被其它線程標記為刪除if ((v = n.value) == null) { // n is deleted// 協助刪除,再重試n.helpDelete(b, f);break;}// 如果b的值為空或者v等于n,說明b已被刪除// 這時候n就是marker節點,那b就是被刪除的那個if (b.value == null || v == n) // b is deletedbreak;// 如果c==0,說明找到了元素,就返回元素值if ((c = cpr(cmp, key, n.key)) == 0) {@SuppressWarnings("unchecked") V vv = (V)v;return vv;}// 如果c<0,說明沒找到元素if (c < 0)break outer;// 如果c>0,說明還沒找到,繼續尋找// 當前節點往后移b = n;// 下一個節點往后移n = f;}}return null; }(1)尋找目標節點之前最近的一個索引對應的數據節點(數據節點都是在最底層的鏈表上);
(2)從這個數據節點開始往后遍歷,直到找到目標節點的位置;
(3)如果這個位置沒有元素,直接返回null,表示沒有找到元素;
(4)如果這個位置有元素,返回元素的value值;
查找元素舉例
假如有如下圖所示這個跳表,我們要查找9這個元素,它走過的路徑是怎樣的呢?可能跟你相像的不一樣。。
(1)尋找目標節點之前最近的一個索引對應的數據節點,這里就是5;
(2)從5開始往后遍歷,經過8,到9;
(3)找到了返回;
整個路徑如下圖所示:
是不是很操蛋?
為啥不從9的索引直接過來呢?
從我實際打斷點調試來看確實是按照上圖的路徑來走的。
我猜測可能是因為findPredecessor()這個方法是插入、刪除、查找元素多個方法共用的,在單鏈表中插入和刪除元素是需要記錄前一個元素的,而查找并不需要,這里為了兼容三者使得編碼相對簡單一點,所以就使用了同樣的邏輯,而沒有單獨對查找元素進行優化。
不過也可能是Doug Lea大神不小心寫了個bug,如果有人知道原因請告訴我。(公眾號后臺留言,新公眾號的文章下面不支持留言了,蛋疼)
彩蛋
為什么Redis選擇使用跳表而不是紅黑樹來實現有序集合?
請查看【拜托,面試別再問我跳表了!】這篇文章。
歡迎關注我的公眾號“彤哥讀源碼”,查看更多源碼系列文章, 與彤哥一起暢游源碼的海洋。
轉載于:https://blog.51cto.com/14267003/2378444
總結
以上是生活随笔為你收集整理的死磕 java集合之ConcurrentSkipListMap源码分析——发现个bug的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PostgreSQL 11 100亿 t
- 下一篇: 不要一棍子打翻所有黑盒模型,其实可以让它