ConcurrentHashMap是如何保证线程安全的

  • 1. 定义和作用
  • 2. JDK1.7中保证线程安全的原理
  • 2.1 数据结构
  • 2.2 基于分段锁实现
  • 2.3 ReentrantLock重入锁
  • 2.3.1 定义
  • 2.3.2 特点
  • 3. JDK1.8中性能优化原理
  • 3.1 数据结构上的变化
  • 3.2 源码实现
  • 3.3 总结
  • 4. 概括
  • JDK1.7
  • JDK1.8


1. 定义和作用

ConcurrentHashMap相当于HashMap的多线程版本

它的功能本质上和HashMap没有什么区别,但HashMap在并发操作的时候会出现各种问题,比如死循环、数据覆盖等问题。

而这些问题只要使用ConcurrentHashMap就能得到完美的解决。

2. JDK1.7中保证线程安全的原理

java currenthashmap put 不是线程安全 concurrenthashmap线程安全_数组

2.1 数据结构

JDK 1.7中ConcurrentHashMap的底层结构基本延续了HashMap的基本设计。它采用的是数组加链表的形式。

和HashMap不同的是ConcurrentHashMap中的数组被封分为大数组和小数组

大数组是Segment,小数组是HashEntry

大数组Segment可以理解为是一个数据库,而这个数据库又有很多张表,这个就是HashEntry。

每个HashEntry中又有很多条数据,这些数据采用的是链表结构

2.2 基于分段锁实现

因为Segment本身是基于ReentrantLock重入锁的实现来加锁和释放锁的操作。

这样的话就能够保证多线程同时访问ConcurrentHashMap的时候,同一时间只能有一个线程操作对应的节点,进而保证了ConcurrentHashMap的线程安全。

也就是说ConcurrentHashMap的线程安全是建立在Segment的加锁的基础上,所以我们称它为分段锁或者是分片锁

2.3 ReentrantLock重入锁

2.3.1 定义

ReentrantLock(重入锁)是Java中提供的一种同步机制,用于实现多线程之间的互斥访问。

它是一种独占锁,也就是说,在任何时候只能有一个线程持有该锁,并且其他线程必须等待锁的释放才能获取它。

2.3.2 特点

与synchronized关键字相比,ReentrantLock提供了很多的灵活性和功能。

它是可重入的,意味着同一个线程可以多次获得同一把锁,而不会导致死锁。当一个线程多次获取锁时,它必须相应地释放相同次数的锁,才能使其他等待的线程获得该锁。

ReentrantLock还提供了公平性选择,可以选择是否按照线程请求锁的顺序来获取锁。默认情况下,它是非公平的,允许通过竞争来提高吞吐量。而公平锁将按照线程的请求顺序来获取锁,保证了每个线程都有公平的机会获得锁,但可能会降低吞吐量。

使用ReentrantLock需要手动获取锁和释放锁

在使用ReentrantLock时,必须确保在获取锁后正确释放锁,否则可能会导致死锁或其他并发问题。因此,通常使用try-finally块来确保锁的释放操作在任何情况下都会执行。

总的来说,ReentrantLock是一种可重入的、灵活而强大的同步机制,可以用于实现更复杂的多线程同步需求。

3. JDK1.8中性能优化原理

3.1 数据结构上的变化

在JDK1.7中,ConcurrentHashMap虽然是线程安全的,但是它的底层实现是采用数组+链表的形式。所以在数据比较多的情况下,因为要遍历整个链表,这样的话会降低它的访问性能。

JDK1.8以后,就采用了数组+链表+红黑树的方式进行的优化。

java currenthashmap put 不是线程安全 concurrenthashmap线程安全_链表_02


当我们的链表长度大于8的时候,并且数组长度大于64的时候,链表就会升级为红黑树的结构。

JDK1.8中,ConcurrentHashMap保留了Segment的定义,但是仅仅是为了保证序列化的时候的兼容性,不再有任何结构上的用途了。

3.2 源码实现

那在JDK1.8中,ConcurrentHashMap的源码又是如何实现的呢?

它主要是通过CAS+volatile或者是synchronized的方法来实现的,保证线程安全。

首先会判断容器是否为空

  1. 如果容器为空,就会使用volatile+CAS来初始化
  2. 如果容器不为空,就会根据存储的元素计算该位置是否为空
    1)如果根据存储元素的计算结果为空,就会利用CAS来设计该节点。
    2)如果根据存储元素的计算结果不为空,就会使用synchronized加锁来进行实现。然后去遍历桶中的数据,并且替换或新增节点到桶中。最后判断是否有必要转为红黑树。这样就保证了并发访问的线程安全。

java currenthashmap put 不是线程安全 concurrenthashmap线程安全_数组_03

3.3 总结

如果把上面的执行用一句话来归纳的话,就相当于ConcurrentHashMap通过对头节点加锁来保证线程安全,这样设计的好处是使得锁的粒度相比Segment来说更小了,发生hash冲突和加锁的频率也更低了,而在并发场景下操作性能也提高了。而且当数据量比较大的时候,查询性能也得到了进一步的提升。

4. 概括

JDK1.7

使用数组+链表,其中数组分为两大类,大数组是Segment,小数组是HashEntry。而加锁是通过Segment添加ReentrantLock重入锁来保证线程安全的。

JDK1.8

使用数组+链表+红黑树,它是通过CAS或者synchronized来实现线程安全。并且也缩小了锁的粒度,查询性能也到了进一步的提升。