资讯专栏INFORMATION COLUMN

一篇搞定基于JDK1.7,JDK1.8 HashMap、ConcurrentHashMap原理分析

Genng / 869人阅读

摘要:但是还会有统计数问题和数据丢失问题。中使用了保证线程安全,但是在中又把它优化掉了,直接使用

一、开篇

HashMap、CurrentHashMap 面试时都要问烂了,用也用烂了。但是网上的解析要不就是不够全面,要么就是copy来copy去,连基于JDK版本有的都很混乱。这篇文章带你解析 基于jdk1.7、jdk1.8不同的hashMap和ConcurrentHashMap简略分析(很多代码和HashMap都是重复的)。希望看完后有所收获。

二、提出问题

1、我们都知道,HashMap是线程不安全的,那它的不安全体现在哪些方面呢?
2、HashMap在JDK1.8引入了红黑树,引入后有什么好处呢?除了引入红黑树于1.7还有什么不同呢?
3、ConcurrentHashMap怎样解决的HashMap的并发问题,用synchronized不行吗?JDK1.8又有什么改变呢?
4、欢迎留言提出各种问题...............

三、源码分析

基于jdk1.7的HashMap

首先来看1.7的存储元素类Entry类

 static class Entry implements Map.Entry {
        final K key;
        V value;
        Entry next;
        int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        ......
      }

hash()方法:

如果Key是字符串类型,则使用专门为字符串设计的Hash方法,否则使用一连串的异或操作增加hash随机性   
   final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
   

接下来是put()方法:

 public V put(K key, V value) {
        if (key == null)  
            return putForNullKey(value); //专门放置NullKey
        int hash = hash(key);//获取hash值
        int i = indexFor(hash, table.length);//获取数组下标 h & (length-1);
       
       //这个for循环进行检测,如果找到key相同的元素则进行覆盖
        for (Entry 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++;//修改次数+1
        
        
        addEntry(hash, key, value, i);
        return null;
    }

可以看出addEntry()方法进行添加元素,进去看一下:

 void addEntry(int hash, K key, V value, int bucketIndex) {
       //扩容检测
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
         
        //创建entry
        createEntry(hash, key, value, bucketIndex);
    }
    
    
  void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry e = table[bucketIndex];//获取原先数组位置的元素
        table[bucketIndex] = new Entry<>(hash, key, value, e);//该数组位置替换新元素,next为原元素(头插)
        size++;
    }

以上put方法就完成了,但是有一个重点是扩容方法,让我们点进去看下resize方法():

 void resize(int newCapacity) {
        Entry[] oldTable = table//原数组
        int oldCapacity = oldTable.length;
        //当扩容到允许最大扩容数时不再扩容
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        //新数组
        Entry[] newTable = new Entry[newCapacity];
        
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;//是否需要再Hash
        transfer(newTable, rehash);//进行数组移动
        table = newTable;//新数组
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//扩大Threshold
    }
    
    
  
      void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        //不断将原元素放进新数组
        for (Entry e : table) {
            while(null != e) {
                Entry next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

get()方法就相对简单些:

public V get(Object key) {
        if (key == null)
            return getForNullKey();//get null key
        Entry entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }
    
    
    final Entry getEntry(Object key) {
        int hash = (key == null) ? 0 : hash(key);//计算hash值
        
        //链表查询
        for (Entry e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }
  

到这里为止1.7HashMap主要方法就分析完毕,1.7的hashMap实现还是很容易理解的。但是如果出现并发,到底在哪里有问题呢。

分析put方法:①modCount++;不是原子的,所以修改次数不准确,size++不是原子的,所以hashMap的size也不准确
 ②在createEntry()方法中,假设两个线程同时获取数组头结点,一个线程修改后另一个线程还使用原来的数组元素头结点会造成数据丢失
 ③扩容死循环问题,在transfer方法中,假设线程1、2同时扩容,则可能出现头插法互相引用扩容死循环的问题

2. 基于jdk1.8的HashMap

让我们以同样的招式看一看在jdk1.8中HashMap有什么变化
首先看下1.8存储元素的Node类

和1.7中的Entry类似,不过换了个名字
 static class Node implements Map.Entry {
        final int hash;
        final K key;
        V value;
        Node next;

        Node(int hash, K key, V value, Node next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        ......
       }
  
当晋升为树形结构时,则采用TreeNode
  static final class TreeNode extends LinkedHashMap.Entry {
        TreeNode parent;  // red-black tree links
        TreeNode left;
        TreeNode right;
        TreeNode prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node next) {
            super(hash, key, val, next);
        }
        ......
      }

hash方法:

     可以看出Hash算法被优化了,使得高位参与运算,减少了hash冲突
    
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

接下来是put方法【方法变长了o(╥﹏╥)o】:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)//如果tabl未创建或table长度为0,则初始化(初始化过程也放入扩容方法中)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)//计算数组位置,如果该位置没有元素直接放入数组中
            tab[i] = newNode(hash, key, value, null);
        else {                                    //该位置有元素
            Node e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;                         //如果该位置与元素相同key直接覆盖值
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);//如果该结构为树形,则加入TreeNode
            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);//如果链表长度过长(8),转换为红黑树
                        break;
                    }
                    //如果链表中存在相同Key则直接覆盖
                    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);//LinkedHashMap使用,保证顺序(HashMap实现为空)
                return oldValue;//返回oldValue
            }
        }
        ++modCount;//操作次数加1
        if (++size > threshold)
            resize();//如果size>容量则进行扩容
        afterNodeInsertion(evict);//LinkedHashMap使用,用来回调移除最早放入Map的对象(HashMap实现为空)
        return null;
    }

