NonBlockingHashMap无阻塞并发map

cliff click博士无阻塞实现的Map NonBlockingHashMap
这个算法是无锁。以下尝试分析下源码。

看下kv结构.

  private transient Object[] _kvs;
  private static final CHM   chm   (Object[] kvs) { return (CHM  )kvs[0]; }
  private static final int[] hashes(Object[] kvs) { return (int[])kvs[1]; }
 private static final Object key(Object[] kvs,int idx) { return kvs[(idx<<1)+2]; }
  private static final Object val(Object[] kvs,int idx) { return kvs[(idx<<1)+3]; }
  private static final boolean CAS_key( Object[] kvs, int idx, Object old, Object key ) {
    return _unsafe.compareAndSwapObject( kvs, rawIndex(kvs,(idx<<1)+2), old, key );
  }
  private static final boolean CAS_val( Object[] kvs, int idx, Object old, Object val ) {
    return _unsafe.compareAndSwapObject( kvs, rawIndex(kvs,(idx<<1)+3), old, val );
  }

以上采用了一个Object数组_kvs存储数据,其中的第零个元素、第一个元素默认采用用于CHM(chm是一个内部类,主要作用于扩容,记录数据长度等,后续会详细讲)和hash码的数组,从第二个元素用于真正的数据存储,偶数位存储key,奇数位存储value,不是ConcurrentHashMap一样将(key,value),作为一个元素整体来存储,至于为什么这样?我没找到相关资料。NonBlockingHashMap默认存储32个元素,共66个数组。
主要api分别是get、put、putIfAbsent、remove 、size 。

  @Override
  public TypeV get( Object key ) {
    final int fullhash= hash (key); // throws NullPointerException if key is null
    final Object V = get_impl(this,_kvs,key,fullhash);
    assert !(V instanceof Prime); // Never return a Prime
    return (TypeV)V;
  }
  @Override
  public TypeV   put        ( TypeK  key, TypeV val ) { return putIfMatch( key,      val, NO_MATCH_OLD); }
@Override
  public TypeV   putIfAbsent( TypeK  key, TypeV val ) { return putIfMatch( key,      val, TOMBSTONE   ); }
  @Override
  public TypeV   remove     ( Object key )            { return putIfMatch( key,TOMBSTONE, NO_MATCH_OLD); }

  @Override 
  public int     size       ( )                       { return chm(_kvs).size(); }

NO_MATCH_OLD代表直接的put。TOMBSTONE代表对应的(key,val)不存在才插入。MATCH_ANY代表对应的(key,val)存在才插入。接着看putIfMatch:

 private final TypeV putIfMatch( Object key, Object newVal, Object oldVal ) {
    if (oldVal == null || newVal == null) throw new NullPointerException();
    final Object res = putIfMatch( this, _kvs, key, newVal, oldVal );
    assert !(res instanceof Prime);
    assert res != null;
    return res == TOMBSTONE ? null : (TypeV)res;
  }

下面看5个参数的putIfMatch:

