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中保证线程安全的原理
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的方法来实现的,保证线程安全。
首先会判断容器是否为空:
- 如果容器为空,就会使用volatile+CAS来初始化。
- 如果容器不为空,就会根据存储的元素计算该位置是否为空。
1)如果根据存储元素的计算结果为空,就会利用CAS来设计该节点。
2)如果根据存储元素的计算结果不为空,就会使用synchronized加锁来进行实现。然后去遍历桶中的数据,并且替换或新增节点到桶中。最后判断是否有必要转为红黑树。这样就保证了并发访问的线程安全。
3.3 总结
如果把上面的执行用一句话来归纳的话,就相当于ConcurrentHashMap通过对头节点加锁来保证线程安全,这样设计的好处是使得锁的粒度相比Segment来说更小了,发生hash冲突和加锁的频率也更低了,而在并发场景下操作性能也提高了。而且当数据量比较大的时候,查询性能也得到了进一步的提升。
4. 概括
JDK1.7
使用数组+链表,其中数组分为两大类,大数组是Segment,小数组是HashEntry。而加锁是通过Segment添加ReentrantLock重入锁来保证线程安全的。
JDK1.8
使用数组+链表+红黑树,它是通过CAS或者synchronized来实现线程安全。并且也缩小了锁的粒度,查询性能也到了进一步的提升。