【学习笔记】学习Java中ConcurrentHashMap底层原理

【学习笔记】学习Java中ConcurrentHashMap底层原理,第1张

【学习笔记】学习Java中ConcurrentHashMap底层原理

学习了HashMap知识点,了解到HashMap不是线程安全的,因为HashMap在高并发情况下可能会出现环形链表,造成下一次读 *** 作形成死循环。所以,要使用ConcurrentHashMap线程安全的方法
不多说了直接学习。每天进步一点点,加油。

文章目录
  • 一、ConcurrentHashMap是什么?
  • 二、ConcurrentHashMap方法部分源码
    • 1.put()方法源码
    • 2.get()方法的源码
  • 三、可能碰到的一些问题
    • 有了解过ConcurrentHashMap吗?它的存储结构是什么样的?
    • ConcurrentHashMap1.7和1.8的区别
    • 1.8版本ConcurrentHashMap使用什么技术来保证线程安全?
    • ConcurrentHashMap默认初始容量是多少?
    • ConCurrentHashmap 的key,value是否可以为null。
    • ConcurrentHashMap有哪些构造函数?
    • ConCurrentHashmap 每次扩容是原来容量的几倍
    • ConcurrentHashMap的get方法是否要加锁,为什么?
    • java1.8中,ConCurrentHashmap是如何计算它的size大小的?


一、ConcurrentHashMap是什么?

在面试的过程面试问完HashMap还会问安全的方法有哪些?HashMap 是线程不安全的集合类,在并发情况下可能由于线程争用导致程序获取不正确的结果。而ConcurrentHashMap 是对 HashMap 的功能增强,使 HashMap 支持高并发下的读写线程安全。看了ConcurrentHashMap源码发现很多方法和代码跟HashMap相似。
只是思路相同,实现不同而已。