// --- putIfMatch ---------------------------------------------------------
  // Put, Remove, PutIfAbsent, etc.  Return the old value.  If the returned
  // value is equal to expVal (or expVal is NO_MATCH_OLD) then the put can be
  // assumed to work (although might have been immediately overwritten).  Only
  // the path through copy_slot passes in an expected value of null, and
  // putIfMatch only returns a null if passed in an expected null.
  private static final Object putIfMatch( final NonBlockingHashMap topmap, final Object[] kvs, final Object key, final Object putval, final Object expVal ) {
    assert putval != null;
    assert !(putval instanceof Prime);
    assert !(expVal instanceof Prime);
    final int fullhash = hash  (key); // throws NullPointerException if key null
    final int len      = len   (kvs); // Count of key/value pairs, reads kvs.length
    final CHM chm      = chm   (kvs); // Reads kvs[0]
    final int[] hashes = hashes(kvs); // Reads kvs[1], read before kvs[0]
    int idx = fullhash & (len-1);

    // ---
    // Key-Claim stanza: spin till we can claim a Key (or force a resizing).
    int reprobe_cnt=0;
    Object K=null, V=null;
    Object[] newkvs=null;
    while( true ) {             // Spin till we get a Key slot
      V = val(kvs,idx);         // Get old value (before volatile read below!)
      K = key(kvs,idx);         // Get current key
      if( K == null ) {         // Slot is free?
        // Found an empty Key slot - which means this Key has never been in
        // this table.  No need to put a Tombstone - the Key is not here!
        if( putval == TOMBSTONE ) return putval; // Not-now & never-been in this table
        // Claim the null key-slot
        if( CAS_key(kvs,idx, null, key ) ) { // Claim slot for Key
          chm._slots.add(1);      // Raise key-slots-used count
          hashes[idx] = fullhash; // Memoize fullhash
          break;                  // Got it!
        }
        // CAS to claim the key-slot failed.
        //
        // This re-read of the Key points out an annoying short-coming of Java
        // CAS.  Most hardware CAS's report back the existing value - so that
        // if you fail you have a *witness* - the value which caused the CAS
        // to fail.  The Java API turns this into a boolean destroying the
        // witness.  Re-reading does not recover the witness because another
        // thread can write over the memory after the CAS.  Hence we can be in
        // the unfortunate situation of having a CAS fail *for cause* but
        // having that cause removed by a later store.  This turns a
        // non-spurious-failure CAS (such as Azul has) into one that can
        // apparently spuriously fail - and we avoid apparent spurious failure
        // by not allowing Keys to ever change.
        K = key(kvs,idx);       // CAS failed, get updated value
        assert K != null;       // If keys[idx] is null, CAS shoulda worked
      }
      // Key slot was not null, there exists a Key here

      // We need a volatile-read here to preserve happens-before semantics on
      // newly inserted Keys.  If the Key body was written just before inserting
      // into the table a Key-compare here might read the uninitalized Key body.
      // Annoyingly this means we have to volatile-read before EACH key compare.
      newkvs = chm._newkvs;     // VOLATILE READ before key compare

      if( keyeq(K,key,hashes,idx,fullhash) )
        break;                  // Got it!

      // get and put must have the same key lookup logic!  Lest 'get' give
      // up looking too soon.
      //topmap._reprobes.add(1);
      if( ++reprobe_cnt >= reprobe_limit(len) || // too many probes or
          K == TOMBSTONE ) { // found a TOMBSTONE key, means no more keys
        // We simply must have a new table to do a 'put'.  At this point a
        // 'get' will also go to the new table (if any).  We do not need
        // to claim a key slot (indeed, we cannot find a free one to claim!).
        newkvs = chm.resize(topmap,kvs);
        if( expVal != null ) topmap.help_copy(newkvs); // help along an existing copy
        return putIfMatch(topmap,newkvs,key,putval,expVal);
      }

      idx = (idx+1)&(len-1); // Reprobe!
    } // End of spinning till we get a Key slot

    // ---
    // Found the proper Key slot, now update the matching Value slot.  We
    // never put a null, so Value slots monotonically move from null to
    // not-null (deleted Values use Tombstone).  Thus if 'V' is null we
    // fail this fast cutout and fall into the check for table-full.
    if( putval == V ) return V; // Fast cutout for no-change

    // See if we want to move to a new table (to avoid high average re-probe
    // counts).  We only check on the initial set of a Value from null to
    // not-null (i.e., once per key-insert).  Of course we got a 'free' check
    // of newkvs once per key-compare (not really free, but paid-for by the
    // time we get here).
    if( newkvs == null &&       // New table-copy already spotted?
        // Once per fresh key-insert check the hard way
        ((V == null && chm.tableFull(reprobe_cnt,len)) ||
         // Or we found a Prime, but the JMM allowed reordering such that we
         // did not spot the new table (very rare race here: the writing
         // thread did a CAS of _newkvs then a store of a Prime.  This thread
         // reads the Prime, then reads _newkvs - but the read of Prime was so
         // delayed (or the read of _newkvs was so accelerated) that they
         // swapped and we still read a null _newkvs.  The resize call below
         // will do a CAS on _newkvs forcing the read.
         V instanceof Prime) )
      newkvs = chm.resize(topmap,kvs); // Force the new table copy to start
    // See if we are moving to a new table.
    // If so, copy our slot and retry in the new table.
    if( newkvs != null )
      return putIfMatch(topmap,chm.copy_slot_and_check(topmap,kvs,idx,expVal),key,putval,expVal);

    // ---
    // We are finally prepared to update the existing table
    while( true ) {
      assert !(V instanceof Prime);

      // Must match old, and we do not?  Then bail out now.  Note that either V
      // or expVal might be TOMBSTONE.  Also V can be null, if we've never
      // inserted a value before.  expVal can be null if we are called from
      // copy_slot.

      if( expVal != NO_MATCH_OLD && // Do we care about expected-Value at all?
          V != expVal &&            // No instant match already?
          (expVal != MATCH_ANY || V == TOMBSTONE || V == null) &&
          !(V==null && expVal == TOMBSTONE) &&    // Match on null/TOMBSTONE combo
          (expVal == null || !expVal.equals(V)) ) // Expensive equals check at the last
        return V;                                 // Do not update!

      // Actually change the Value in the Key,Value pair
      if( CAS_val(kvs, idx, V, putval ) ) {
        // CAS succeeded - we did the update!
        // Both normal put's and table-copy calls putIfMatch, but table-copy
        // does not (effectively) increase the number of live k/v pairs.
        if( expVal != null ) {
          // Adjust sizes - a striped counter
          if(  (V == null || V == TOMBSTONE) && putval != TOMBSTONE ) chm._size.add( 1);
          if( !(V == null || V == TOMBSTONE) && putval == TOMBSTONE ) chm._size.add(-1);
        }
        return (V==null && expVal!=null) ? TOMBSTONE : V;
      } 
      // Else CAS failed
      V = val(kvs,idx);         // Get new value
      // If a Prime'd value got installed, we need to re-run the put on the
      // new table.  Otherwise we lost the CAS to another racing put.
      // Simply retry from the start.
      if( V instanceof Prime )
        return putIfMatch(topmap,chm.copy_slot_and_check(topmap,kvs,idx,expVal),key,putval,expVal);
    }
  }

