线程安全-5 ConcurrentHashMap

一.HashMap为什么是线程不安全的?

1.非同步操作:HashMap的操作不是线程同步的,多线程时会出现可见性问题。

2.非原子操作:HashMap的操作是非原子性的,多线程时会导致原子性问题。

(1)put()操作

        a.jdk1.7的HashMap的put()操作,采用了链表头插法。多线程情况下,在扩容时可能会出现链表死循环的问题。

        b.jdk1.8的HashMap的put()操作,虽然改用链表尾插法避免了死循环的问题,但仍会因为非原子操作而产生问题:

                问题1:发生哈希冲突本该以链表形式存储冲突的数据,但在多线程情况下,可能会由于线程上下文切换而导致忽视了哈希冲突,出现值覆盖的情况。

                问题2:put()操作在添加完元素后,还会对size变量进行修改,由于该操作不是原子性的,同样会引发多线程对size的修改产生冲突的情况。

(2)resize()操作:resize()操作涉及到了对节点的复制和重定位,若非原子操作,则在扩容期间可能会有其他线程对数组进行并发修改,产生问题。

3.HashTable是线程安全的,但其线程安全的实现是通过在方法上加synchronized锁,性能低下。

二.说一下jdk1.7和jdk1.8的ConcurrentHashMap的区别

1.数据结构不同

(1)jdk1.7的ConcurrentHashMap使用segment数组和HashEntry数组实现

        a.segment[]:大数组,将HashMap的table数组切分成若干段小数组HashEntry,segment数组的每个元素就对应一段HashEntry。

        b.HashEntry[]:小数组,采用数组+链表的形式存储数据节点。

(2)jdk1.8的ConcurrentHashMap使用与HashMap一样的结构:数组+链表/红黑树

(3)jdk1.7的ConcurrentHashMap访问数据需要进行两次定位,第一次定segment元素位置,第二次定HashEntry元素未知;而jdk1.8只需要进行一次定位。

2.加锁方式不同

(1)jdk1.7的ConcurrentHashMap的加锁对象是segment数组的一个元素,即一次锁一整段HashEntry,用的是可重入锁ReentrantLock。

(2)jdk1.8的ConcurrentHashMap的加锁对象是当前槽位的头节点,一次只锁一个槽位,用的是synchronized锁。

(3)jdk1.8用synchronized锁一个槽位,相比于jdk1.7用ReentrantLock锁一整段HashEntry,分段粒度更细,性能更好。

三.jdk1.8的ConcurrentHashMap是如何保证线程安全的?

1.table数组用volatile关键字修饰,保证了可见性和有序性。

2.put()操作

在最外层加一个乐观锁,或者说是一个for死循环,通过这个循环来完成多层条件逻辑的执行:

(1)判断table数组是否为null或长度为0,若满足则初始化数组,并进入下一次循环

(2)通过索引公式定位到目标槽位,如果目标槽位为空,则通过CAS操作插入数据

        a.若CAS操作成功,则退出循环

        b.若CAS操作失败,说明有其他线程修改,进入下一次循环

(3)判断当前数组是否在扩容,若满足则帮助数组扩容。无论是否在扩容,都会进入下一次循环

(4)无论是CAS操作失败还是目标槽位已有数据,都会走到当前判断逻辑,即说明当前槽位已经被占有。因此接下来执行判断,若满足以下条件:不需要覆盖原值、槽位节点的hash与新节点的hash相同、槽位节点的key与新节点的key相同,则直接将当前槽位节点的值返回;否则进入下一次循环

(5)为当前槽位的头节点申请synchronized锁,申请到锁后,执行HashMap的添加逻辑。

退出乐观锁后会进行判断:是否要扩容或链表转红黑树,若满足则执行对应逻辑。

上述五个条件,乐观锁的每次循环完成一个条件的处理,每多执行一次循环,则往下推进一个条件判断。

put()操作基于CAS操作和synchronized锁保证线程安全且只有在发生哈希冲突时才需要加synchronized锁,降低了锁的粒度,提高了效率。

3.get()操作:由于table数组已经被volatile关键字修饰,因此get()操作无需加锁。

四.对于ConcurrentHashMap,在执行put()操作时,为什么key和value不能为null?

为了防止二义性。

1.对于HashMap:用于单线程环境

(1)在put()时允许key和value为null。key为null则直接映射到0号槽位,value为null则直接存一个null值。

(2)如果get()方法得到一个null值,为了判断是因为不存在这个key而返回一个null还是本来就存了一个null,可以调用containsKey()判断是否存在key,从而解决二义性问题。

2.对于ConcurrentHashMap:用于多线程环境

(1)在put()时如果key或value为null,则直接抛出NullPointException。

(2)如果get()方法得到一个null值,是无法通过containsKey()来解决二义性问题的,因为在此期间可能会有其他线程对该槽位进行并发修改,从而导致判断结果与实际结果出现差异。

  • 17
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值