OK,put方法分析完后就着重分析下扩容方法了resize():【吐槽一下我一直觉得jdk1.8可读性变差了,不仅体现在hashMap上,是提高了性能原因吗?】

final Node[] resize() {
        Node[] oldTab = table;     //老的数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE; //如果已达到最大容量不在扩容
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY) //扩容到原来的两倍
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) 
            newCap = oldThr;
        else {               // 如果原容量为0,则进行初始化
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;//   新可容纳元素个数终于确定了
        @SuppressWarnings({"rawtypes","unchecked"})
            Node[] newTab = (Node[])new Node[newCap];//新数组
        table = newTab;
        if (oldTab != null) {                  //排出初始化情况
            for (int j = 0; j < oldCap; ++j) { //开始循环数组
                Node e;                   
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;           //如果数组位置不为null取值
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;//如果该位置元素没有next节点,将该元素放入新数组
                    else if (e instanceof TreeNode)
                        ((TreeNode)e).split(this, newTab, j, oldCap); //如果是树形节点
                    else { // preserve order                
                          //如果是链表且有next节点 ,以原位置移动到新位置
                        Node loHead = null, loTail = null;
                        Node hiHead = null, hiTail = null;
                        Node next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

get()方法:

 public V get(Object key) {
        Node e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;// 判断key为null的情况
    }
    
    
     final Node getNode(int hash, Object key) {
        Node[] tab; Node first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {                 //确保数组被初始化
            if (first.hash == hash &&                           // 检查第一个元素
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)             //树查找
                    return ((TreeNode)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&                 //链表查找
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

1.8的HahMap主要方法也分析完毕了。那么1.8还会有死循环问题吗?可以看出保在移入新数组中保证了链表的原顺序,所以不会有这个问题了。但是 ,还会有统计数问题和数据丢失问题。

基于jdk1.7的currentHashMap

concurrentHashMap介绍就不能像hashmap一样直接进入方法了,有些同学不了解它的属性,让我们先看一下

HashEntry :用来存储元素
static final class HashEntry {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry next;

        HashEntry(int hash, K key, V value, HashEntry next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        
Segment桶  实现线程的关键类(部分属性为列出,太长了。。)
static final class Segment extends ReentrantLock implements Serializable {   
  
       //可以看出Segment继承自ReentrantLock,是一个天然的锁
       
       
      transient volatile HashEntry[] table;

       ···
        transient int count;

      
        transient int modCount;

       
        transient int threshold;

        
       
        final float loadFactor;  transient volatile HashEntry[] table;

        transient int count;

       
        transient int modCount;

       
        transient int threshold;

       
        final float loadFactor;
        
        ···
}  

put()方法:

public V put(K key, V value) {
        Segment s;
        if (value == null)
            throw new NullPointerException();  //不予许null value
        int hash = hash(key);                   //或许hash
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment)UNSAFE.getObject           
             (segments, (j << SSHIFT) + SBASE)) == null) 
            s = ensureSegment(j);              //根据hash获取对应的桶
        return s.put(key, hash, value, false); //put
    }
    
    
   //可以看出put方法和hashMap非常像,只不过多了一个加解锁的过程
 final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            HashEntry node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);//tryLock失败会重复执行tryLock则进行lock【阻塞】,等待锁的释放
            V oldValue;
            try {
                HashEntry[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry first = entryAt(tab, index);
                for (HashEntry e = first;;) {
                    if (e != null) {
                        K k;
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    else {
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry(hash, key, value, first);
                        int c = count + 1;
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }
   
    

get()方法:

//get方法是不加锁的,所有共享变量都是通过volatile修饰的,确保或许最新值
public V get(Object key) {
        Segment s; 
        HashEntry[] tab;
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null &&  //获取volatile
            (tab = s.table) != null) {
            for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile  //获取volatile
                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
                 e != null; e = e.next) {
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }

基于jdk1.8的ConcurrentHashMap

存储节点:

 static class Node implements Map.Entry {
        final int hash;
        final K key;
        volatile V val;
        volatile Node next;

put()方法:

可以看出jdk1.8中取消了桶的设计,使用了Node + CAS + Synchronized来保证并发安全进行实现
  
final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node[] tab = table;;) {
            Node f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();            //初始化
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {    //如果该位置无元素则使用cas插入
                if (casTabAt(tab, i, null,
                             new Node(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;     
                synchronized (f) {          //如果有元素或cas插入失败 则使用synchronized 锁
                    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;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

get()方法

 // 也是不加锁的,直接获取volatile
 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;
    }

三、总结
JDK1.7中HashMap采用了数组+链表的数据结构,有线程安全问题(统计不准确,丢失数据,死循环cpu100%),1.8中采用了数组+链表+红黑树的结构,有线程安全问题(统计不准确,丢失数据)。1.8中优化了hash算法,并且每次扩容不需要重新计算hash值。
JDK1.7中concurentHashMap使用了segment保证线程安全,但是在1.8中又把它优化掉了,直接使用cas+synchronized

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76794.html

相关文章

  • 这几道Java集合框架面试题在面试中几乎必问

    摘要:若遇到哈希冲突,则将冲突的值加到链表中即可。之后相比于之前的版本,之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值默认为时,将链表转化为红黑树,以减少搜索时间。有序,唯一红黑树自平衡的排序二叉树。 本文是最最最常见Java面试题总结系列第三周的文章。主要内容: Arraylist 与 LinkedList 异同 ArrayList 与 Vector 区别 HashMap的底层...

    bigdevil_s 评论0 收藏0
  • ConcurrentHashMap基于JDK1.8源码剖析

    摘要:下面我来简单总结一下的核心要点底层结构是散列表数组链表红黑树,这一点和是一样的。是将所有的方法进行同步,效率低下。而作为一个高并发的容器,它是通过部分锁定算法来进行实现线程安全的。 前言 声明,本文用的是jdk1.8 前面章节回顾: Collection总览 List集合就这么简单【源码剖析】 Map集合、散列表、红黑树介绍 HashMap就是这么简单【源码剖析】 LinkedHas...

    sanyang 评论0 收藏0
  • JDK源码(容器篇)

    摘要:三系列用于保存键值对,无论是,还是已弃用的或者线程安全的等,都是基于红黑树。是完全基于红黑树的,并在此基础上实现了接口。可以看到,只有红黑树,且红黑树是通过内部类来实现的。 JDK容器 前言 阅读JDK源码有段时间了,准备以博客的形式记录下来,也方便复习时查阅,本文参考JDK1.8源码。 一、Collection Collection是所有容器的基类,定义了一些基础方法。List、Se...

    Soarkey 评论0 收藏0
  • Java-Mysql你所需要的面试题集内容

    摘要:注意排版不需要花花绿绿的,尽量使用语法。协议的长连接和短连接,实质上是协议的长连接和短连接。长连接短连接究竟是什么三次握手和四次挥手面试常客为了准确无误地把数据送达目标处,协议采用了三次握手策略。 一 简历该如何写 1.1 为什么说简历很重要?1.2-这3点你必须知道1.3-两大法则了解一1.4-项目经历怎么写?1.5-专业技能该怎么写?1.6-开源程序员简历模板分享1.7 其他的一些...

    OpenDigg 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<