推荐博文:
http://www.blogjava.net/DLevin/archive/2013/10/18/405030.html
http://www.iteye.com/topic/344876
http://ifeve.com/concurrenthashmap/
HashTable、ConcurrentHashMap与HashMao最大的不同就是在线程安全上,内部实现存储和获取数据的方式是一样的,都是通过hash来散列数据,必要的时候进行扩容。
HashTable中put(K key, V value)时:
// Make sure the value is not null if (value == null) { throw new NullPointerException(); }
public synchronized V put(K key, V value)
public synchronized V get(Object key)
如果有一个线程使用put元素,则另外的线程不能再向该HashTable put元素和get元素。
ConcurrentHashMap采用锁分段技术,解决锁竞争的问题。ConcurrentHashMap容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率。
首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
它与HashTable的不同就是锁粒度的不同。
从源码中可以看到ConcurrentHashMap的内部类Segment和HashMap的结构相似。
Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。
在JDK 1.7中采用了自旋的机制,进一步减少了加锁的可能性。
remove
remove(key):是一个segment私有的,所以在不同块中的元素不会存在线程安全问题。
public V remove(Object key) { int hash = hash(key); Segment<K,V> s = segmentForHash(hash); return s == null ? null : s.remove(key, hash, null); }
remove(key, hash, null)
tryLock()仅在调用时锁未被另一个线程保持的情况下,才获取该锁。如果该锁没有被另一个线程保持,并且立即返回 true
值,则将锁的保持计数设置为 1。
remove时也需要尝试加锁,如果没有获取到锁则尝试获取,达到最大次数后进入自旋等待:
scanAndLock(key, hash);
先定位segment,然后找到key对应的节点链的链头,不再重新创建一条新的链,而是只在当起缓存中将链中遍历找到要删除的节点移除,而另一个遍历线程的缓存中继续存在原来的链。当移除的是链头是更新数组项的值,否则更新找到节点的前一个节点的next指针。这也是HashEntry中next指针没有设置成final的原因(final保证读取的时候不会死锁,也不用加锁)。
put
scanAndLockForPut(K key, int hash, V value)持续查找key对应的节点链中是已存在该机节点,如果没有找到已存在的节点,则预创建一个新节点,并且尝试n次,直到尝试次数操作限制,才真正进入等待状态,计所谓的自旋等待。对最大尝试次数,目前的实现单核次数为1,多核为64。
ConcurrentHashMap中的get、containsKey、put、putIfAbsent、replace、Remove、clear操作
Segment中对HashEntry数组以及数组项中的节点链遍历操作是线程安全的,因而get、containsKey操作只需要找到相应的Segment实例,通过Segment实例找到节点链,然后遍历节点链即可。
对put、putIfAbsent、replace、remove、clear操作,它们在Segment中都实现,只需要通过hash值找到Segment实例,然后调用相应方法即可。
ConcurrentHashMap中的size、containsValue、contains、isEmpty操作
因为这些操作需要全局扫瞄整个Map,正常情况下需要先获得所有Segment实例的锁,然后做相应的查找、计算得到结果,再解锁,返回值。然而为了尽可能的减少锁对性能的影响,并没有直接加锁,而是先尝试的遍历查找、计算2遍,如果两遍遍历过程中整个Map没有发生修改(即两次所有Segment实例中modCount值的和一致),则可以认为整个查找、计算过程中Map没有发生改变,我们计算的结果是正确的,否则,在顺序的在所有Segment实例加锁,计算,解锁,然后返回。
rehash
rehash的逻辑比较简单,它创建一个原来两倍容量的数组,然后遍历原来数组以及数组项中的每条链,对每个节点重新计算它的数组索引,然后创建一个新的节点插入到新数组中,这里需要重新创建一个新节点而不是修改原有节点的next指针时为了在做rehash时可以保证其他线程的get遍历操作可以正常在原有的链上正常工作,有点copy-on-write思想。然而Doug Lea继续优化了这段逻辑,为了减少重新创建新节点的开销,这里做了两点优化:1,对只有一个节点的链,直接将该节点赋值给新数组对应项即可(之所以能这么做是因为Segment中数组的长度也永远是2的倍数,而将数组长度扩大成原来的2倍,那么新节点在新数组中的位置只能是相同的索引号或者原来索引号加原来数组的长度,因而可以保证每条链在rehash是不会相互干扰);2,对有多个节点的链,先遍历该链找到第一个后面所有节点的索引值不变的节点p,然后只重新创建节点p以前的节点即可,此时新节点链和旧节点链同时存在,在p节点相遇,这样即使有其他线程在当前链做遍历也能正常工作.
(需要继续深入研究和理解)