二、ConcurrentHashMap方法部分源码 1.put()方法源码
    
    public V put(K key, V value) {
        return putVal(key, value, false);
    }
   final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 不允许插入空值或空键
    // 允许value空值会导致get方法返回null时有两种情况:
    // 1. 找不到对应的key2. 找到了但是value为null;
    // 当get方法返回null时无法判断是哪种情况,在并发环境下containsKey方法已不再可靠,
    // 需要返回null来表示查询不到数据。允许key空值需要额外的逻辑处理,占用了数组空间,且并没有多大的实用价值。
    // HashMap支持键和值为null,但基于以上原因,ConcurrentHashMap是不支持空键值。
    if (key == null || value == null) throw new NullPointerException();
    // 高低位异或扰动hashcode,和HashMap类似
    int hash = spread(key.hashCode());
    // bincount表示链表的节点数
    int binCount = 0;
    // 尝试多种方法循环处理,后续会有很多这种设计
    for (Node[] tab = table;;) {
        Node f; int n, i, fh;
        // 情况一:如果数组为空则进行初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // 情况二:目标下标对象为null
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 重点:采用CAS进行插入
            if (casTabAt(tab, i, null,new Node(hash, key, value, null)))
                break;
        }
        // 情况三:数组正在扩容,帮忙迁移数据到新的数组
        // 同时会新数组,下次循环就是插入到新的数组
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // 情况四:直接对节点进行加锁,插入数据
        // 下面代码很多,但逻辑和HashMap插入数据大同小异
        // 因为已经上锁,不涉及并发安全设计
        else {
            V oldVal = null;
            // 同步加锁
            synchronized (f) {
                // 重复检查一下刚刚获取的对象有没有发生变化
                if (tabAt(tab, i) == f) {
                    // 链表处理情况
                    if (fh >= 0) {
                        binCount = 1;
                        // 循环链表
                        for (Node e = f;; ++binCount) {
                            K ek;
                            // 找到相同的则记录旧值
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                // 判断是否需要更新数值
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node pred = e;
                            // 若未找到则插在链表尾
                            if ((e = e.next) == null) {
                                pred.next = new Node(hash, key, value, null);
                                break;
                            }
                        }
                    }
                    // 红黑树处理情况
                    else if (f instanceof TreeBin) {
                        Node p;
                        binCount = 2;
                        if ((p = ((TreeBin)f).putTreeval(hash, key, value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                    else if (f instanceof ReservationNode)
                        throw new IllegalStateException("Recursive update");
                }
            }
            // 判断是否需要转化为红黑树,和返回旧数值
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 总数+1
    addCount(1L, binCount);
    return null;
}

从上面的源码可以看出ConcurrentHashMap 不支持使用 null 作为 key 或者 value。
ConcurrentHashMap添加数据时,采取了CAS+synchronize结合策略。首先会判断该节点是否为null,如果为null,尝试使用CAS添加节点;如果添加失败,说明发生了并发冲突,再对节点进行上锁并插入数据。在并发较低的情景下无需加锁,可以显著提高性能。同时只会CAS尝试一次,也不会造成线程长时间等待浪费cpu时间的情况。ConcurrentHashMap的put方法整体流程如下(并不是全部流程):

流程如下:

1.首先会判断数组是否已经初始化,如若未初始化,会先去初始化数组;
2.如果当前要插入的节点为null,尝试使用CAS插入数据;
3.如果不为null,则判断节点hash值是否为-1;-1表示数组正在扩容,会先去协助扩容,再回来继续插入数据。
4.最后会执行上锁,并插入数据,最后判断是否需要返回旧值;
5.如果不是覆盖旧值,需要更新map中的节点数,也就是图中的addCount方法。

注意到源码中有两个关键方法:初始化数组的initTable(),修改map中节点总数的addCount。是put插入的关键方法。

2.get()方法的源码
   public V get(Object key) {
        Node[] tab; Node e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

通过上面的get()方法源码可以看出,首先要计算 hash 值,然后再根据hash 值找到数组对应位置: (n - 1) & h。最后根据该位置处结点性质进行相应找。如果该位置为 null,那么直接返回 null 。如果该位置处的节点刚好就是需要的值,就返回该节点的值。如果该位置节点的 hash 值小于 0,说明容量不足正在扩容,或者是红黑树进行遍历对比查询数据。


三、可能碰到的一些问题 有了解过ConcurrentHashMap吗?它的存储结构是什么样的?

作为Java开发工程师一定要学习和查看ConcurrentHashMap源码数据结构知识点,
ConcurrentHashMap数据结构跟HashMap一样数组+链表+红黑树实现的。

ConcurrentHashMap1.7和1.8的区别

1.7 版本数组+链表,Segment+HashEntry。锁的粒度基于segment,包含多个HashEntry。锁的粒度比较大。
1.8 版本数组+链表+红黑树(Node+CAS+Synchronized),锁的粒度就是node,大大降低了锁的粒度。

1.8版本ConcurrentHashMap使用什么技术来保证线程安全?

采用了Node+CAS+Synchronized锁的方式来保证线程安全

ConcurrentHashMap默认初始容量是多少?

1.8版本中ConcurrentHashMap默认初始容量是16,可以根据业务需要设置容量大小,不设置就是取默认值。

ConCurrentHashmap 的key,value是否可以为null。

都不可以为null ,如果Key或者Value为null会报空指针异常。

ConcurrentHashMap有哪些构造函数?

有五个构造函数,分别是:无参构造函数,可传入容器大小的构造函数,可传入Map的构造函数,可设置阈值和初始容量的构造函数,可设置初始容量和阈值并发级别等五种,具体源码可查看以下部分

  //无参构造函数
    public ConcurrentHashMap() {
    }
    //可传初始容器大小的构造函数
    public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }
    //可传入map的构造函数
    public ConcurrentHashMap(Map m) {
        this.sizeCtl = DEFAULT_CAPACITY;
        putAll(m);
    }
    //可设置阈值和初始容量
    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, 1);
    }

    //可设置初始容量和阈值和并发级别
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            initialCapacity = concurrencyLevel;   // as estimated threads
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        this.sizeCtl = cap;
    }
    
ConCurrentHashmap 每次扩容是原来容量的几倍

每次扩容是原来的2倍
在transfer方法里面会创建一个原数组的俩倍的node数组来存放原数据。

ConcurrentHashMap的get方法是否要加锁,为什么?

不需要,get方法采用了unsafe方法来保证线程安全

java1.8中,ConCurrentHashmap是如何计算它的size大小的?

对于size的计算,在put()的时候使用了addCount()方法。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5694181.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存