ConcurrentHashMap是线程安全的容器。
ConcurrentHashMap是基于乐观锁实现线程安全的。
ConcurrentHashMap是基于CAS算法和volatile实现线程安全的。
java1.8版本对ConcurrentHashMap进行了较大的优化,我们先看1.8之前的ConcurrentHashMap,然后文章的后半部分再来研究1.8版本的ConcurrentHashMap。
我们都知道,ConcurrentHashMap使用了分段map的思想,也就是segment,segment是如何提高并发性的?又是如何保证线程安全的?
1,分段锁技术
什么是segment呢,ConcurrentHashMap会把数据分段存储,然后每段数据分别加锁。看源码可以发现,其实就是遍历node的时候,加上synchronized关键字,而Hashtable是在整个put方法上加上synchronized关键字。
数据分段加锁以后,由于一个线程只锁定了某一段数据,所以其他线程还可以访问其他数据。这样就达到了提高并发能力的要求。
为了理解更加深入,我们考虑一下下面这种场景,如果两个线程要获取map中的同一个key,是否能同时访问呢?也就是说,这种情况下能并发执行吗?segment到底是如何分段的?如果我的map中只有2个兼职对,难道也要分成16个segment吗?
从代码的角度来说,Segment是ConcurrentHashMap的一个内部类,继承了重入锁ReentrantLock,实现了序列化接口。
2,put方法
先通过segmentFor(hash)定位到数据所在的分段,然后调用分段的put方法。put方法的实现使用的是加锁操作,这里加的是重入锁,也就是说put方法没有使用CAS乐观锁,而只是使用了segment分段处理。java1.8开始使用CAS乐观锁代替RentrantLock
3,get方法
4,remove方法
先根据key定位segment,然后调用segment的containKey(key, hash, null)方法。
remove方法的实现使用的是加锁操作,这里加的是重入锁,也就是说remove方法没有使用CAS乐观锁,而只是使用了segment分段处理。
5,size方法
size方法统计的是整个map的大小,所以必须要统计所有的segment大小,然后求和。Segment里面有一个count变量,这个变量是volatile类型的,也就是内存可见的,但是在多线程环境下,不是线程安全的。因为你拿到count以后还要求和,虽然拿到的那一刻,是线程安全的,但是还要求和,求和操作的过程中如果count改变了,最后统计的结果就不准确了。
为了解决这个问题,ConcurrentHashMap提供了这样一种思路:累加过程中,累加结果发生改变的概率很小,所以
ConcurrentHashMap的做法是先在segment不加锁的情况下,进行两次累加操作,如果累加结果一致,OK,直接把累加结果当做size返回。如果累加结果不一致,则把所有segment的put方法、remove方法、clear方法锁住,然后在获取count进行累加操作。
6,containKey方法
先根据key定位segment,然后调用segment的containKey(key, hash)方法。
这个方法不存在线程安全的问题。源代码如下:
二,java1.8 ConcurrentHashMap源码解析
1,put方法
put方法
1,如果Node数组为空,则初始化,也就是说map中Node的初始化是延迟的,知道put操作的时候,才真正初始化。
2,spread(key.hashcode()),二次hash,减少hash碰撞。
3,如果桶的深度大于1,需要追加到链表或者红黑树时,使用synchronized进行同步。ConcurrentHashMap主要是这个操作使用了synchronized进行同步,其他都是使用RentrantLock。
三,Java1.8对ConcurrentHashMap的优化
1,使用CAS乐观锁和volatile代替RentrantLock
2,spread二次哈希进行segment分段。
3,stream提高并行处理能力。