zoukankan      html  css  js  c++  java
  • jdk1.8 ConcurrentHashMap 源码学习

    上次讲到HashMap,但是HashMap并不是线程安全的,那么有哪些线程安全的Map或者是实现线程安全的map呢?

    1、 HashTable(已弃用),使用的是内置对象锁对map进行同步,并发执行的效率比较低(key和value均不能为null,因为这是用在多线程的,当get返回null时,无法确定是不包含这个key还是值为null,hashMap允许key为null,因为运行在单线程,可以通过containKey来判断是否存在key,而containKey在多线程中可能刚好在你调用之前remove了当前key导致当前key为null);

    2、 ConcurrentHashMap(流行使用),JDK1.7使用分段锁,JDK1.8使用的是CAS+synchronized实现并发访问(key和value均不能为null);

    3、使用Collections的synchronizedMap(Map m) 进行包装,使用的是传入m的内置锁,同样并发执行效率低。

    下面记录一下JDK1.8的ConcurrentHashMap的源码学习,花了挺长时间,比hashmap难多了

    属性

      下面只列出了一些属性

    // 保存键值对总数
    private transient volatile long baseCount;
    
    /* 默认为0,用来控制table的初始化和扩容操作, 小于0时代表正在扩容,并且 -n表示
    * 有n – 1个线程在扩容,正数代表table容量
    */
    private transient volatile int sizeCtl;
    static final int MOVED     = -1; // hash for forwarding nodes,forwarding nodes 是扩容时用到的node
    static final int TREEBIN   = -2; // hash for roots of trees
    static final int RESERVED  = -3; // hash for transient 

    构造方法

    // 带容量的初始化,只是初始化了容量,并没有建立桶数组
    public ConcurrentHashMap(int initialCapacity) {
            if (initialCapacity < 0)
                throw new IllegalArgumentException();
            int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                       MAXIMUM_CAPACITY :
                       tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
            this.sizeCtl = cap;
    }

    这里只列出了带容量的构造方法,还有其他的没列出了。

    put()方法

    public V put(K key, V value) {
            return putVal(key, value, false);
    }
    // onlyIfAbsent 为 true的话表示若当前key-value不存在,进行插入;若存在,不对当前存在key-value进行更新
    final V putVal(K key, V value, boolean onlyIfAbsent) {
            // key 和value不能为null
            if (key == null || value == null) throw new NullPointerException();
                //计算key的哈希值
            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)
                        // 初始化表数组(或者叫桶数组,默认容量为16),和hashMap一样,table是延迟加载的,initTable()通过CAS机制实现同步,稍后会讲(看到这可以先到后面看看initTable())
                    tab = initTable();
                        
                else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {                    
                  // 通过CAS机制进行并发put,
                  /*
                   *param1 :桶数组
                   *param2 :节点(node)位置偏移
                   *param3 :节点当前预期内存值
                   *param4 :要在当前节点内更新的值
                  */
                    if (casTabAt(tab, i, null,
                                 new Node<K,V>(hash, key, value, null)))
                        break;                   // no lock when adding to empty bin
                }
                    // 常量 MOVED = -1,表示正在扩容
                else if ((fh = f.hash) == MOVED)
           //内置锁是加在每个桶上的,扩容实际上是对每个桶上的元素重新分配桶,扩容可以在不同的桶上多线程并发执行
                    tab = helpTransfer(tab, f);
                else {
                    V oldVal = null;
            // 对桶 f 进行同步put操作
                    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;
                                    }
                                }
                            }
                                // f < 0 
                            else if (f instanceof TreeBin) {
                                Node<K,V> p;
                           // 赋值为2的意义只是为了不等于0?不太清楚,addCount(1L, binCount)看到如果在桶数组不为空时,binCount <= 1会直接返回,是这个原因?
                                binCount = 2;
                   // 红黑树节点,加入红黑树中
                                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);
                        if (oldVal != null)
                            return oldVal;
                        break;
                    }
                }
            }
            // 数量+1,并进行是否扩容判断
            addCount(1L, binCount);
            return null;
        }
    View Code

    initTable() 方法

    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)) {
                  //若CAS成功 把当前对象SIZECTL偏移位置修改为-1,即sizeConrol = -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;
                // 用减法来替代 *0.75,计算机中乘法的操作比减法耗时
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                    break;
                }
            }
            return tab;
        }
    View Code

    addCount()方法

    private final void addCount(long x, int check) {
         // counterCells 在 currentMap 初始化和 put 过程都没有进行初始化,本人暂时也不知道这是用来干嘛的,doc注释为Table of counter cells. When non-null, size is a power of 2.
            CounterCell[] as; long b, s;
            // counterCells为null,不进入if语句
        if ((as = counterCells) != null ||
                !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
                CounterCell a; long v; int m;
                boolean uncontended = true;
                if (as == null || (m = as.length - 1) < 0 ||
                    (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                    !(uncontended =
                      U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                    fullAddCount(x, uncontended);
                    return;
                }
                if (check <= 1)
                    return;
                s = sumCount();
            }
        // 尝试扩容
            if (check >= 0) {
                Node<K,V>[] tab, nt; int n, sc;
                while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                       (n = tab.length) < MAXIMUM_CAPACITY) {
                /* resizeStamp(n){ // RESIZE_STAMP_BITS = 16
                 *return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
                 *}
                 */
                    int rs = resizeStamp(n);
                    if (sc < 0) {
                    // RESIZE_STAMP_SHIFT = 16 ,下面的if语句判断条件是jdk的bug,在网上查资料一大佬提到 oracle bug库链接:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8214427
                    //下列的解释参考自:https://www.jianshu.com/p/749d1b8db066
                    // 如果 sc 的低 16 位不等于 标识符(校验异常 sizeCtl 变化了)
                    // 如果 sc == 标识符 + 1 (扩容结束了,不再有线程进行扩容)(默认第一个线程设置 sc ==rs 左移 16 位 + 2,当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1)
                    // 如果 sc == 标识符 + 65535(帮助线程数已经达到最大)
                    // 如果 nextTable == null(结束扩容了)
                    // 如果 transferIndex <= 0 (转移状态变化了)
                    // 结束循环
                        if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                            sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                            transferIndex <= 0)
                            break;
                        if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                            transfer(tab, nt); // 线程加入协同扩容
                    }
                    else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
                        transfer(tab, null); //扩容
                    s = sumCount();
                }
            }
        }
    View Code

    transfer()方法

      暂时没有认真研读,了解大概过程,这里先给参考链接,下次再仔细研读一下:

      https://www.jianshu.com/p/aaf769fdbd20

    总结

      相比 hashMap ,concurrentHashMap复杂很多,在处理并发安全的问题上,ConcurrenthahMap用到了 CAS + syschroynized,CAS这就要求了要了解java的内存模型,计算机的底层,所以在这些上面花了一部分时间,对 synchronized现在我也还是一知半解,要去啃啃源码,下次博客就要记录synchronized 和 lock、红黑树、线程池原理等等许多java知识,任重而道远。。。

    参考链接

    https://www.jianshu.com/p/c0642afe03e0

    https://www.jianshu.com/p/aaf769fdbd20

  • 相关阅读:
    关于unbox.any castclass ldobj
    SQL 语句 之 增删改查 (一)
    .NET(C#):使用SmtpClient发送带有图片和附件的电子邮件
    Ext.Net 1.2.0_演示 Ext.Net+QRCode 封装条形码控件
    局域网共享打印机(不需要密码)
    Windows 2003单用户单会话登录远程桌面
    不过如此
    CellMerge
    Windows Server 2008服务器支持iso文件下载的方法
    SQL2008 Express 无法打开备份设备 '‘xxxxx'。出现操作系统错误 5(拒绝访问。)。BACKUP DATABASE 正在异常终止。
  • 原文地址:https://www.cnblogs.com/X-huang/p/10847507.html
Copyright © 2011-2022 走看看