ConcurrentHashMap它是HashMap的线程安全版本,内部使用的是(数组+链表+红黑树)这一种结构来存储元素。它相对于同样是线程安全的HashTable,它的效率都比HashTable有很大的提高。

底层数据结构

在JDK1.7的ConcurrentHashMap底层是用分段式的数组+链表来实现的。

JDK1.8和HashMap1.8的结构一样是数组+链表/红黑树。

多线程Map

那么在Java当中,HashMap是非线程安全我们是知道的,那么如果想在多线程安全下操作map有什么方法呢?

  1. 可以使用Hashtable
  2. 可以使用Collections.synchronizedMap
  3. 可以使用ConcurrentHashMap

ConcurrentHashMap并发策略

jdk1.7之前,ConcurrentHashMap采用的是锁分段策略来优化性能,这相当来说就是把整一个数组拆分了,每次操作的话只需要把小数组来锁住即可。在不同的segment之间是相互不影响的,这样就提高了性能。

老王带你从头到尾理解ConcurrentHashMap_链表

在JDK1.8就开始把整一个策略来进行重构了,这时候它锁的并不是segment了,而锁的是节点,这样就可以让锁的粒度降低,这个并发的效率也得以提升。

老王带你从头到尾理解ConcurrentHashMap_数组_02

ConcurrentHashMap添加数据

ConcurrentHashMap在添加数据的时候,它是采用了CAS+synchronize的结合这一种策略,它会先判断节点是不是为null的,如果是那么就添加节点。如果添加是失败的话,这说明了发生了冲突,然后会对节点进行上锁并且插入数据。在并发比较低的时候就不用加锁,因为会损耗性能。同时的话CAS只尝试一次,这样的操作也不会造成线程进行长时间的等待耗费性能。

步骤:

  1. 判断数组是否初始化
  2. 插入节点为null,就使用CAS插入数据
  3. 节点不为null,就判断hash值是不是-1,-1就说明在扩容
  4. 最后会进行上锁插入数组

老王带你从头到尾理解ConcurrentHashMap_加锁_03

/**
* Maps the specified key to the specified value in this table.
* Neither the key nor the value can be null.
*
* <p> The value can be retrieved by calling the <tt>get</tt> method
* with a key that is equal to the original key.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>
* @throws NullPointerException if the specified key or value is null
*/
public V put(K key, V value) {
if (value == null)
throw new NullPointerException();
int hash = hash(key.hashCode());
return segmentFor(hash).put(key, hash, value, false);
}

可以看到ConcurrentHashMap它是跟hashmap不一样的,它不允许key和value都为null。当向ConcurrentHashMap进行put操作的时候,它会先获得key的哈希值并且再次哈希,最后根据hash值定位到所插入的段当中,看源码。

V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock(); // 上锁
try {
int c = count;
if (c++ > threshold) // ensure capacity
rehash();
HashEntry<K,V>[] tab = table; // table是Volatile的
int index = hash & (tab.length - 1); // 定位到段中特定的桶
HashEntry<K,V> first = tab[index]; // first指向桶中链表的表头
HashEntry<K,V> e = first;

// 检查该桶中是否存在相同key的结点
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;

V oldValue;
if (e != null) { // 该桶中存在相同key的结点
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value; // 更新value值
}else { // 该桶中不存在相同key的结点
oldValue = null;
++modCount; // 结构性修改,modCount加1
tab[index] = new HashEntry<K,V>(key, hash, first, value); // 创建HashEntry并将其链到表头
count = c; //write-volatile,count值的更新一定要放在最后一步(volatile变量)
}
return oldValue; // 返回旧值(该桶中不存在相同key的结点,则返回null)
} finally {
unlock(); // 在finally子句中解锁
}
}

相对于HashTable和HashMap只能够有一个线程来执行读或者是写的操作,这使得ConcurrentHashMap在并发的性能上有了质的提高。

ConcurrentHashMap的get操作

get操作与put操作很相似,当从ConcurrentHashMap查询一个指定Key的键值对的时候,会定位存在的段,然后查询请求给这个段来进行处理。

/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code key.equals(k)},
* then this method returns {@code v}; otherwise it returns
* {@code null}. (There can be at most one such mapping.)
*
* @throws NullPointerException if the specified key is null
*/
public V get(Object key) {
int hash = hash(key.hashCode());
return segmentFor(hash).get(key, hash);
}
//--------------------------------------------------------------------------------------
V get(Object key, int hash) {
if (count != 0) { // read-volatile,首先读 count 变量
HashEntry<K,V> e = getFirst(hash); // 获取桶中链表头结点
while (e != null) {
if (e.hash == hash && key.equals(e.key)) { // 查找链中是否存在指定Key的键值对
V v = e.value;
if (v != null) // 如果读到value域不为 null,直接返回
return v;
// 如果读到value域为null,说明发生了重排序,加锁后重新读取
return readValueUnderLock(e); // recheck
}
e = e.next;
}
}
return null; // 如果不存在,直接返回null
}

所以在ConcurrentHash来进行存取的时候,它首先是会定位到具体的段,然后再对整一个ConcurrentHashMap来进行存取。所以不论是读还是写ConcurrentHash具有很高的性能,在进行操作的时候是不需要加锁的,在写操作通过锁分段技术只是对所操作的段加锁,其他的客户端访问是不影响的。

ConcurrentHashMap 读操作不需要加锁的奥秘

用HashEntery对象的不变性来降低读操作对加锁的需求;

用Volatile变量协调读写线程间的内存可见性;

若读时发生指令重排序现象,则加锁重读

老王带你从头到尾理解ConcurrentHashMap_数组_04老王带你从头到尾理解ConcurrentHashMap_链表_05