zoukankan      html  css  js  c++  java
  • HashMap与ConcurrentHashMap

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

    modCount++

    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        } 
     
        //modCount++ 是一个非原子性操作,包括了三步:读取,增加,保存。线程不安全
        modCount++;
     
        addEntry(hash, key, value, i);
        return null;
    }

    扩容期间取出的值不准确

    在扩容期间,它会新建一个新的空数组,并且用旧的项填充到这个新的数组中去。那么,在这个填充的过程中,如果有线程获取值,很可能会取到 null 值

    put 碰撞导致数据丢失

    如果多个线程同时使用 put 来添加元素,而且恰好两个 put 的 key经过hash 值计算出来的 bucket 位置一样,并且两个线程又同时判断该位置是空的,可以写入,所以这两个线程的两个不同的 value 便会添加到数组的同一个位置,这样最终就只会保留一个数据,丢失一个数据。

    死循环造成 CPU 100%

    这种情况发生最主要的原因就是在扩容的时候,也就是内部新建新的 HashMap 的时候,扩容的逻辑会反转散列桶中的节点顺序,当有多个线程同时进行扩容的时候,由于 HashMap 并非线程安全的,所以如果两个线程同时反转的话,便可能形成一个循环,并且这种循环是链表的循环,相当于 A 节点指向 B 节点,B 节点又指回到 A 节点,这样一来,在下一次想要获取该 key 所对应的 value 的时候,便会在遍历链表的时候发生永远无法遍历结束的情况,也就发生 CPU 100% 的情况。

     

    解决方案有Hashtable和Collections.synchronizedMap(hashMap),不过这两个方案基本上是对读写进行加锁操作,一个线程在读写元素,其余线程必须等待,性能可想而知。

    ConcurrentHashMap

    ConcurrentHashMap是线程安全的,并且性能比同样性能安全的HashTable强很多,ConcurrentHashMap实现线程安全的方式在JDK1.7和JDK1.8是不太一样的。

    JDK1.7

    ConcurrentHashMap内部通过分段锁实现线程安全。

    ConcurrentHashMap 内部进行了 Segment 分段,Segment 继承了 ReentrantLock,各个 Segment 之间都是相互独立上锁的,互不影响。

    每个 Segment 的底层数据结构与 HashMap 类似,仍然是数组和链表组成的拉链法结构。默认有 0~15 共 16 个 Segment,所以最多可以同时支持 16 个线程并发操作(操作分别分布在不同的 Segment 上)。16 这个默认值可以在初始化的时候设置为其他值,但是一旦确认初始化以后,是不可以扩容的。

    JDK1.8

    1.8的实现已经抛弃了Segment分段锁机制,利用CAS+Synchronized来保证并发更新的安全,底层采用数组+链表+红黑树的存储结构,它的存储分以下几种情况:

    1. bucket为空,当前还没有元素来填充。

    2. 链表形式。在每一个bucket中会首先填入第一个节点,但是如果后续又来了一个key,并且计算出相同的 Hash 值,就用链表的形式往后进行延伸

    3. 红黑树结构。链表长度大于8(默认)且容量达到64(默认)的时候,ConcurrentHashMap 便会把这个链表从链表的形式转化为红黑树的形式,目的是进一步提高它的查找性能。链表查询时间复杂度O(N),红黑树O(log(n)),节点越来越多的情况下,O(log(n)) 体现出的优势更加明显。

    put源码

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) {
            throw new NullPointerException();
        }
        //计算 hash 值
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K, V>[] tab = table; ; ) {
            Node<K, V> f;
            int n, i, fh;
            //如果数组是空的,就进行初始化
            if (tab == null || (n = tab.length) == 0) {
                tab = initTable();
            }
            // 找该 hash 值对应的数组下标
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //NOTE:如果该位置是空的,就用 CAS 的方式放入新值
                if (casTabAt(tab, i, null,
                        new Node<K, V>(hash, key, value, null))) {
                    break;
                }
            }
            //hash值等于 MOVED 代表在扩容
            else if ((fh = f.hash) == MOVED) {
                tab = helpTransfer(tab, f);
            }
            //槽点上是有值的情况
            else {
                V oldVal = null;
                //NOTE:用 synchronized 锁住当前槽点,保证并发安全
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        //如果是链表的形式
                        if (fh >= 0) {
                            binCount = 1;
                            //遍历链表
                            for (Node<K, V> e = f; ; ++binCount) {
                                K ek;
                                //如果发现该 key 已存在,就判断是否需要进行覆盖,然后返回
                                if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                                (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent) {
                                        e.val = value;
                                    }
                                    break;
                                }
                                Node<K, V> pred = e;
                                //到了链表的尾部也没有发现该 key,说明之前不存在,就把新值添加到链表的最后
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K, V>(hash, key,
                                            value, null);
                                    break;
                                }
                            }
                        }
                        //如果是红黑树的形式
                        else if (f instanceof TreeBin) {
                            Node<K, V> p;
                            binCount = 2;
                            //调用 putTreeVal 方法往红黑树里增加数据
                            if ((p = ((TreeBin<K, V>) f).putTreeVal(hash, key,
                                    value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent) {
                                    p.val = value;
                                }
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    //检查是否满足条件并把链表转换为红黑树的形式,默认的 TREEIFY_THRESHOLD 阈值是 8
                    if (binCount >= TREEIFY_THRESHOLD) {
                        treeifyBin(tab, i);
                    }
                    //putVal 的返回是添加前的旧值,所以返回 oldVal
                    if (oldVal != null) {
                        return oldVal;
                    }
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

    get源码

    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        //计算 hash 值
        int h = spread(key.hashCode());
        //如果整个数组是空的,或者当前槽点的数据是空的,说明 key 对应的 value 不存在,直接返回 null
        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;
            }
            //如果头结点 hash 值小于 0,说明是红黑树或者正在扩容,就用对应的 find 方法来查找
            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;
    }

    Java 8 中,锁粒度更细,理想情况下 table 数组元素的个数(也就是数组长度)就是其支持并发的最大个数,并发度比之前有提高。而Java 7中最大并发数默认是 16。

  • 相关阅读:
    ​《数据库系统概念》5-连接、视图和事务
    ​《数据库系统概念》4-DDL、集合运算、嵌套子查询
    ​《数据库系统概念》3-主键、关系运算
    ​《数据库系统概念》2-存储、事务等的简介
    ​《数据库系统概念》1-数据抽象、模型及SQL
    Web API与JWT认证
    巨杉Tech | 十分钟快速搭建 Wordpress 博客系统
    巨杉内核笔记(一)| SequoiaDB 会话(session)简介
    SequoiaDB巨杉数据库入门:快速搭建流媒体服务器
    微服务?数据库?它们之间到底是啥关系?
  • 原文地址:https://www.cnblogs.com/zz-ksw/p/12832949.html
Copyright © 2011-2022 走看看