java基础之---concurrentHashMap分析

一,前言

大家好,我是小墨,be foolish,be hungry。本篇文章主要写concurrentHashmap相关知识,欢迎大家多多斧正,如果觉得小墨我写得用心的话,可以点个赞啊。

我之前写过一篇 hashmap源码分析,hashmap无法处理多线程并发的情况,而古老的hashtable采用内置锁syncronized则影响并发速度,一次只能允许一个线程去访问,对于concurrentHashMap则采用锁分段技术,我们看下concurrentHashMap的数据结构,通过锁各个node节点来实现锁分段。
在这里插入图片描述

二,代码分析

我们先关注这个table,作为节点数组,可以用于放入各个node数据

  /**
     * The array of bins. Lazily initialized upon first insertion.
     * Size is always a power of two. Accessed directly by iterators.
     */
    transient volatile Node<K,V>[] table;

1.初始化

1.1 sizeCtl

我们看其中一个构造器。
重点关注一个参数sizeCtl,该值作为同步多个线程的共享变量,这里先初步了解。后面再深入代码理解,分几种情况

  1. 为负数,-1 表示正在初始化,如果为-N 则表示当前正有 N-1 个线程进行扩容操作,

  2. 为正数,如果当前数组为 null 的话表示 table 在初始化过程中,sizeCtl 表示为需要新建数组的长度;若已经初始化了,表示当前数据容器(table 数组)可用容量也可以理解成临界值(插入节点数超过了该临界值就需要扩容),具体指为数组的长度 n 乘以 加载因子 loadFactor;当值为 0 时,即数组长度为默认初始值。

private transient volatile int sizeCtl;

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;
    }

1.2 initTable()

  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<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
                ................
  }

concurrentHashMap的初始化函数是在put方法中的initTable()方法。我们一定要注意到在多线程抢占中要注意如果当前操作非原子操作,那么一定得注意当前使用来判断的状态是否过期,在initTable这段代码就用到了双重判断
((tab = table) == null || tab.length == 0)
1,先在一个while循环中进行cas操作,如果判断当前节点数组为空,进入
2,判断sizeCtl是否为负数,是的话说明其他线程正在初始化,让出当前cpu时间,等待调度。
3,sizeCtl不为负数,cas原子操作判断是否能写sizeCtl为-1,不行的话说明被其他线程先抢占
4,能够写入-1,说明抢占成功,再判断一遍是否table为null,因为存在可能前一个线程正在初始化table但是还没有成功初始化,也就是table依然还为null,而有一个线程发现table为null他就会进行竞争sizeCtl以进行table初始化,但是当前线程在完成初始化之后,那个试图初始化table的线程获得了sizeCtl,但是此时table已经被初始化了,所以,如果没有再次判断的话,可能会将之后进行put操作的线程的更新覆盖掉



    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            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);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

2.put操作

我们看下面代码,分几步:
1,重点:我们注意到concurrentHashMap是不允许key或者value为空的,与hashMap有点不同,所以可以我们get操作如果为空就可以判断没有这个key值
2,获取散列后的hash值
3,节点数组如果为空,初始化
4,获取数组上的节点,为空,设置新节点放入节点数组中
5,如果发现当前在进行扩容,(fh = f.hash) == MOVED,该线程将帮助转移自己节点的数据
6,synchronized (f),这时分段锁的基础,f是数组上的节点。再进行第二次判断是否节点数组对应位置是该节点,tabAt(tab, i) == f,我们发现使用的是等于号,说明这个方法是直接去内存获取这个值,而非获取引用。
7,如果是链表节点(fh>0),则得到的结点就是 hash 值相同的节点组成的链表的头节点。需要依次向后遍历确定这个新加入的值所在位置。如果遇到 key 相同的节点,则只需要覆盖该结点的 value 值即可。否则依次向后遍历,直到链表尾插入这个结点,这样的话binCount的计算就很准确,用于后面获取size(),扩容,
8,如果这个节点的类型是 TreeBin 的话,直接调用红黑树的插入方法进行插入新的节点
9,插入完节点之后再次检查链表长度,如果长度大于 8,就把这个链表转换成红黑树
if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i);
10,addCount(1L, binCount);
对当前容量大小进行检查,如果超过了临界值(实际大小*加载因子)就需要扩容

  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);
    }
 public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    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<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            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
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> 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<K,V> pred = e;
                                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;
                            if ((p = ((TreeBin<K,V>)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;
    }

3.get操作

代码如下,分几步:
1,先看当前的 hash 桶数组节点不为空(e = tabAt(tab, (n - 1) & h)) != null),接着判断两个条件1)hash值是否相同,2)key值是否相同,都相同则说明该节点是查找的节点返回。
2,eh < 0,节点的 hash 值是否为小于 0,如果小于 0 则为树节点。如果是树节点在
3,为链表结构,向后遍历找到节点

  public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> 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;
    }

4,transfer扩容方法

扩容方法是一个比较复杂的方法,我在下面贴出的代码去掉了
类似hashMap的将节点数据按照hash值分配节点数据到i,i + n两个位置的代码,只留下更重点的代码。

   /**
     * Moves and/or copies the nodes in each bin to new table. See
     * above for explanation.
     */
    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        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;
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        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;
                }
                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);
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                     .......
                    }
                }
            }
        }
    }

扩容代码主要分这几部分:

  1. Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];,创建一个节点数据容量为原来两倍,单线程,为什么称为单线程?是因为我们直到之前分析过有个helpTransfer方法,其他线程会迁移自己的节点数据,但是这个创建新节点数组只能是单线程
  2. 接下来我们重点关注如何多线程合作一起进行移动数据,
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    else if ((f = tabAt(tab, i)) == null)
    advance = casTabAt(tab, i, null, fwd);
    else if ((fh = f.hash) == MOVED)
    advance = true; // already processed
    以上是并发处理的核心,使用ForwardingNode这个数据结构去协调多线程之间的工作,允许其他线程去helpTransfer()工作
         

     * Helps transfer if a resize is in progress.
     */
    final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            int rs = resizeStamp(tab.length);
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }

4.计算size

对于并发多线程其实concurrentHashMap并不能提供一个准确的统计数据,对于这个size方法不建议使用

三,1.8与1.7版本对比

这部分是借鉴其他人的,用于抛砖引玉吧:

1、整体结构
1.7:Segment + HashEntry + Unsafe
1.8: 移除Segment,使锁的粒度更小,Synchronized + CAS + Node + Unsafe

2、put()
1.7:先定位Segment,再定位桶,put全程加锁,没有获取锁的线程提前找桶的位置,并最多自旋64次获取锁,超过则挂起。

1.8:由于移除了Segment,类似HashMap,可以直接定位到桶,拿到first节点后进行判断,1、为空则CAS插入;2、为-1则说明在扩容,则跟着一起扩容;3、else则加锁put(类似1.7)

3、get()
基本类似,由于value声明为volatile,保证了修改的可见性,因此不需要加锁。

4、resize()
1.7:跟HashMap步骤一样,只不过是搬到单线程中执行,避免了HashMap在1.7中扩容时死循环的问题,保证线程安全。

1.8:支持并发扩容,HashMap扩容在1.8中由头插改为尾插(为了避免死循环问题),ConcurrentHashmap也是,迁移也是从尾部开始,扩容前在桶的头部放置一个hash值为-1的节点,这样别的线程访问时就能判断是否该桶已经被其他线程处理过了。

参考文章
并发容器之ConcurrentHashMap(JDK 1.8版本)
Java 8 ConcurrentHashMap源码分析
面试题:ConcurrentHashMap 1.7和1.8的区别

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值