ConcurrentHashMap

10 篇文章 0 订阅


ConcurrentHashMap(jdk1.6)(并发读无需锁。锁分离:和Hashtable主要区别就是锁的粒度及锁法,类似把一个大的HashTable分解成多个): 内部用final段(Segment (static final classSegment<K,V> extends ReentrantLock implements Serializable {  private static final long serialVersionUID = 2249069246763182397L;//类结构版本标记    transient(不参与序列化) int modCount(统计段结构改变的次数,为了检测对多个段进行遍历过程中某个段是否发生改变,跨段操作);   transient int threshold(rehash/size的阈值);   final float loadFactor;  transient volatile int count(统计该段数据的个数,协调修改和读,以保证能够读取到几乎最新的修改。每次修改了结构后,如增加/删除节点,都要写count值,每次读都要先读count的值);    transient volatileHashEntry<K,V>[] table(是个hash链,每个元素(HashEntry(节点static final(不变性的访问不需要同步从而节省时间) classHashEntry<K,V> {       final K key;      final int hash;     volatile V value; //为了确保读到最新的值,避免了加锁    final HashEntry<K,V> next;   }HashMap在hash链的中间添加或删除元素,读操作不加锁将得到不一致的数据,保证HashEntry几乎是不可变的,即不能从hash链的中间或尾部添加或删除节点,因为这需要修改next 引用值,所有的节点的修改只能从头部开始;put操作,可一律添加到Hash链的头部;remove操作,可能需要从中间删除一节点,这需要将要删除节点的前面所有节点整个复制一遍,最后一节点指向要删除结点的下一个结点));  }))数组 (和HashMap使用相同的解决hash碰撞方式,将hash值相同的节点放在一hash链中,但ConcurrentHashMap使用多个子Hash表,即段: public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>  implements ConcurrentMap<K, V>, Serializable {      final int segmentMask;      final int segmentShift;    final Segment<K,V>[] segments; //仅将数组声明为final的并不保证其成员也是final的,不过这里成员实现成了final的,可确保不会出现死锁,因为获得锁的顺序是固定的  }所有的成员都是final的,segmentMask和segmentShift定位段:final Segment<K,V> segmentFor(int hash) {     return segments[(hash >>> segmentShift) & segmentMask];   }。为了加速定位段及段中hash槽,每个段hash槽的的个数都是2^n(便于位运算定位,但可能导致hash槽分配不均然后要对hash值重新hash)。并发级别为默认值16时,即段的个数,hash值的高4位决定分配在哪个段中)表示不同的部分,每段就是一个hashtable,有自己的锁。在不同的段上修改可以并发。一个大数组要在多个线程共享时就可以考虑把它分成多个节点,避免大锁,并可以考虑通过hash算法定位模块;设计数据表的事务时(事务某种意义上也是同步机制的体现),可把一个表看成一个需要同步的数组,若操作的表数据太多就可以考虑事务分离(所以要避免大表),比如把数据依字段拆分,水平分表。

public V remove(Object key) {    hash = hash(key.hashCode());     return segmentFor(hash).remove(key, hash, null);   }先定位到段,然后委托给段的remove操作。并发是删除不同段数据。Segment。remove V remove(Object key, int hash, Object value) {      lock();      try {         int c = count - 1;    HashEntry<K,V>[] tab = table;        int index = hash & (tab.length - 1);          HashEntry<K,V> first = tab[index];         HashEntry<K,V> e = first;        while (e != null && (e.hash != hash || !key.equals(e.key)))    e = e.next;        V oldValue = null;       if (e != null) {          V v = e.value;             if (value == null || value.equals(v)) {             oldValue = v;   // All entries following removed node can stayin list, but all preceding ones need to be  cloned.             ++modCount;              HashEntry<K,V> newFirst = e.next;  for(HashEntry<K,V> p = first; p != e; p = p.next)//将定位之后的所有entry克隆并拼回前面去,每次删除一个元素就要将那之前的元素克隆一遍,是由entry的不变性来决定的,第一次设置了next域之后便不能再改变它,取而代之的是将它之前的节点全都克隆一次。newFirst = new HashEntry<K,V>(p.key, p.hash,  newFirst, p.value);       tab[index] = newFirst;             count = c; // write-volatile        }  }  return oldValue;   } finally {      unlock();     }  }整个操作是在持有段锁时执行的,空白行之前的行主要是定位到要删除的节点e,如果不存在这个节点就返回null,否则将e前面的结点复制一遍,尾结点指向e的下一个结点。e后面的结点不需要复制,它们可以重用. 要删除的结点存在时,删除的最后一步要将count的值减一,必须是最后一步,否则读取操作可能看不到之前对段所做的结构性修改;remove执行的开始就将table(volatile变量,读写volatile变量的开销很大,编译器也优化不了,所以)赋给一个局部变量tab。

put也是委托给段的put: V put(K key, int hash, V value, boolean onlyIfAbsent) {  
  lock();  try {  
    int c = count;  
    if (c++ > threshold) // ensure capacity  
    rehash();  //updating
  HashEntry<K,V>[] tab = table;  
  int index = hash & (tab.length - 1);  //每个segment是一个传统意义上的hashtable
  HashEntry<K,V> first = tab[index];  //找出需要的entry在table的哪一个位置
    HashEntry<K,V> e = first;  //得到的entry就是这个链的第一个节点
  while (e != null && (e.hash != hash || !key.equals(e.key)))         e = e.next;         V oldValue;  
  if (e != null) {   oldValue = e.value;            if (!onlyIfAbsent) //替换节点的值   e.value = value;       }  
  else {          oldValue = null;           ++modCount;          tab[index] = new HashEntry<K,V>(key, hash, 后继是first, value);  
 // 将新entry插入到链头     count = c; // write-volatile         }  return oldValue;      } finally {        unlock();       }  }put方法也是在持有段锁(锁定整个segment)时执行的,为了并发的安全,但修改数据不能并发,得判断是否超限以确保容量不足时能rehash,再找是否存在同样key的结点,如果存在就直接替换这个结点的值,否则创建一个新结点并添加到hash链头部,并修改modCount和count(在最后一步修改)的值。

putAll多次调用put方法,replace甚至不用做结构上的更改.

get委托给Segment的get方法: V get(Object key, int hash) {  

if (count != 0) { // read-volatile 当前桶的数据个数是否为0 
    HashEntry<K,V> e = getFirst(hash);  得到头节点
  while (e != null) {  
        if (e.hash == hash && key.equals(e.key)) {          V v = e.value;          if (v != null) return v;  
        return readValueUnderLock(e); // recheck(V readValueUnderLock(HashEntry<K,V> e) {   lock();     try {       return e.value;     } finally {       unlock();     }  }
boolean containsKey(Object key, int hash) {  //不需读取值
    if (count != 0) { // read-volatile          HashEntry<K,V> e = getFirst(hash);  
  while (e != null) {             if (e.hash == hash && key.equals(e.key))                return true;  
         e = e.next;          }      }      return false;  } } )         
e = e.next;    }  }    return null;  }get无需锁。先访问count(对于非结构更新,也就是结点值的改变,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值), 再根据hash和key遍历(不需要加锁的原因在于链指针next是final的,但头指针却不是,这是通过getFirst(hash)返回的,即存在 table数组中的值,这使得getFirst(hash)可能返回过时的头结点,如get时,刚执行完getFirst(hash)后,另一个线程执行了删除并更新头结点,这就导致get方法中返回的头结点不是最新的。通过对count变量的协调机制get能读取到几乎最新的数据,虽然可能不是最新的。要得到最新的数据只有采用完全的同步。)hash链找到要获取的结点,没有就返回null,如果找到了所求的结点,判断它的值,非空就直接返回,否则在有锁的状态下再读一次。理论上结点的值不可能为空,因为 put时判断如果为空就抛NullPointerException,空值的唯一源头就是HashEntry中的默认值,因为 HashEntry中的value不是final的,非同步读取有可能读取到空值。tab[index] = new HashEntry<K,V>(key, hash, first, value),HashEntry构造函数中对value的赋值以及对tab[index]的赋值可能被重新排序,这就可能导致结点的值为空。这里当v为空时,可能是一个线程正在改变节点,而之前的get操作都未进行锁定,根据bernstein条件,读后写或写后读都会引起数据的不一致,所以这里要对e重新上锁再读以保证得到的是正确值。


ConcurrentHashMap(jdk1.8)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值