jdk1.8 HashMap&ConcurrentHashMap分析

1.8 HashMap分析

1.8中的HashMap不同点有:

数据结构:采用的是数组+链表+红黑树,当链表长度上已经有8个元素了,put第9个元素时,链表变为红黑树

put()方法:在put()操作时采取的尾插法(1.7是头插法),1.8中采用尾插法

扩容条件:1.8中if(++size>threshoId)而1.7中if ((size >= threshold) && (null != table[bucketIndex]))

扩容过程:1.7中在转移元素时是一个一个转移,并采用头插法形式(这会导致多线程下扩容造成死循环)。1.8中分链表转移和红黑树转移,这两个转移共同点是,已拆分成两个链表,然后一个链表整体转移,具体实现看下文。

1,红黑树

我们都知道,红黑树是一种特殊的二叉树。二叉树都有一个特点,逐条插入数据时,左边的子节点小于父节点,右边的子节点大于父节点。红黑树除了这个特征外还有以下特征:

特征1. 结点是红色或黑色。 
特征2. 根结点是黑色。 
特征3.所有叶子都是黑色。(叶子是Null结点) 
特征4. 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
特征5.. 从任一节结点其每个叶子的所有路径都包含相同数目的黑色结点。

这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树

这里有一个学习数据结构的网站:https://www.cs.usfca.edu/~galles/visualization/RedBlack.html

 红黑树在插入数据时有以下规律:先调整最下面的子孙三代符合红黑树5个特征,再递归往上面调整一直到root节点符合红黑树5个特征

(1) 新数据进来时一开始默认是红节点

(2) 父亲节点是黑色的:不用调整

(3) 父亲节点是红色:

      叔叔是空的,旋转+变色

      叔叔是红色,父节点+叔叔节点变黑色,祖父节点变为红色

      叔叔是黑色,旋转+变色

2,源码分析

我们先看看HashMap中红黑树TreeNode这个类:我们可以看到有以下几个属性,

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        //父节点
        TreeNode<K,V> parent;  // red-black tree links
        //左子节点
        TreeNode<K,V> left;
        //右子节点
        TreeNode<K,V> right;
        //指向前节点,主要是在链表转红黑树时用到
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        //是否为红色节点
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }

而且继承了LinkedHashMap,LinkedHashMap又继承了HashMap。我们都知道,HashMap中有Node<K,V>静态内部类:(1.7中存的叫Entry<K,V>,属性一样,只是命名不同)

put()方法分析:过程和1.7差不多,就多了一个红黑树的操作

看下这几个参数代表的含义:

/**
     *
     * @param hash  由key计算出来的 hash值
     * @param key   要存储的key
     * @param value  要存储的value
     * @param onlyIfAbsent  如果当前位置已存在一个值,是否替换,false是替换,true是不替换
     * @param evict  表是否在创建模式,如果为false,则表是在创建模式。
     */

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict)
 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)
            //对tab数组的初始化,初始化和扩都写在resize()方法中
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)//
            //当前key算出数组下标位置为空,直接new一个新Node<K,V>节点
            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);
                        //如果长度超过8,则变为红黑树
                        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;
                }
            }
            //如果有重复的key,则覆盖value,并把老值返回
            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;
    }

我们看看变红黑树这个方法treeifyBin(tab, hash):链表长度大于8只是个前提条件,我们会发现还必须满足数组长度>=64。否则进行扩容而非转红黑树。(因为转红黑树的目的为了缩短链表长度,而扩容重新计算hash也能达到这个效果)

我们在看下treeify(tab)这个方法:

我们看看moveRootToFront(tab,root)这个方法:

static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
            int n;
            if (root != null && tab != null && (n = tab.length) > 0) {
                int index = (n - 1) & root.hash;
                TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
                //把root节点移到双向链表的第一个位置
                if (root != first) {
                    Node<K,V> rn;
                    //把红黑树放到数组下标位置
                    tab[index] = root;
                    TreeNode<K,V> rp = root.prev;
                    if ((rn = root.next) != null)
                        ((TreeNode<K,V>)rn).prev = rp;
                    if (rp != null)
                        rp.next = rn;
                    if (first != null)
                        first.prev = root;
                    root.next = first;
                    root.prev = null;
                }
                assert checkInvariants(root);
            }
        }

