并发环境下为什么使用ConcurrentHashMap
1. HashMap在高并发的环境下,执行put操作会导致HashMap的Entry链表形成环形数据结构,从而导致Entry的next节点始终不为空,因此产生死循环获取Entry
2. HashTable虽然是线程安全的,但是效率低下,当一个线程访问HashTable的同步方法时,其他线程如果也访问HashTable的同步方法,那么会进入阻塞或者轮训状态。
3. 在jdk1.6中ConcurrentHashMap使用锁分段技术提高并发访问效率。首先将数据分成一段一段地存储,然后给每一段数据配一个锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问。然而在jdk1.8中的实现已经抛弃了Segment分段锁机制,利用CAS+Synchronized来保证并发更新的安全,底层依然采用数组+链表+红黑树的存储结构。
JDK1.6分析
ConcurrentHashMap采用 分段锁的机制,实现并发的更新操作,底层由Segment数组和HashEntry数组组成。Segment继承ReentrantLock用来充当锁的角色,每个 Segment 对象守护每个散列映射表的若干个桶。HashEntry 用来封装映射表的键 / 值对;每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组,下面我们通过一个图来演示一下 ConcurrentHashMap 的结构:
JDK1.8分析
改进一:取消segments字段,直接采用transient volatile HashEntry<K,V> table
保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。
改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。
ConcurrentHashMap初始化
首先会创建segment数组,长度为默认(16)或传入的并发值的大于等于的2的次方数,不可扩容
初始化(s0)segment对象并保存,并初始化他的属性,创建一个entry数组,entry数组初始化长度为 (容器长度 / segment数组长度)大于等于2的次方,在下一次进行添加数据的时候,会直接取这个s0对象
这里初始化了他的segment[0]
ConcurrentHashMap如何存取数据的
1.7:
put():
1.先计算出key值的hash值,然后通过hash值(对segment[]进行取余)找到数组中对应的segment对象
2.尝试获取锁,失败则自旋保证成功。
3.获取锁,然后通过计算出的hash值(对hashentry[]进行取余)找出对应的entry对象
遍历链表中,查找有没有相同key值对象
有: 旧值覆盖新值
没有: 添加到链表中(1.7/头部 | 1.8/尾部)
get():
1.通过key值的hash值定位到对应segment对象,再通过hash值定位到具体的entry对象
2.遍历链表,通过equals取出数据
3.由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的
保证了内存可见性,所以每次获取时都是最新值
4.整个过程不需要加锁
1.8:
put():
1.根据 key 计算出 hashcode
2.判断是否需要进行初始化。
3.即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据
4.利用 CAS 尝试写入,失败则自旋保证成功。如果都不满足,则利用 synchronized 锁写入数据
5.如果数量大于 树形化阈值 则要转换为红黑树。
get():
1.根据计算出来的 hashcode 寻找segment,如果就在桶上那么直接返回值。
2.如果是红黑树那就按照树的方式获取值。
3.就不满足那就按照链表的方式遍历获取值。
实现有何不同?为什么这么做?
线程同上.
数据结构:
1.7: 数组 + 链表
1.8:
1.7的底层还是链表,在查询数据的时候需要遍历,导致效率很低
1.8和HashMap比较像,也引入了红黑树,把值和next采用了volatile去修饰,保证了可见性
volatile的特性是啥?
volatile是Java提供的一种轻量级的同步机制:
1.保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值
这新值对其他线程来说是立即可见的。(实现可见性)
2.禁止进行指令重排序。(实现有序性)
3.volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
ConcurrentHashMap 线程安全怎么做的?
1.7:
采用了分段锁的机制,当一个线程占用锁时,不会影响到其他的Segment对象
1.8:
抛弃了原来的分段锁,采用了 CAS 和 synchronized 来保证并发的安全
CAS是啥?ABA是啥?场景有哪些,怎么解决?
CAS:
是乐观锁的一种实现方式,是一种轻量级锁
线程在读取数据时不进行加锁,在准备写回数据时,比较原值是否修改,
若未被其他线程修改则写回,若已被修改,则重新执行读取流程
ABA:
当线程对值的修改过程中,另一个线程也对这个值进行了修改,并把它改为了原来的值
此时,这个值已经不是原的值了
解决方案:
版本号 / 时间戳
synchronized锁升级策略
1.先使用 偏向锁 优先同一线程然后再次获取锁
2.如果失败,就升级为 CAS 轻量级锁,失败就会短暂自旋,防止线程被系统挂起。
3.最后如果以上都失败就升级为重量级锁。