NonBlockingHashMap类几乎每一行都有注释,这个方法是我们put的主逻辑。分成三个部分来看,无论被哪个接口调用时候,第一步是根据key hashcode来定位,假如已经存在数据,并且两个key不相等,则线性探测+1,继续循环,假如不存在数据,则用cas存储key,若失败表示有并发则线性探测+1,继续循环。第二步如果失败的次数达到一定的程度(map总容量的1/4+10)或者key==TOMBSTONE或者newkvs为null,V为null并且需要增加value而现在map容量过小或者V是Prime类型,那我们需要扩容。第三步用CAS更新所对应value。假如失败,然后查看是否可以重试,或者需要往新的kvs里面插入(扩容情况)。以上详细讲。然后是扩容。
第一步将多余的注释和代码删除后得到如下代码:

while( true ) {             // Spin till we get a Key slot
  V = val(kvs,idx);         // Get old value (before volatile read below!)
  K = key(kvs,idx);         // Get current key
  if( K == null ) {         // Slot is free?
    if( putval == TOMBSTONE ) return putval; // Not-now & never-been in this table
    if( CAS_key(kvs,idx, null, key ) ) { // Claim slot for Key
      chm._slots.add(1);      // Raise key-slots-used count
      hashes[idx] = fullhash; // Memoize fullhash
      break;                  // Got it!
    }
    K = key(kvs,idx);       // CAS failed, get updated value
    assert K != null;       // If keys[idx] is null, CAS shoulda worked
  }

  newkvs = chm._newkvs;     // VOLATILE READ before key compare

  if( keyeq(K,key,hashes,idx,fullhash) )
    break;                  // Got it!

  if( ++reprobe_cnt >= reprobe_limit(len) || // too many probes or
      key == TOMBSTONE ) { // found a TOMBSTONE key, means no more keys

    newkvs = chm.resize(topmap,kvs);
    if( expVal != null ) topmap.help_copy(newkvs); // help along an existing copy
    return putIfMatch(topmap,newkvs,key,putval,expVal);
  }

  idx = (idx+1)&(len-1); // Reprobe!
}

