ConcurrentHashMap 原始碼分析

dustinny發表於2021-03-06

  和HashMap不同的是,ConcurrentHashMap採用分段加鎖的方式保障執行緒安全,JDK 1.8之後,ConcurrentHashMap的底層資料結構從1.8開始跟HashMap差不多。

  HashTable也是執行緒安全的,儲存Key-Value鍵值對的資料結構,Key和Value都不能為空,但不推薦使用,因為其所有的方法採用synchronized修飾,效率低。

  Key和Value都不能為Null的原因是:如果map.get(key)返回null,可以認為是value的值本來就是null,也可以認為map中不存在key的儲存資料,因此具有二義性,但HashMap在單執行緒環境,可以透過map.containsKey(key)判斷,消除而已性。

  但在多執行緒環境中,map.get(key)和map.containsKey(key)是非原子的操作,可能線上程A的兩個語句執行之間,其他執行緒B執行map.put(key,value),導致執行緒A無法消除上面的二義性。

  下圖是ConcurrentHashMap的UML關係圖。

  image-20200809215755661

  1、底層儲存結構

  1.1、JDK 1.7的儲存結構,瞭解即可

  在JDK 1.7,ConcurrentHashMap透過對Segment的分段加鎖實現執行緒安全。一個Segment裡面就是HashMap的儲存結構,可以擴容。Segment的資料量初始化以後不可以更改,預設值16,因此預設支援16個執行緒同時操作ConcurrentHashMap。

  img

  1.2 JDK 1.8的儲存結構

  JDK 1.8之後,儲存結構變化比較大,跟HashMap類似。紅黑樹節點小於某個數(預設值6)又會轉換為連結串列。

  image-20200809221531416

  []ConcurrentHashMap的主要成員變數,類似HashMap,補上註釋

  2、ConcurrentHashMap的構造方法

  ConcurrentHashMap的預設構造容量為16,在初始化的時候並不會初始化table陣列。同HashMap一樣,在put第一個元素的時候才會initTable()初始化陣列。

  /**Creates a new,empty map with the default initial table size(16).*/

  public ConcurrentHashMap(){

  }

  //設定初始化大小的建構函式

  public ConcurrentHashMap(int initialCapacity){

  this(initialCapacity,LOAD_FACTOR,1);

  }

  //根據傳入的map初始化

  public ConcurrentHashMap(Map<?extends K,?extends V>m){

  this.sizeCtl=DEFAULT_CAPACITY;

  putAll(m);

  }

  //設定初始容量和載入因子的大小

  public ConcurrentHashMap(int initialCapacity,float loadFactor){

  this(initialCapacity,loadFactor,1);

  }

  //初始容量、載入因子、併發級別

  public ConcurrentHashMap(int initialCapacity,

  float loadFactor,int concurrencyLevel){

  //資料校驗

  if(!(loadFactor>0.0f)||initialCapacity<0||concurrencyLevel<=0)

  throw new IllegalArgumentException();

  //如果初始容量小於併發級別

  if(initialCapacity<concurrencyLevel)//Use at least as many bins

  initialCapacity=concurrencyLevel;//as estimated threads

  //一些比較

  long size=(long)(1.0+(long)initialCapacity/loadFactor);

  int cap=(size>=(long)MAXIMUM_CAPACITY)?

  MAXIMUM_CAPACITY:tableSizeFor((int)size);

  this.sizeCtl=cap;

  }

  3、get、put方法

  3.1 get方法,根據key找value,沒有返回null

  get的流程總體和HashMap差不多,只不過是透過頭結點的hash值判斷是紅黑樹還是連結串列。

  static final int MOVED=-1;//轉發節點?TODO作用?

  static final int TREEBIN=-2;//跟節點

  static final int RESERVED=-3;//臨時保留的節點?TODO作用?

  static final int HASH_BITS=0x7fffffff;//hash的擾動函式spread()計算用的

  //根據key獲取value值

  public V get(Object key){

  Node<K,V>[]tab;Node<K,V>e,p;int n,eh;K ek;

  //計算hash值

  int h=spread(key.hashCode());

  //集散所在的hash桶

  if((tab=table)!=null&&(n=tab.length)>0&&

  (e=tabAt(tab,(n-1)&h))!=null){

  if((eh=e.hash)==h){

  //頭結點,剛好是要找的節點

  if((ek=e.key)==key||(ek!=null&&key.equals(ek)))

  return e.val;

  }

  else if(eh<0)

  //頭結點hash值小於0,說明正在擴容或者是紅黑樹,find查詢

  return(p=e.find(h,key))!=null?p.val:null;

  while((e=e.next)!=null){

  //連結串列遍歷查詢

  if(e.hash==h&&

  ((ek=e.key)==key||(ek!=null&&key.equals(ek))))

  return e.val;

  }

  }

  return null;

  }

  3.2、put方法

  put方法的流程跟HashMap的流程差不多,不同點在於執行緒安全,自旋,CAS,synchronized

  onlyIfAbsent如果為true,如果已經存在了key,不會替換舊的值。

  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){

  //key和value都不能為null

  if(key==null||value==null)throw new NullPointerException();

  //計算hash(key)的擾動函式

  int hash=spread(key.hashCode());

  //離岸邊的長度

  int binCount=0;

  for(Node<K,V>[]tab=table;;){

  Node<K,V>f;int n,i,fh;K fk;V fv;

  //如果table還沒有初始化,就初始化table(自旋+CAS)

  if(tab==null||(n=tab.length)==0)

  tab=initTable();

  else if((f=tabAt(tab,i=(n-1)&hash))==null){

  //如果當前hash桶為null,直接放入,CAS加入,成功了就直接break

  if(casTabAt(tab,i,null,new Node<K,V>(hash,key,value)))

  break;//no lock when adding to empty bin

  }

  //TODO:

  else if((fh=f.hash)==MOVED)

  tab=helpTransfer(tab,f);

  else if(onlyIfAbsent//check first node without acquiring lock

  &&fh==hash

  &&((fk=f.key)==key||(fk!=null&&key.equals(fk)))

  &&(fv=f.val)!=null)

  return fv;

  else{

  //舊的值

  V oldVal=null;

  //加鎖

  synchronized(f){

  if(tabAt(tab,i)==f){

  if(fh>=0){

  binCount=1;

  for(Node<K,V>e=f;;++binCount){

  K ek;

  //如果存在hash(key)和key對應的節點,直接更改value值

  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);

  break;

  }

  }

  }

  //如果是紅黑樹,紅黑樹插入

  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;

  }

  }

  else if(f instanceof ReservationNode)

  throw new IllegalStateException("Recursive update");

  }

  }

  if(binCount!=0){

  //是否要轉為紅黑樹

  if(binCount>=TREEIFY_THRESHOLD)

  treeifyBin(tab,i);

  //舊的值

  if(oldVal!=null)

  return oldVal;

  break;

  }

  }

  }

  addCount(1L,binCount);

  return null;

  }

  4、TODO ConcurrentHashMap的擴容方法

  ConcurrentHashMap也是預設擴容2倍,擴容的方法transfer()

  Node<K,V>[]nt=(Node<K,V>[])new Node<?,?>[n<<1];

  5、總結

  ConcurrentHashMap在JDK 1.7和1.8變化很大,在JDK 1.7中,採用Segment分段儲存資料,也透過Segment分段加鎖。

  而在JDK 1.8中,使用synchronized鎖定hash桶的連結串列的首節點/紅黑樹的根節點,只要hash(key)不衝突,就不會影響其他執行緒。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69995861/viewspace-2761551/,如需轉載,請註明出處,否則將追究法律責任。

相關文章