是时候研究一波HashMap了(二)

接上一篇继续说

经常说HashMap非线程安全,是如何体现的

我们经常说HashMap线程非安全,那到底是如何提现的呢,答案就是在多线程并发的条件下,还记的HashMap中有一个resize的方法么,就是把map扩容两倍的地方,不记得没关系,我给你贴源码,其实这里有两点

  1. 在jdk1.7中,多线程环境下,扩容时候会造成环形链或数据丢失
  2. 在jdk1.8中,多线程环境下,会发生数据覆盖的情况

1,7的原因是:在发生hash碰撞的时候,采用的头插入法,不是直接插入链表尾部,所以在1.8中改掉了,直接插入链表尾部,但是多线程还是不安全的,这里我只分析在1.8中的为啥不安全

       final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
        //如果没有发生hash碰撞就直接插入元素
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

注意我在代码中写的注释,就是那一行,如果线程A和线程B刚好同时操作,也刚好这两条数据的hash值是一样的,并且该位置数据都为null,所以线程A和线程B都会进入到注释下面的一行代码,这样就有可能发生一个线程的数据把另外的一个线程的数据覆盖掉,其实我们可以对比的来看一下HashMap和号称线程安全的ConcurrentHashMap的两个方法就可以下面是对比的代码

这是HashMap的

        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

这是ConcurrentHashMap的

            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }

从上面的代码里就可以看到,ConcurrentHashMap中,多了一个方法,casTabAt这个方法,这个方法,我们点进去看

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }

可以看到,这里用到了U.compareAndSwapObject这个方法,这个方法在下面就的其他的方式查找了,其实这个方法在AtomicReference也是用到的,这里是源码

    public final boolean compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
    }

再往下就是cpu层面的CAS了,这里简单介绍一下

若期望值等于对象地址存储的值,则用新值来替换对象地址存储的值,否则,把期望值变为当前对象地址存储的值.CAS是所有原子变量的原子性的基础,为什么一个看起来如此不自然的操作却如此重要呢?其原因就在于这个native操作会最终演化为一条CPU指令cmpxchg,而不是多条CPU指令。由于CAS仅仅是一条指令,因此它不会被多线程的调度所打断,所以能够保证CAS操作是一个原子操作。补充一点,当代的很多CPU种类都支持cmpxchg操作,但不是所有CPU都支持,对于不支持的CPU,会自动加锁来保证其操作不会被打断。
由此可知,原子变量提供的原子性来自CAS操作,CAS来自Unsafe,然后由CPU的cmpxchg指令来保证。

说完CAS就应该知道了在ConcurrentHashMap中是比HashMap中的put是多了一个CAS的操作的,在原子层面就限制住了多线程并发条件下修改值被另外的线程覆盖的问题,这点也提前透漏了为什么ConcurrentHashMap会线程安全的原因

ConcurrentHashMap为什么是线程安全的

其实看ConcurrentHashMap的源码和HashMap的源码就知道,他们好多地方是相同的,初始化也好,扩容也罢,链表向红黑树的转化也好,但是ConcurrentHashMap终究是多了一些HashMap所不具备的点,这个点就是他大量的使用了U.compareAndSwapObject简称CAS,这个就比较有意思了,这个方法是利用一个CAS算法实现无锁化的修改值的操作,他可以大大降低锁代理的性能消耗。这个算法的基本思想就是不断地去比较当前内存中的变量值与你指定的

一个变量值是否相等,如果相等,则接受你指定的修改的值,否则拒绝你的操作。因为当前线程中的值已经不是最新的值,你的修改很可能会覆盖掉其他线程修改的结果。这一

点与乐观锁,SVN的思想是比较类似的。
并且,它定义了三个原子操作,分别是

//获取tab数组的第i个node
    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }
//利用cas算法,设置i位置上的node节点,在cas中,会比较内存中的值与你指定的这个值是否相等,只有相等的才会接受
    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }
//利用valatile方法设置第i个节点,这个操作是一定成功的
    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }

下图就是ConcurrentHashMap的put方法
在这里插入图片描述

这里用到了锁分离的思想,就是在锁住一个node之前是建立在了CAS和Volatile的之上

猜你喜欢

转载自blog.csdn.net/lovePaul77/article/details/106911736