2-3行,根据前面hash码计算出的idx来定位对象组_kvs中的K和V。
4-13行,处理当发现K是null的情况,这种情况下假如调用的是remove方法,可以直接结束。否则就cas将key填入对应的下标。如果成功则增加size(后面会详细讲,这是个单独的类)。并且将计算出的fullhash填入hashes以备后用。然后直接跳出循环。
15-18行,假如cas操作失败,或则K!=null,那么我们就得对比K与我们尝试的key的值(传入的hashes显然是之前存入,用于此时的对比),这里的第15行比较特别:newkvs = chm._newkvs; 因为_newkvs是volatile变量,所以先读_newkvs的这个volatile read的语义能够确保接下来,K读到的数据是初始化完全的,从而能够参与equals对比。假如对比的结果是true,说明找对了K,则跳出这个循环。
20-26行,到了这里说明K不对,我们就要继续找对的。首先增加一个reprobe_cnt 用于统计失败次数。如果失败的次数达到一定的程度(map总容量的1/4+10)或者key==TOMBSTONE,则扩容。
28行,向右移动一个节点查再次查找。

第二部分扩容:

if( putval == V ) return V; // Fast cutout for no-change
if( newkvs == null &&       // New table-copy already spotted?
    // Once per fresh key-insert check the hard way
    ((V == null && chm.tableFull(reprobe_cnt,len)) ||
     V instanceof Prime) )
  newkvs = chm.resize(topmap,kvs); // Force the new table copy to start
if( newkvs != null )
  return putIfMatch(topmap,chm.copy_slot_and_check(topmap,kvs,idx,expVal),key,putval,expVal);

第一部分的代码包含一部分第二部分代码
1行,假如put进来的val与原来的值相同,则不需要做工作直接返回。
2-6行,newkvs为null,V为null并且需要增加value而现在map容量过小或者V是Prime类型,那我们需要扩容,得到新的newkvs。
7-8行,如果返现旧值新的kvs已经构造了,我们尝试先将旧值复制到新的kvs上(此过程可能需要协助复制旧kvs的一部分数据),然后接着将当前值put进新的kvs。

第三部分:

while( true ) {
  assert !(V instanceof Prime);

  if( expVal != NO_MATCH_OLD && // Do we care about expected-Value at all?
      V != expVal &&            // No instant match already?
      (expVal != MATCH_ANY || V == TOMBSTONE || V == null) &&
      !(V==null && expVal == TOMBSTONE) &&    // Match on null/TOMBSTONE combo
      (expVal == null || !expVal.equals(V)) ) // Expensive equals check at the last
    return V;                                 // Do not update!

  if( CAS_val(kvs, idx, V, putval ) ) {

    if( expVal != null ) {
      // Adjust sizes - a striped counter
      if(  (V == null || V == TOMBSTONE) && putval != TOMBSTONE ) chm._size.add( 1);
      if( !(V == null || V == TOMBSTONE) && putval == TOMBSTONE ) chm._size.add(-1);
    }
    return (V==null && expVal!=null) ? TOMBSTONE : V;
  } 
  V = val(kvs,idx);         // Get new value
  if( V instanceof Prime )
    return putIfMatch(topmap,chm.copy_slot_and_check(topmap,kvs,idx,expVal),key,putval,expVal);
}

