【Java并发】-- ConcurrentHashMap如何实现高效地线程安全(jdk1.8)

在这里插入图片描述

1.传统集合框架并发编程中Map存在的问题?

  • HashMap死循环,造成CPU100%负载
    HashMap进行存储时,如果size超过(当前最大容量*负载因子)时候会发生resize,而resize中又调用了又调用了transfer()方法,而这个方法实现的机制就是将每个链表转化到新链表,并且链表中的位置发生反转,而这在多线程情况下是很容易造成链表回路,从而发生死循环;
  • 元素丢失问题,多线程put操作,hash碰撞时候两个线程得到同样的bucketIndex可能会导致覆盖的情况,有一个元素会丢失;
  • 还有其他的,路过的评论区补充一下……

2.早期改进策略

  1. HashTable
    HashTable相比HashMap是线程安全的,因为HashTable所有的方法都是加了synchronized的,锁的是整个hashMap,也就是我们说的锁的粒度比较大,由于最基本的put,set操作都加了互斥锁,造成的结果就是同一时间点只能由一个线程put或只能get,并发操作时所有的put,get操作都必须等一个线程完了之后再操作,线程安全得到了保证,但大大降低了并发效率,在非高度的并发的场景可取,高度并发时往往不可取, 。
  2. jdk1.8以前的ConcurrentHashMap
    ConcurrentHashMap在jdk1.7及以前采用的是锁分段机制来保证HashMap的线程安全,锁分段也就是将HashMap内部分段,每段是一个segment, 对每个segment加锁( 可以理解为ConcurrentHashMap是一个segment数组 ),每个段里面包含多个HashEntry,和原HashMap类似,hash相同的entry也是以链表形式存放,这样锁的粒度相比HashTable就小了很多,值得注意的是,1.7的ConcurrentHashMap是通过继承ReentrantLock 来进行加锁的,不同于之前HashTable使用synchronize的加锁形式;通过锁住每个segment来保证每个segment内的操作的线程安全性,也就避免了HashTable的整体同步,一定程度上提升了性能;
    另外在构造的时候, Segment的数量由所谓的concurrentcyLevel决定, 默认是16; 和HashMap的初始容量一致, 也可以在相应构造函数直接指定。 同样是2的幂数值, 如果输入是类似15这种非幂值, 会被自动调整到16之类2的幂数值。所以,默认情况下此时的ConcurrentHashMap支持16个线程并发操作
    在这里插入图片描述
  3. 除了以上两种方法意外,Collections本身也提供了一种安全机制,就是通过Map<K,V> synchronizedMap(Map<K,V> m)方法将其包装为一个线程安全的map,我们看一下它的put源码实现就清除了:
public V put(K key, V value) {
   
    synchronized (mutex) {
   return m.put(key, value);}
}

以上简单的说了早期如何保证HashMap的线程安全,下面详细分析一下jdk1.8如何保证线程安全

3.ConcurrentHashMap采取了哪些方法来提高并发表现(jdk1.8)?

相比1.7做了两个改进:
1.取消了锁分段的设计,直接使用Node 数组来保存数据,并且用Node数组来保存数据,并且采用Node数组元素作为锁来实现对每一行数据加锁来进一步减少并发冲突的概率。
2.引入了红黑树的设计,在原来的数组+链表的基础上新增了红黑树的设计,当链表的长度超过8的时候就将链表转为红黑树,此时查询的复杂度也降低到了O(logN), 提升了查询的性能。
3.这一点不知道算不算是改进,但是和1.7确实是不一样的,为了解决线程安全问题,这一版的ConcurrentHashMap采用了synchronzied和CAS的方式,至于为什么选用了synchronzied我猜是因为1.8的synchronzied也做了很多的优化,包括偏向锁到轻量级所到重量级锁膨胀,因此改进后的synchronzied相较于ReentrantLock的性能在某些情况下并不差或许会更优,所以这里才选择了synchronzied来加锁,cas无锁操作的特性我就不多说了,比较容易理解。
稍后我们分析put源码的时候会看到这部分变化的具体实现。
另外,关于1.8版本的synchronzied优化可以查看本系列中博客中的:
【Java并发】-- synchronized原理 (偏向锁,轻量级锁,重量级锁膨胀过程)
结构图:
这个和jdk1.8的hashmap结构一致,但增加了线程安全的实现,所以结构简单,但实现会复杂一些;
在这里插入图片描述

4.ConcurrentHashMap实现分析

4.1 ConcurrentHashMap中关键的属性

table:

 //装载Node的数组,作为ConcurrentHashMap的数据容器,采用懒加载的方式,
 //直到第一次插入数据的时候才会进行初始化操作,数组的大小总是为2的幂次方。
  volatile Node<K,V>[] table:

nextTable

//扩容时使用,平时为null,只有在扩容的时候才为非null,
volatile Node<K,V>[] nextTable;

sizeCtl (不同场景有不同意义,肥肠重要!!!)

// 该属性用来控制table数组的大小,根据是否初始化和是否正在扩容有几种情况:
-------------------------
// 当值为负数时,-1这时表示数组有一个线程正在初始化,-n表示有n-1个线程正在进行扩容操作
// 注意:(扩容时可以多线程协作,但初始化只能有一个线程来完成)
-------------------------
// 当值为正数时:表示当前数组的临界值,也就是数组程度*负载因子得到的临界值,到达这个值就会进行扩容操作
// 当值为0时,是数组的默认初始值,此时还未被初始化。
volatile int sizeCtl;

sun.misc.Unsafe U

在ConcurrentHashMap的实现中也可以看到大量的cas操作,也就是U.compareAndSwapXXX类型的方法,调用这些方法去修改ConcurrentHashMap属性的时候就是利用了cas无锁算法来保证线程安全性,这是乐观锁的完美运用,cas是通过sun.misc.Unsafe类实现的,点到这个类之后我们发现所有的方法基本都是native的,也就是非java实现的接口; Unsafe类提供的方法是可以直接操作内存和线程的底层操作,该成员变量的获取是在静态代码块中:

 static {
   
    try {
   
        U = sun.misc.Unsafe.getUnsafe();
        .......
    } catch (Exception e) {
   
        throw new Error(e);
    }
}

4.2 ConcurrentHashMap中关键的CAS操作

tabAt

该方法获取对象中offset偏移地址对应的对象field的值, 简单来说也就是获取该方法用来获取table数组中索引为i的Node元素,但大家思考一下为什么不直接通过table[i]获取到第i个元素,而非要通过底层Unsafe类来进行table的操作呢?
因为我们虽然在table数组上加了volatile关键字来保证可见性,但是被volatile修饰的数组只针对数组的引用具有可先性,而不针对数组的元素,所以如果有其他个线程对这个数组的某个元素进行写操作的时候,不一定能保证可见性,当前线程也就不一定读到最新的值了。所以这里调用了Unsafe的getObjectVolatile方法保证每个元素都读到最新的值,同时也保证了性能。下面的casTabAt和setTabAt也是同理。

// 该方法用来获取table数组中索引为i的Node元素
 static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
   
     return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
 }

casTabAt

// 利用CAS操作设置table数组中索引为i的元素
 static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                     Node<K,V> c, Node<K,V> v) {
   
     return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
 }

setTabAt

// 该方法用来设置table数组中索引为i的元素
 static final <K,V> void setTabAt(Node<K,V>
  • 6
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值