红黑树转变过程如下:

put<K,V>流程图:

resize()扩容方法分析:这个方法分为两部分,初始化和扩容

真正的扩容操作:分两种情况转移Node<K,V>对象,一种是链表的转移,另一种是红黑树的转移。

链表的转移:

大致如下:

红黑树的转移:TreeNode<K,V>里有 TreeNode<K,V> pre节点,也有Node<K,V> next节点,有隐藏的双向链表性质,遍历较为方便。

这里在将红黑树拆分成两个TreeNode<K,V>节点时,只对TreeNode<K,V> pre,Node<K,V>赋值,而left,right是没有赋值的。

   总结:

  • 添加时,当桶中链表个数超过 8 时会转换成红黑树;
  • 删除、扩容时,如果桶中结构为红黑树,并且树中元素个数太少的话,会进行修剪或者直接还原成链表结构;
  • 查找时即使哈希函数不优,大量元素集中在一个桶中,由于有红黑树结构,性能也不会差。

1.8 ConcurrentHashMap分析

1.8中的ConcurrentHashMap不同点有:

(1)内部存储数据不同:取消了Segment<K,V>[]数组(1.7中是两个数组)

(2)数据结构不同:增加了红黑树(数组+链表+红黑树),这与1.8HashMap又稍微有点不同。他红黑树时封装在TreeBin<K,V>中(HashMap是封装在TreeNode<K,V>中),TreeBin里面包含了TreeNode<K,V>属性,红黑树的实现还是在TreeNode<K,V>中,只是多封装了一层TreeBin。(这样设计的目的是:synchronized(TreeBin<K,V>),这个永远不变,而synchronized(TreeNode<K,V>),红黑树的根节点会随着数据的插入而发生变化)

(3)加锁方式不同:1.7采用分段锁(每个Segment<K,V>一个锁),1.8中是利用CAS(自旋锁)和synchronized

(4)扩容的过程不同:1.8中取消了Segment[]的数组,扩容是针对整个table(1.7是单线程Segment内部扩容,多线程对多个Segment扩容,互不影响),而1.8中支持多线程对table同时扩容

1,源码分析

默认ConcurrentHashMap cmap = new ConcurrentHashMap();创建了一个空map,put()操作时进行初始化

 /**
     * Creates a new, empty map with the default initial table size (16).
     */
    public ConcurrentHashMap() {
    }

如果我们自己传参数:ConcurrentHashMap cmap = new ConcurrentHashMap(65,0.75f,16);

我们直接来看put()方法:注意看for()循环里面的if分支,每个线程进来只可能走其中一个分支,但每个if的判断条件还是会走