我觉得第三部分也就最后三句可讲解下,若cas设置V失败,则重新获取V,若V是Prime类型,则先协助久的kvs完成复制,然后往新的kvs插入数据。若不是Prime类型,则继续循环,这里有个问题,会不会死循环?单从代码来看有可能。

下面试着写下扩容resize方法源码分析:

    private final Object[] resize( NonBlockingHashMap topmap, Object[] kvs) {
      assert chm(kvs) == this;

      // Check for resize already in progress, probably triggered by another thread
      Object[] newkvs = _newkvs; // VOLATILE READ
      if( newkvs != null )       // See if resize is already in progress
        return newkvs;           // Use the new table already

      // No copy in-progress, so start one.  First up: compute new table size.
      int oldlen = len(kvs);    // Old count of K,V pairs allowed
      int sz = size();          // Get current table count of active K,V pairs
      int newsz = sz;           // First size estimate

      // Heuristic to determine new size.  We expect plenty of dead-slots-with-keys
      // and we need some decent padding to avoid endless reprobing.
      if( sz >= (oldlen>>2) ) { // If we are >25% full of keys then...
        newsz = oldlen<<1;      // Double size
        if( sz >= (oldlen>>1) ) // If we are >50% full of keys then...
          newsz = oldlen<<2;    // Double double size
      }
      // This heuristic in the next 2 lines leads to a much denser table
      // with a higher reprobe rate
      //if( sz >= (oldlen>>1) ) // If we are >50% full of keys then...
      //  newsz = oldlen<<1;    // Double size

      // Last (re)size operation was very recent?  Then double again; slows
      // down resize operations for tables subject to a high key churn rate.
      long tm = System.currentTimeMillis();
      long q=0;
      if( newsz <= oldlen && // New table would shrink or hold steady?
          tm <= topmap._last_resize_milli+10000 && // Recent resize (less than 1 sec ago)
          (q=_slots.estimate_get()) >= (sz<<1) ) // 1/2 of keys are dead?
        newsz = oldlen<<1;      // Double the existing size

      // Do not shrink, ever
      if( newsz < oldlen ) newsz = oldlen;

      // Convert to power-of-2
      int log2;
      for( log2=MIN_SIZE_LOG; (1<<log2) < newsz; log2++ ) ; // Compute log2 of size

      // Now limit the number of threads actually allocating memory to a
      // handful - lest we have 750 threads all trying to allocate a giant
      // resized array.
      long r = _resizers;
      while( !_resizerUpdater.compareAndSet(this,r,r+1) )
        r = _resizers;
      // Size calculation: 2 words (K+V) per table entry, plus a handful.  We
      // guess at 32-bit pointers; 64-bit pointers screws up the size calc by
      // 2x but does not screw up the heuristic very much.
      int megs = ((((1<<log2)<<1)+4)<<3/*word to bytes*/)>>20/*megs*/;
      if( r >= 2 && megs > 0 ) { // Already 2 guys trying; wait and see
        newkvs = _newkvs;        // Between dorking around, another thread did it
        if( newkvs != null )     // See if resize is already in progress
          return newkvs;         // Use the new table already
        // TODO - use a wait with timeout, so we'll wakeup as soon as the new table
        // is ready, or after the timeout in any case.
        //synchronized( this ) { wait(8*megs); }         // Timeout - we always wakeup
        // For now, sleep a tad and see if the 2 guys already trying to make
        // the table actually get around to making it happen.
        try { Thread.sleep(8*megs); } catch( Exception e ) { }
      }
      // Last check, since the 'new' below is expensive and there is a chance
      // that another thread slipped in a new thread while we ran the heuristic.
      newkvs = _newkvs;
      if( newkvs != null )      // See if resize is already in progress
        return newkvs;          // Use the new table already

      // Double size for K,V pairs, add 1 for CHM
      newkvs = new Object[((1<<log2)<<1)+2]; // This can get expensive for big arrays
      newkvs[0] = new CHM(_size); // CHM in slot 0
      newkvs[1] = new int[1<<log2]; // hashes in slot 1

      // Another check after the slow allocation
      if( _newkvs != null )     // See if resize is already in progress
        return _newkvs;         // Use the new table already

      // The new table must be CAS'd in so only 1 winner amongst duplicate
      // racing resizing threads.  Extra CHM's will be GC'd.
      if( CAS_newkvs( newkvs ) ) { // NOW a resize-is-in-progress!
        //notifyAll();            // Wake up any sleepers
        //long nano = System.nanoTime();
        //System.out.println(" "+nano+" Resize from "+oldlen+" to "+(1<<log2)+" and had "+(_resizers-1)+" extras" );
        //if( System.out != null ) System.out.print("["+log2);
        topmap.rehash();        // Call for Hashtable's benefit
      } else                    // CAS failed?
        newkvs = _newkvs;       // Reread new table
      return newkvs;
    }

