ConcurrentHashMap是如何保证线程安全的
- 定义和问题解决
- JDK 1.7实现原理
- JDK 1.8性能优化
- 总结
定义和问题解决
ConcurrentHashMap相当于HashMap的多线程版本。
它的功能本质上和HashMap没有什么区别,因为HashMap在并发操作的时候会出现各种问题,比如:
- 死循环问题
- 数据覆盖等问题
而这些问题只要使用ConcurrentHashMap就能得到完美的解决。
ConcurrentHashMap是如何保证线程安全的。
JDK 1.7实现原理
JDK 1.7中ConcurrentHashMap的底层结构基本延续了HashMap的基本设计。它采用的是数组加链表的形式。
和HashMap不同的是ConcurrentHashMap中的数组被封分为大数组和小数组。大数组是Segment,小数组是HashEntry。大数组Segment可以理解为是一个数据库,而这个数据库又有很多张表,这个表就是HashEntry。
而每个HashEntry中又有很多条数据,这些数据采用的是链表结构。
因为Segment本身是基于ReentrantLock重入锁的实现来加锁和释放锁的操作。这样的话就能够保证多线程同时访问ConcurrentHashMap的时候,同一时间只能有一个线程操作对应的节点。这样的话,保证了ConcurrentHashMap的线程安全。也就是说ConcurrentHashMap的线程安全是建立在Segment的加锁的基础上,所以我们称它为分段锁或者是分片锁。
JDK 1.8性能优化
在JDK1.7中,ConcurrentHashMap虽然是线程安全的,但是它的底层实现是采用数组加链表的形式。所以在数据比较多的情况下,因为要遍历整个链表,这样的话会降低它的访问性能。
所以JDK1.8以后,就采用了数组加链表加红黑树的方式进行的优化。其具体实现如图所示:
当我们的链表长度大于8的时候,并且数组长度大于64的时候,链表就会升级为红黑树的结构。
JDK1.8中,ConcurrentHashMap保留了Segment的定义,但是者仅仅是为了保证序列化的时候的兼容性,不再有任何结构上的用途了。那在JDK1.8中,ConcurrentHashMap的源码又是如何实现的呢?
它主要是通过CAS加volatile或者是synchronized的方法来实现的,保证线程安全,我们可以从源码片段中看到添加元素的时候,首先会判断容器是否为空,如果容器为空,就会使用volatile加CAS来初始化。如果容器不为空,就会根据存储的元素计算该位置是否为空。如果根据存储元素的计算结果为空,就会利用CAS来设计该节点;如果根据存储元素的计算结果不为空,就会使用synchronized加锁来进行实现,然后去遍历桶中的数据,并且替换或新增节点到桶中。最后判断是否有必要转为红黑树。这样就保证了并发访问的线程安全。
如果把上面的执行用一句话来归纳的话,就相当于ConcurrentHashMap通过对头节点加锁来保证线程安全,这样设计的好处是使得锁的粒度相比Segment来说更小了。发生hash冲突和加锁的频率也更低了,而在并发场景下操作性能也提高了。而且当数据量比较大的时候,查询性能也得到了进一步的提升。
总结
- JDK 1.7给Segment添加ReentrantLock锁来实现线程安全。
ConcurrentHashMap在JDK1.7中使用的是数组加链表结构,其中数组分为两大类,大数组是Segment,小数组是HashEntry。而加锁是通过Segment添加ReentrantLock重入锁来保证线程安全的。 - JDK 1.8通过CAS或者synchronized来实现线程安全。
ConcurrentHashMap在JDK1.8中使用的是数组加链表加红黑树的方式来实现。它是通过CAS或者synchronized来实现线程安全。并且也缩小了锁的粒度。查询性能也到了进一步的提升。
参考资料:【Java面试】京东二面,ConcurrentHashMap是如何保证线程安全?