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)