16-20行,计算新扩容长度,如果当前元素对数达到1/4,则扩容为原来两倍,如果达到1/2,扩容为4倍。
28-36行,如果新扩容长度小等于原来的长度,并且最近调整时间小于等于1s,并且_slots的estimate_get长度小于原来数据长度的一半进行缩减map(关于_slots后续会详细讲,这个类还进行统计size的计算),根据(if( newsz < oldlen ) newsz = oldlen)来看,至少维持当前的容量。
39-62行,计算megs。计算当前参与resize的线程个数,假如多于2个则多sleep一会儿8*megs,然后取得_newkvs。
余下的代码,构造newkvs ,并且尝试CAS将它替代,然后返回构造完成的_newkvs。
注:resize方法仅仅计算扩容大小,不做额外的操作。
来看help_copy方法,因为help_copy最终调用的是help_copy_impl,直接看help_copy_impl方法:

 private final void help_copy_impl( NonBlockingHashMap topmap, Object[] oldkvs, boolean copy_all ) {
      assert chm(oldkvs) == this;
      Object[] newkvs = _newkvs;
      assert newkvs != null;    // Already checked by caller
      int oldlen = len(oldkvs); // Total amount to copy
      final int MIN_COPY_WORK = Math.min(oldlen,1024); // Limit per-thread work

      // ---
      int panic_start = -1;
      int copyidx=-9999;            // Fool javac to think it's initialized
      while( _copyDone < oldlen ) { // Still needing to copy?
        // Carve out a chunk of work.  The counter wraps around so every
        // thread eventually tries to copy every slot repeatedly.

        // We "panic" if we have tried TWICE to copy every slot - and it still
        // has not happened.  i.e., twice some thread somewhere claimed they
        // would copy 'slot X' (by bumping _copyIdx) but they never claimed to
        // have finished (by bumping _copyDone).  Our choices become limited:
        // we can wait for the work-claimers to finish (and become a blocking
        // algorithm) or do the copy work ourselves.  Tiny tables with huge
        // thread counts trying to copy the table often 'panic'.
        if( panic_start == -1 ) { // No panic?
          copyidx = (int)_copyIdx;
          while( copyidx < (oldlen<<1) && // 'panic' check
                 !_copyIdxUpdater.compareAndSet(this,copyidx,copyidx+MIN_COPY_WORK) )
            copyidx = (int)_copyIdx;      // Re-read
          if( !(copyidx < (oldlen<<1)) )  // Panic!
            panic_start = copyidx;        // Record where we started to panic-copy
        }

        // We now know what to copy.  Try to copy.
        int workdone = 0;
        for( int i=0; i<MIN_COPY_WORK; i++ )
          if( copy_slot(topmap,(copyidx+i)&(oldlen-1),oldkvs,newkvs) ) // Made an oldtable slot go dead?
            workdone++;         // Yes!
        if( workdone > 0 )      // Report work-done occasionally
          copy_check_and_promote( topmap, oldkvs, workdone );// See if we can promote
        //for( int i=0; i<MIN_COPY_WORK; i++ )
        //  if( copy_slot(topmap,(copyidx+i)&(oldlen-1),oldkvs,newkvs) ) // Made an oldtable slot go dead?
        //    copy_check_and_promote( topmap, oldkvs, 1 );// See if we can promote

        copyidx += MIN_COPY_WORK;
        // Uncomment these next 2 lines to turn on incremental table-copy.
        // Otherwise this thread continues to copy until it is all done.
        if( !copy_all && panic_start == -1 ) // No panic?
          return;       // Then done copying after doing MIN_COPY_WORK
      }
      // Extra promotion check, in case another thread finished all copying
      // then got stalled before promoting.
      copy_check_and_promote( topmap, oldkvs, 0 );// See if we can promote
    }

