ConcurrentHashMap
是做Java开发必须要掌握的类的用法之一,它弥补了HashTable
在并发环境下的性能的不足。引用分离锁的概念,极大地增强了并发性能。
ConcurrentHashMap 存储结构图
ConcurrentHashMap
相比于HashMap
新增了一个segments数组,每一个数组分别对应一把锁,ConcurrentHashMap
默认情况下这个数组长度为16,即在理想状态下ConcurrentHashMap
可以支持16个线程无锁安全访问数据。
举个例子:当有一个线程正在修改segments[0]中的内容时,同时另一个线程要查询的value刚好在segment[2]中,那么segment[2]就不会加锁。
ConcurrentHashMap中的HashEntry内部类
static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
...
}
HashMap
是以数组加链表的形式存储数据的,ConcurrentHashMap
当然也不例外,上面这个是ConcurrentHashMap
中用作链表存储的内部类。它除了value 其余变量全部设为final型。
ConcurrentHashMap 中的数据交互
其实分析ConcurrentHashMap
最重要的就是分析它是怎么做数据交互的,下面将会依次分析ConcurrentHashMap
中数据是怎么存储和删除
1.如何put
由于HashEntry
中 next变量被声明为了final,那么要增加一个新的Entry那就只能从头部添加,如下图:
它会将新new出来的HashEntry
指向原本数组指向的表头元素,然后将数组指向这个类。
2.如何delete
在ConcurrentHashMap
中删除一个元素可不像传统链表删除一个元素时只需要修改下指针位置那么简单。因为HashEntry
中next为final类型,所以位于要删除的元素之前的元素必须要重新new一次,再依次指定next。如下图:
ConcurrentHashMap 中的数据脏读
参照图1-3,我们假设一种情况,当线程1想要读取e3的数据,此时读取到了e1位置;而与此同时线程2正在执行删除e3操作,那么就有可能线程2执行完删除操作后,线程1仍然能读取出e3的数据。这就是ConcurrentHashMap中的数据脏读,所以ConcurrentHashMap不适合存储敏感类型的数据。
必须应该了解的Clear方法
首先看一下ConcurrentHashMap中的clear方法的源码
public void clear() {
final Segment<K,V>[] segments = this.segments;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> s = segmentAt(segments, j);
if (s != null)
s.clear();
}
}
它在for循环中逐个去清除Segment下的数据,如果不为空,则调用segment的clear方法。
基于这个方法,有时候就会出现一种比较奇怪的现象:某一个线程刚存进去的key-value,马上就没有了,并且存进去之后并没有任何线程调用clear等类似方法。
结合clear的代码思考一下不难理解,这种情况是在put一个value之前恰好有一个线程调用了clear方法,在这个方法逐个清理segment时,put的这个value刚好保存在了将要被clear但还没有被clear的segment上,所以这个value就会立马被清理掉。
总结
ConcurrentHashMap
是一个并发情况下线程安全的HashMap,但是它的安全并不是那么绝对,在一些特殊情况下仍然有可能会发生数据脏读,数据丢失等情况。所以在我们使用ConcurrentHashMap
时一定要了解它的一些特性,这样我们才能在使用它的时候不会产生一些意料之外的问题。