ConcurrentHashMap是如何保证线程安全的

ConcurrentHashMap是Java中线程安全的哈希映射表,通过分段锁在JDK1.7中实现并发控制。每个Segment是一个独立的Hash表并持有ReentrantLock,保证了并发访问的安全。JDK1.8则使用了CAS和synchronized优化,且在数据量大时链表会转换为红黑树,提高查询性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 定义和作用

ConcurrentHashMap相当于HashMap的多线程版本

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

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

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

在这里插入图片描述

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以后,就采用了数组+链表+红黑树的方式进行的优化。
在这里插入图片描述
当我们的链表长度大于8的时候,并且数组长度大于64的时候,链表就会升级为红黑树的结构。

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

3.2 源码实现

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

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

首先会判断容器是否为空

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

在这里插入图片描述

3.3 总结

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

4. 概括

JDK1.7

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

JDK1.8

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

参考资料【Java面试】京东二面,ConcurrentHashMap是如何保证线程安全?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值