6行:取出原有数据长度和1024较小的一个,作为一次性复制的一段数据个数。
9-31行:copyidx用于复制数据开始的下标,假如copyidx超过oldlen*2,则说明当前所有数据都正在被复制。panic_start用于确保数据复制完成,看了下代码参数意义不大,因为每个线程都会执行数据复制是否完成的逻辑。
其余代码是for循环完成复制数据和校验是否复制完成的两个方法,即copy_slot和copy_check_and_promote方法,分析这两个源码方法之前,先看另外一个入口copy_slot_and_check,这个方法是get和putIfMatch调用的:

 private final Object[] copy_slot_and_check( NonBlockingHashMap topmap, Object[] oldkvs, int idx, Object should_help ) {
      assert chm(oldkvs) == this;
      Object[] newkvs = _newkvs; // VOLATILE READ
      // We're only here because the caller saw a Prime, which implies a
      // table-copy is in progress.
      assert newkvs != null;
      if( copy_slot(topmap,idx,oldkvs,_newkvs) )   // Copy the desired slot
        copy_check_and_promote(topmap, oldkvs, 1); // Record the slot copied
      // Generically help along any copy (except if called recursively from a helper)
      return (should_help == null) ? newkvs : topmap.help_copy(newkvs);
    }

这段逻辑比较清晰调用数据复制copy_slot和确保是否复制完成copy_check_and_promote,第三行同样是VOLATILE保证数据可见性。
下面分析下copy_slot方法,去除了多余的注释:

  private boolean copy_slot( NonBlockingHashMap topmap, int idx, Object[] oldkvs, Object[] newkvs ) {
      Object key;
      while( (key=key(oldkvs,idx)) == null )
        CAS_key(oldkvs,idx, null, TOMBSTONE);
      Object oldval = val(oldkvs,idx); // Read OLD table
      while( !(oldval instanceof Prime) ) {
        final Prime box = (oldval == null || oldval == TOMBSTONE) ? TOMBPRIME : new Prime(oldval);
        if( CAS_val(oldkvs,idx,oldval,box) ) { // 
          if( box == TOMBPRIME )
            return true;
          oldval = box;         // Record updated oldval
          break;                // Break loop; oldval is now boxed by us
        }
        oldval = val(oldkvs,idx); // Else try, try again
      }
      if( oldval == TOMBPRIME ) return false; // Copy already complete here!
      Object old_unboxed = ((Prime)oldval)._V;
      assert old_unboxed != TOMBSTONE;
      boolean copied_into_new = (putIfMatch(topmap, newkvs, key, old_unboxed, null) == null);
      while( !CAS_val(oldkvs,idx,oldval,TOMBPRIME) )
        oldval = val(oldkvs,idx);
      return copied_into_new;
    } // end copy_slot
  } // End of CHM