这里讲一下put()放元素时是通过synchronize(f)来保证线程安全:锁的是table[i]位置的对象。锁的这个对象f两种情况(如果该位置是链表,就是链表的头结点对象),如果是红黑树,锁的就是TreeBin对象。我们来看看TreeBin对象里的属性:这里跟1.7不一样,1.7是存的是 TreeNode对象(我们都知道,树化的过程根节点(TreeNode<K,V> root 是可能会变的。所以1.8在synchronize(f)把红黑树封装在TreeBin对象里,以保证锁的对象不变)

 static final class TreeBin<K,V> extends Node<K,V> {
        TreeNode<K,V> root;//红黑树的根节点
        volatile TreeNode<K,V> first;第一个节点
        volatile Thread waiter;
        volatile int lockState;
        // values for lockState
        static final int WRITER = 1; // set while holding write lock
        static final int WAITER = 2; // set when waiting for write lock
        static final int READER = 4; // increment value for setting read lock

我们先看初始化的方法initTable():

/**
*sizeCtl默认为0
*-1代表正在初始化
*还有代表阈值
*/ 
private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                //当前线程让出CPU时间,重新竞争
                Thread.yield(); // lost initialization race; just spin
            //CAS算法保证只有一个线程进来进行初始化
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);//0.75n 即阈值
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

接下来看下树化这个方法treeifyBin(table,i):

 private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            //和hashmap一样,数组长度小于64则扩容不进行树化
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                synchronized (b) {
                    if (tabAt(tab, index) == b) {
                        TreeNode<K,V> hd = null, tl = null;
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        //进行树化
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

我们再看看addCount(1,binCount)方法;多线程下怎么对数量加一的

我们来看看fullAddCount()方法:这个方法就是每put成功一次进行加一,对ConcurrentHashMap里面数量的统计。这个方法的大概思路就是利用自旋锁(死循环+CAS)的方式对binCount加一或者CounterCell属性里面的value值加一。就是说多线程竞争给baseCount加一失败,会生成CounterCell数组,然后对CounterCell里面的value加一。(每个线程会生成一个随机数,然后进行 随机数&length-1 计算出下标,然后对立面的属性value加一)。这样子设计就有从一开始很多个线程竞争baseCounter,然后分散到CounterCell[]数组上(可能会出翔两个线程算出同一个下标,竞争同一个CounterCell竞争,失败的又重新循环。里面的代码是for(;;),知道每个线程加成功)。

所以我们可以看到计算ConcurrentHashMap里面元素总个数的方法是这样的:baseCount+遍历CounterCell数组里面ConterCell的value值

 我们再看看扩容的逻辑:

我们再看put方法里面的帮助扩容方法也是一样:

如何判断扩容工作完成呢,transfer里有一个判断:

 

看转移元素transfer()方法:这里先讲下整个逻辑

 private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        //算出stride步长(即每个线程需要转移元素的范围),最小16
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        //翻倍扩容并赋值给nextTab
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;
        }
        int nextn = nextTab.length;
        //重要的属性fwd:当老数组这个位置的元素被转走了(或被线程判断了为空,不需要转移),会在这 
        个位置放入fwd。如果一个线程在 
        put()时,发现put的位置存的是fwd,则该线程会去帮助扩容(转移元素)
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        //当前这个线程是否继续往前面找元素
        boolean advance = true;
        //当前这个线程转移工作是否完成(注:不是整个扩容工作)
        boolean finishing = false; // to ensure sweep before committing nextTab
        //i,bound老数组下标位置,控制上面算的数组步长,从右往左移转移元素
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                 //通过CAS算法(改bound值)保证每个线程算出的数组下标不同,即保证每个线程转移不同 
                   范围的元素到新数组
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);//该位置为null变为fwd
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                synchronized (f) {//加锁,开始真正的转移操作
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                       //链表的转移,和1.7的ConcurrentHashMap一样,可以看我上 一篇文章
                        if (fh >= 0) {
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);//第i个位置转移后设置为fwd
                            advance = true;
                        }
                        //红黑树的转移,和1.8的HashMap转移红黑树一样
                        else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);//第i个位置转移完成后设置fwd
                            advance = true;
                        }
                    }
                }
            }
        }
    }

 /重要的属性:

fwd:当老数组这个位置的元素被转走了(或被线程判断了为空,不需要转移),会在这 个位置放入fwd。如果一个线程在put()时,发现put的位置存的是fwd,则该线程会去帮助扩容(转移元素)

boolean advance:当前这个线程是否继续往前面找元素

boolean finishing:当前这个线程步长区间转移工作是否完成(注:不是整个扩容工作)

通过CAS算法(改bound值)保证每个线程算出的数组下标不同,即保证每个线程转移不同范围的元素到新数组
                   


        

总结:ConcurrentHashMap扩容原理:支持多线程扩容,对于每一个线程来说会计算出需要去转移的位置元素(步长范围,最小16),转移过程中会对该位置对象加锁,其他位置可以进行put()不影响,转移完后释放锁,并放入ForwardingNode<K,V>对象到该位置,从右往左继续往前进行转移。对于当前线程如果把这个区域的元素都转移完了,就会去看是不是还有其他区域需要帮助转移,有就继续,没有就退出。等待其他线程扩容完成。最后把新数组赋值给table。
 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值