优秀博文:http://www.cnblogs.com/yydcdut/p/3959815.html
线程不安全的hashmap
hashmap是线程不安全,在多线程情况下,使用hashmap的put操作会引起死循环。所以在并发情况下不能使用HashMap
线程安全的hashtable
但是在多线程情况下效率极低。因为synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,所有访问hashtable的线程都对同一个锁进行竞争。
线程安全且效率高的ConCurrentHashMap
采用了锁分离技术,每个段都有一个锁,所以可以在一定程度上保证写并发。读操作不需要加锁,所以可以实现完全读并发。
内部结构
ConCurrentHashMap内部使用段(Segment),每个段都是一个小的hashtable(其中包含多个HashEntry[])。每个段都有自己独立的锁,只要多个修改操作发生在不同的段上,就可以实现并发。(读操作可以完全实现并发,因为读不加锁)
但是!
有些方法需要跨段,如size() containsValue()。这些方法可能需要锁定整个表。因此,需要按顺序锁定所有段,操作完毕后,又需要按顺序释放所有锁。(如果这里加锁以及释放锁没有按顺序,可能会出现死锁。在ConCurrentHashMap内部,段数组是声明为final的,但是这并不保证段数组成员也是final的)
源码分析
static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
}
如上所示,除value之外,其他成员都是final的,这意味者只能从hash表的头部添加或删除节点(因为需要需改next引用值,所有节点的修改只能从头部开始,put操作也一律添加到头部)
为了确保读操作能够读取到最新的值,将value设置为volatile,这就避免了加锁。
但是对于remove操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制一遍,最后一个节点指向要删除结点的下一个结点。
get方法:
public V get(Object key) {
int hash = hash(key); // throws NullPointerException if key null
return segmentFor(hash).get(key, hash);
}
可见,get方法并没有加锁,交给segment去寻找。
V get(Object key, int hash) {
if (count != 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);
}
e = e.next;
}
}
return null;
}
它也没有用锁实现数据同步。归根结底就是因为value设置为volatile
get操作第一步访问count变量(该变量声明为volatile)。所有的结构修改操作都会在最后一步写count变量,(这就保证了get操作能够获取到最新的值)