ConcurrentHashMap是线程安全的,在JDK1.7中是使用ReentrantLock来保证线程安全,在JDK1.8中,它是使用synchronized进行加锁保证线程安全的;
但是concurrentHashMap中的get操作是没有加锁的,那么它是如何来保证线程安全的呢?
concurrentHashMap中的get源码:
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
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)
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;
}
通过观察源码可知,concurrentHashMap中的get操作没有加锁,它是如何保证线程安全,保证读到的数据不是脏数据呢?此时,我们就要先说一说volatile这个关键字了:
在说volatile之前,让我们先来了解下java内存模型:
Java内存模型规定了所有的变量都存储在主内存中,每条线程都有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在各自的工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,而工作内存中的变量何时重写入主内存,又不是我们能确定的;
普通的共享变量是不能保证可见性的,因为普通变量在某一个线程的工作内存中被修改后,什么时候被写入主内存是不确定的,当其他线程去读取主内存变量时,此时主内存内的值可能是原来的旧值;
但是如果使用了volatile来修饰该变量,其会强制将修改的值立即写入主内存,而且为了保证各个处理器的工作内存是一致的,还会实现缓存一致性协议:当某个线程在写数据时,如果发现被操作的变量是共享变量,cpu则会通知其他线程该变量的缓存行是无效的,因此其他线程在读取该变量时会发现其变量无效而去主内存中加载数据;
而ConcurrentHashMap的get操作无锁就是因为其操作的Node和next都是由volatile来修饰的:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}