从ConcurrentHashMap出发引发的一系列思考


前言

在做mit6.830时,关于数据库的全局目录catalog类持有一个线程安全的ConcurrentHashMap去保存数据库中每一张表。为了避免多个线程对数据库目录修改造成数据不一致的情况,是不能使用HashMap的,这是因为HashMap是线程不安全的。


一、HashMap

1.JDK1.7和JDK1.8中HashMap底层原理的不同

首先在JDK1.7中HashMap的无参构造会默认创建一个数组长度为16的Entry数组,而在JDK1.8中只有我们在首次进行put操作的时候才会创建一个数组长度为16的Node数组。

在JDK1.7中,是先判断是否到达阈值,再进行插入操作,假设数组容量已经到达了阈值,且此时散列得到的位置不为空,则进行扩容操作;而在JDK1.8中是插入结点后判断链表长度是否大于8,并且数组长度是否小于64,如果小于64,进行数组扩容,如果大于64,将链表转换成红黑树。

2.HashMap为什么线程不安全?

在JDK1.7中,在进行扩容操作的时候,可能会造成环形链表或者数据丢失的情况,这是由于在扩容后会对原数组中的Entry结点重新散列,由于链表插入时采用的是头插法,新的链表的顺序与原来的链表顺序相反,假设有多个线程同时进行put操作,可能会发生环形链表或者数据丢失的情况;

在JDK1.8中,链表的插入是直接在链表尾部插入,避免了环形链表的产生,但HashMap仍然是线程不安全的,因为在并发的情况下,两个线程同时进行put操作,假设它们散列到同一个槽,在线程A进行判断发现没有hash碰撞后,此时CUP时间片耗尽,线程A被挂起,线程B得到CPU时间片同时进行了put操作,操作完成后线程A再次得到时间片,由于已经判断过没有hash冲突,会直接进行put覆盖线程B插入的数据。

面试官:HashMap 为什么线程不安全?
hashmap1.7头插法造成死循环的原因分析

二、ConcurrentHashMap

1.为什么是ConcurrentHashMap而不是HashTable

对于HashMap中线程不安全的问题,可以用HashTable来解决,但是该Map实现会存在无论读还是写,它会给整个集合加锁,导致同一时间的其他操作阻塞,而ConcurrentHashMap兼顾性能和线程安全,一个线程进行写操作时,它会锁住一小部分,其他部分的读写不受影响,其他线程访问没上锁的地方不会被阻塞。

2.JDK1.7和JDK1.8下,ConcurrentHashMap的结构对比

说在最开始,JDK1.7的ConcurrentHashMap是通过分段加锁保证并发;而JDK1.8的ConcurrentHashMap是通过cas加上synchronized的方式保证并发。

在JDK1.7中,ConcurrentHashMap由16个 Segment 组合而成,共同保存在segments 数组中。Segment 本身就相当于一个 HashMap 对象。每个Segment都独立有一把ReentrantLock,保证了并发效率。在Segment内如果发生哈希冲突,同 HashMap 一样,以链表的形式放在冲突槽位上。

在这里插入图片描述

在JDK1.8中,ConcurrentHashMap同HashMap一样,对于冲突过长的链表会转换成红黑树。

为什么默认哈希冲突是链表的形式,而链表长度超过8就要红黑树化,又为什么是8呢?
首先因为红黑树的空间占用是链表的两倍,所以最开始默认是链表的形式。根据泊松分布计算,哈希冲突导致链表长度增加到8的概率已经是千万分之一了,所以我们链表的长度一般不会到达这个值,如果到了这个值,我们就红黑树化。

值得注意的是,HashMap中允许一个key值为null,但在ConcurrentHashMap中,key值为null会抛出异常。

在这里插入图片描述

在JDK1.8中,ConcurrentHashMap使用put()方法时,计算出哈希值得到槽位后会判断该槽位是否有值,如果没有值会用cas的方法把值放入进去。

在这里插入图片描述

如果cas失败,会进入下一层判断,判断是否处于扩容状态,如果是,则帮助进行扩容。

在这里插入图片描述

如果该槽位有值,则会用synchronized的方式进行put操作。

在这里插入图片描述

3.JDK1.8下,ConcurrentHashMap的put()和get()方法总结

在这里插入图片描述
在这里插入图片描述

三、ConcurrentHashMap一定是线程安全的吗

结论:一定是。但是如果在多线程并发的情况下,组合操作并不能保证线程安全。如下代码,如果我们的Runnable是在get后又进行了put,两个线程安全的操作组合在一起就变成了线程不安全

在这里插入图片描述

怎么解决?

加synchronized吗?固然这种方法可以解决,这样和我们使用HashMap没什么区别了。在ConcurrentHashMap中提供了一个replace()方法,可以让我们进行cas的原子操作,这样利用自旋和cas就可以解决问题了。

package collections.concurrenthashmap;

import java.util.concurrent.ConcurrentHashMap;

/**
 * 描述:     组合操作并不保证线程安全
 */
public class OptionsNotSafe implements Runnable {

    private static ConcurrentHashMap<String, Integer> scores = new ConcurrentHashMap<String, Integer>();

    public static void main(String[] args) throws InterruptedException {
        scores.put("小明", 0);
        Thread t1 = new Thread(new OptionsNotSafe());
        Thread t2 = new Thread(new OptionsNotSafe());
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(scores);
    }


    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            while (true) {
                Integer score = scores.get("小明");
                Integer newScore = score + 1;
                boolean b = scores.replace("小明", score, newScore);
                if (b) {
                    break;
                }
            }
        }

    }
}

总结

最开始只是想看看ConcurrentHashMap的底层原理,后来发生衍生出来的一些东西搞不明白,就去进行了递归学习哈哈哈,后面应该还会有类似由MIT6.830引发的一些思考的随笔。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值