源码解析之为何要用ConcurrentHashMap

为什么要用ConcurrentHashMap?

ConcurrentHashMap是JUC包下的一个线程安全的HashMap类,我们都知道多线程的场景下要用ConcurrentHashMap来代替HashMap使用,有没有想过为什么不能用HashMap,为什么能用ConcurrentHashMap呢?下面我通过走源码的方式,带大家看一看其中的一些细节!

HashMap

map数组的一种,JDK1.8中的HashMap以数组+链表/红黑树的形式存在,这里不做过多解释

当我们在执行多线程任务时,若是操作的资源为HashMap类型时就可能会导致程序出现并发异常,如图

点到nextNode这个方法中去看源码,原因很明显,如图

那么,if判断中的两个变量是干什么的呢,为什么这俩变量不相同就要抛出一个异常?

首先,我们看一下我们从创建Hashmap变量到抛出异常这段时间内都做了些什么事情

整个流程的代码如下

public static void main(String[] args) {
        HashMap<String,String> map = new HashMap<>();
        for (int i = 0;i < 20; i++){
            new Thread(()-> {
                map.put(Thread.currentThread().getName(),new Date().toString());
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }

我们点进HashMap的方法中的构造方法能看到,这里只给了loadFactor这个变量一个初始值,也就是我们熟知的加载因子0.75

我们再去看一下put方法都干了些什么

put->putVal

点开后发现,这不就是我们上面看到的if判断中的第一个变量吗?我们点到这个变量的定义的地方可以看到,这是一个int类型的且并未赋值

我们通过这点能得到一个初步的结论,当我们每执行一次put方法的时候,这个值就会进行一次+1的操作

大家可以看到下面还有一个叫afterNodeInsertion的方法,我们点开后发现是一个空方法,再看上边的注释,大概意思是给LinkedHashMap提供的回调的方法,LinkedHashMapHashMap的一个子类,我们不必理会

然后我们点开println方法

println->valueOf->toString

找到我们HashMap重写的toString方法,我们发现在HashMap中并没有找到被重写的toString方法,那么我们直接去搜一下他的父类

搜查过程中,如果用Ctrl+f搜索整个类,会看到有一条toString查询记录,此处为HashMap的内部类Node的方法,并非HashMap的

发现AbstractMap类中果然重写了toString方法

此处我们终于看到了迭代器,我们再返回HashMap类去看一下entrySet方法都干了啥

因为我们走过了put方法,所以此时HashMap中的entrySet是有内容的,可以看到这里是直接把entrySet返回了

entrySet实际上就是一个Set集合,将我们HashMap中的存储单元Entry放到了里面(HashMap.put)

然后我们通过泛型找到实现集合接口中迭代器方法的位置,又是一个HashMap的内部类,EntrySet

发现又一个没见过的类,我们顺势点开

点开nextNode方法后发现,哎,这不是报错的地儿吗,到现在我们modCount也有了,报错的节点也找到了,还差一个expectedModCount没找到呢,于是在一通检查后,突然想到,会不会在某个构造方法中,回去看了一下EntryIterator也没有自己写构造方法呀,于是打开了他的父类,一下子豁然开朗,直接看图吧

到这,这俩哥们总算找齐了,那么这里问题又来了,给expectedModCount赋的就是modCount的值呀,这俩哥们咋还能不一样呢

答案也浮出水面了,单线程下肯定是不会出问题,可是我们是多线程操作呀,如果A线程刚给完值,B线程跑putVal方法去了,跑完modCount+1了,然后A线程紧接着走到了nextNode方法中,一对比就不对了,然后就抛异常了

所以说HashMap在并发场景下还是很容易出问题的

ConcurrentHashMap

HashMap在多线程的场景下不能用了,不安全呀,于是适配多线程的线程安全的HashMap:ConcurrentHashMap应运而生

然后我们把原代码中的HashMap换成ConcurrentHashMap

public static void main(String[] args) {
        ConcurrentHashMap<String,String> map = new ConcurrentHashMap<>();
        for (int i = 0;i < 20; i++){
            new Thread(()-> {
                map.put(Thread.currentThread().getName(),new Date().toString());
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }

执行了N遍,确实不报错了,每次循环都能打印成功,如此神奇的东西,让我们看看为什么他不报错了

首先点开这个类进去看看

发现这个类也继承了AbstractMap类,说明与HashMap是师出同门的呀,然后我们再去找上边那俩兄弟的时候发现,不见了,然后去找迭代器和next方法的时候发现,完全换了一套,所以自然不会像这样报错,那么他是怎么处理多线程操作的场景的呢

ConcurrentHashMap是通过synchronized + CAS 算法来实现线程安全的

如果去看源码的话,你会发现ConcurrentHashMap里面有很多Unsafe.compareAndSwap+数据类型的写法,这种写法就是利用CAS算法实现无锁化的修改值操作,此算法可以很大程度的减少加锁过程中造成的性能损耗

这个算法大概就是不断地去用内存中的变量值与代码预期的变量值是否相同,如果是一样的就会修改成功,如果不一样就会拒绝执行修改,用这种方式去判断当前线程中是否是最新的值,若不是则可能会覆盖其他线程的结果

正因此算法的判断方式,如果某个线程将值修改然后又改回去了,该算法仍然会认为这是最新值没有被改过

而通过观察源码发现,在操作Node相关对象时,会用synchronized将对象锁住

原创作者: zyyjgsj 转载于: https://www.cnblogs.com/zyyjgsj/p/18371452
ConcurrentHashMapJava 中的一个线程安全的哈希表,它是通过分段锁技术实现线程安全。下面对 ConcurrentHashMap源码进行简要解析。 ### 数据结构 ConcurrentHashMap 内部维护了一个 Segment 数组,每个 Segment 都是一个独立的哈希表,而且这些哈希表的数量可以在创建 ConcurrentHashMap 时指定。每个 Segment 内部都是一个类似 HashMap 的数据结构,也就是一个数组加链表的结构。ConcurrentHashMap 中的所有操作都是先定位到对应的 Segment,然后在 Segment 中进行操作。 ### put 方法 ConcurrentHashMap 的 put 方法首先会调用 hash 方法计算键的哈希值,然后根据哈希值找到对应的 Segment。接着会调用 Segment 的 put 方法,这个方法会加锁并且调用内部的 put 方法将键值对放入内部的 HashMap 中。如果 put 时,HashMap 中已经存在了这个键值对,那么就会更新这个键值对的值。最后释放锁。 ### get 方法 ConcurrentHashMap 的 get 方法也是先定位到对应的 Segment,然后调用内部的 get 方法,在内部的 HashMap 中查找键对应的值。由于在查找的过程中没有加锁,所以在多线程的情况下可能会出现一些数据不一致的问题,但是这个问题被认为是可以接受的,因为它不会影响数据的正确性。 ### size 方法 ConcurrentHashMap 的 size 方法也是先定位到对应的 Segment,然后调用内部的 count 方法,这个方法返回的是当前 Segment 中键值对的数量。最后将所有 Segment 中的键值对数量相加得到 ConcurrentHashMap 的大小。 ### 总结 ConcurrentHashMap 是通过分段锁技术实现线程安全的哈希表,它的内部维护了一个 Segment 数组,每个 Segment 都是一个独立的哈希表。ConcurrentHashMap 中的所有操作都是先定位到对应的 Segment,然后在 Segment 中进行操作。在 put 操作的过程中会加锁,而在 get 操作的过程中不会加锁,所以在多线程的情况下可能会出现一些数据不一致的问题,但是这个问题被认为是可以接受的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值