3-4行,如果key为null,则替换为TOMBSTONE。这样做的目的是因为在put数据时,并不是直接判断是否正在扩容,而是直接新增数据,此时给一个标识该位置已经扩容过,新增数据时,会跳过这个位置。
这里多说下,remove数据时,remove会将value的值变成TOMBSTONE,并不是直接删除,是为了get正确的数据,因为get退出条件是K==null,所以NonBlockingHashMap最差时间复杂度需要遍历整个数组,而ConcurrentHashMap利用数组+链表或者红黑树代替,减少了检索时间。
5-16行,判断oldval是否需要更新成TOMBSTONE类型还是Prime类型。
17-22行,调用putIfMatch将oldval复制到新的newkvs。

copy_check_and_promote方法就暂不看了,主要是将_copyDone数据更新并检查是否完成复制将旧oldkvs替换成新的_newkvs。

remove get源码便不写了,下面主要写下size计算,计算size的类是在ConcurrentAutoTable类,主要方法是add_if_mask:

    public long add_if_mask( long x, long mask, int hash, ConcurrentAutoTable master ) {
      long[] t = _t;
      int idx = hash & (t.length-1);
      // Peel loop; try once fast
      long old = t[idx];
      boolean ok = CAS( t, idx, old&~mask, old+x );
      if( _sum_cache != Long.MIN_VALUE )
        _sum_cache = Long.MIN_VALUE; // Blow out cache
      if( ok ) return old;      // Got it
      if( (old&mask) != 0 ) return old; // Failed for bit-set under mask
      // Try harder
      int cnt=0;
      while( true ) {
        old = t[idx];
        if( (old&mask) != 0 ) return old; // Failed for bit-set under mask
        if( CAS( t, idx, old, old+x ) ) break; // Got it!
        cnt++;
      }
      if( cnt < MAX_SPIN ) return old; // Allowable spin loop count
      if( t.length >= 1024*1024 ) return old; // too big already

      // Too much contention; double array size in an effort to reduce contention
      long r = _resizers;
      int newbytes = (t.length<<1)<<3/*word to bytes*/;
      while( !_resizerUpdater.compareAndSet(this,r,r+newbytes) )
        r = _resizers;
      r += newbytes;
      if( master._cat != this ) return old; // Already doubled, don't bother
      if( (r>>17) != 0 ) {      // Already too much allocation attempts?
        // TODO - use a wait with timeout, so we'll wakeup as soon as the new
        // table is ready, or after the timeout in any case.  Annoyingly, this
        // breaks the non-blocking property - so for now we just briefly sleep.
        //synchronized( this ) { wait(8*megs); }         // Timeout - we always wakeup
        try { Thread.sleep(r>>17); } catch( InterruptedException e ) { }
        if( master._cat != this ) return old;
      }

      CAT newcat = new CAT(this,t.length*2,0);
      // Take 1 stab at updating the CAT with the new larger size.  If this
      // fails, we assume some other thread already expanded the CAT - so we
      // do not need to retry until it succeeds.
      master.CAS_cat(this,newcat);
      return old;
    }

_t是统计size的数组,获取size时就是遍历整个_t数组。
2-8行,根据idx获取_t索引下的值,利用cas更新结果,若更新成功,则结束,否则代表多个线程竞争。
11-19行,死循环根据idx获取_t索引下的值,利用cas更新结果,直到更新成功或者(old&mask) != 0直接return结束。18,19行代表竞争小于2或者大等于1024*1024直接结束。
22-35行,已经有太多分配尝试,则休眠一段时间。
最后几行代码代表将数据扩容2倍的长度。

至此,分析源码也算结束了,NonBlockingHashMap和jdk1.8ConcurrentHashMap有一些相似点,扩容时利用多线程加速复制数据,利用数组统计size减少线程竞争。至于两者性能对比我只找到了最近时间一篇文章链接为:https://fmt.ewi.utwente.nl/media/211.pdf 里面提到说ConcurrentHashMa在性能和伸缩性上的表现都是好于NonBlockingHashMap的。具体的需要自己做些benchmark。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值