Java核心——concurrentHashMap

参考

http://www.importnew.com/28263.html

http://ifeve.com/concurrenthashmap/

http://www.importnew.com/26049.html 

https://www.cnblogs.com/yangming1996/p/8031199.html

 

JDK8 concurrentHashMap实现的基本原理

java8摒弃了java7 segment的概念,而是用 Node数组+链表+红黑树实现concurrentHashMap.

采用 synchronized+CAS操作来实现线程安全,看起来是hashMap的加强版。 java8中也有segment,但是是用来兼容旧版本。

JVM模型相关概念简介

原子性,可见性,有序性

Java内存模型是围绕着在并发过程中如何处理原子性,可见性和有序性这3个特征简历的。

原子性:read,load,assign,use,store,write等原子性变量操作,即基本数据类型的访问读写是具备原子性的(long,double除外),是由java内存模型直接保证的。

但是高层次的原子性要用其他方式实现(lock,synchronized等)

可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改,就是可见性。java内存模型通过在变量修改后将新值同步回主存,在读取变量前刷新变量值来实现的。

java的volatile关键字能保证变量的可见性。

另外synchronized(加锁)和final()也可以实现可见性。

有序性:

synchronized关键字

可用来实现线程的原子性,可见性,有序性。

java语言中存在两种内建的synchronized语法:1、synchronized语句;2、synchronized方法。两种方法的实现不同。

同步代码块
monitorenter指令插入到同步代码块的开始位置。monitorexit指令插入到同步代码块结束的位置。JVM需要保证每一个monitorenter都有一个monitorexit与之对应。
任何对象,都有一个monitor与之相关联,当monitor被持有以后,它将处于锁定状态。线程执行到monitorenter指令时,会尝试获得monitor对象的所有权,即尝试获取锁。

同步方法
synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。

volatile关键字

用volatile修饰之后就变得不一样了:

第一:使用volatile关键字会强制将修改的值立即写入主存;

第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

可用来保证可见性和有序性。

CAS原理

如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,返回true。否则处理器不做任何操作,返回false。

比如当前线程比较成功后,准备更新共享变量值的时候,这个共享变量值被其他线程更改了,那么CAS函数必须返回false。

final实现可见性

 

下面就来说java8的concurrentHashMap实现。

首先是内部数据结构,如下

839d7b428cd171d9b54cee51330d6597f68.jpg

主要属性及内部结构

  // table数组最大容量:2^30=1073741824
    private static final int MAXIMUM_CAPACITY = 1 << 30;

    // table默认初始值,必须是2的幕数
    private static final int DEFAULT_CAPACITY = 16;

    //可能的数组最大值
    static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    //并发级别,未使用,只是兼容老版本
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

    
    private static final float LOAD_FACTOR = 0.75f;

    //链表转红黑树的节点个数阈值
    static final int TREEIFY_THRESHOLD = 8;

    //树转链表节点个数阈值
    static final int UNTREEIFY_THRESHOLD = 6;

 
   //树结构化所需的table的最小容量(若一个bin中太多节点,就给table扩容)
  //这个值最少应该是 4 * TREEIFY_THRESHOLD ,才能避免扩容和树结构化阈值的冲突
    static final int MIN_TREEIFY_CAPACITY = 64;

   //每次transfer的最小值。范围被细分以便允许多线程扩容。这个值是作为避免发生超过内存的下界。最小应该是 DEFAULT_CAPACITY,即16
    private static final int MIN_TRANSFER_STRIDE = 16;

   //产生stamp的位数,32位数组中最小应该是6
    private static int RESIZE_STAMP_BITS = 16;

    
   // help resize的最大线程数,2^15-1
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
   
   // sizeCtl中记录size大小的偏移量,32-16 = 16
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

    
    static final int MOVED     = -1; // forwarding 的hash值
    static final int TREEBIN   = -2; // 根节点的hash值
    static final int RESERVED  = -3; // ReservationNode的hash值
    static final int HASH_BITS = 0x7fffffff; // 普通node节点hash的位数

//cpu个数,在某个大小上设置限制
    static final int NCPU = Runtime.getRuntime().availableProcessors();

/*控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义
 *当为负数时:-1代表正在初始化,-N代表有N-1个线程正在 进行扩容
 *当为0时:代表当时的table还没有被初始化
 *当为正数时:表示初始化或者下一次进行扩容的大小
*/
   private transient volatile int sizeCtl;

private transient volatile int sizeCtl;这个属性非常重要,取值如下

  • 0:默认值
  • -1:代表哈希表正在进行初始化
  • 大于0:相当于 HashMap 中的 threshold,表示阈值
  • 小于-1:代表有多个线程正在进行扩容

 

HashMap内部的核心数据结构,数组,其元素为Node类型


    //存储节点(bins)的数组。懒加载。由第一次插入元素进行初始化,数组长度始终未2^n,用迭代器访问
    transient volatile Node<K,V>[] table;

   //第二个table容器,扩容期间为空
    private transient volatile Node<K,V>[] nextTable;

//主要在没有内容的时候使用,也作为初始化期间的返回值。通过CAS策略更新
    private transient volatile long baseCount;

    //见前面注解
    private transient volatile int sizeCtl;

//下一个table的索引(加1)用来在扩容期间切分
    private transient volatile int transferIndex;

//扩容和创建CounterCells期间的自选锁
    private transient volatile int cellsBusy;

    /**
     * Table of counter cells. When non-null, size is a power of 2.
     */,
    private transient volatile CounterCell[] counterCells;

    // views
    private transient KeySetView<K,V> keySet;
    private transient ValuesView<K,V> values;
    private transient EntrySetView<K,V> entrySet;

其中的成员变量transient volatile Node<K,V>[] table对应的类型Node的定义是一个内部类,继承自Map,

也就是HashMap的容器table元素是KV实体(key-value entity),只读。 Node定义如下

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

        ...

        //将用来查找节点
        public final boolean equals(Object o) {
            Object k, v, u; Map.Entry<?,?> e;
            return ((o instanceof Map.Entry) &&
                    (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                    (v = e.getValue()) != null &&
                    (k == key || k.equals(key)) &&
                    (v == (u = val) || v.equals(u)));
        }

        /**
         * Virtualized support for map.get(); overridden in subclasses.
         */
        Node<K,V> find(int h, Object k) {
            Node<K,V> e = this;
            if (k != null) {
                do {
                    K ek;
                    if (e.hash == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                } while ((e = e.next) != null);
            }
            return null;
        }
    }

另外,还TreeNode,继承自Node,只不过实现了红黑树结构。树结构节点用在HashMap元素(Hash冲突的元素表)头节点。当链表的节点数大于8时会转换成红黑树的结构。

static final class TreeNode<K,V> extends Node<K,V>

此外还有TreeBin,TreeBin从字面含义中可以理解为存储树形结构的容器,而树形结构就是指TreeNode,所以TreeBin就是封装TreeNode的容器,它提供转换黑红树的一些条件和锁的控制

put操作

主要流程如下

 final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode()); //首先计算key的hash值
        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(); //发现table容器为null,则初始化容器,这在首次put的时候会发生
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //用hash做下标在容器中查找
                //用CAS算法(无锁)进行线程安全插入(即对比要插入位置是否为null,是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)   //处理扩容时的put(hash为-1)
                tab = helpTransfer(tab, f); //当前正在扩容,则处理扩容
            else {
                //到这里说明要插入的节点存在hash冲突,则用synchronized控制来插入到链表或者红黑树
                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; //参数控制key相同的元素则覆盖value
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null); //key不相同则插入表尾
                                    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) { //若已经满足链表元素>8,则链表转红黑树存储
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

总结一下put操作主要步骤

  1. 计算key的hash值,后面好几个地方要用到
  2. 判断当前的插入动作是不是正在扩容,是的话跳转到扩容逻辑
  3. 尝试插入table,CAS算法保证线程安全
  4. 若存在hash冲突,则尝试插入链表或者红黑树(旋转插入),整个过程用到synchronized代码块保证线程安全
  5. 如果当前已经满足链表节点>8,则将链表转为红黑树
  6. 最后检查是否已经满足扩容条件,是则扩容

 

initTable操作

初始化容器大小,需要配合重要属性 sizeCtl

 private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0) //sizeCtl <0 说明已经有线程正在初始化,则挂起当前线程
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //sc置-1表示开始初始化了,CAS保证线程安全

                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); //记录下次扩容大小,n*0.75
                    }
                } finally {
                    sizeCtl = sc;  //
                }
                break;
            }
        }
        return tab;
    }

扩容操作

扩容操作相对复杂,暂时不分析源码,其大致思路如下

与initTable只允许一个线程操作不同的是,扩容是允许多个线程加入的,而且特意设计成让新线程加入已提升性能。步骤如下:

当前线程检查到当前节点正在扩容时,加入扩容。final Node<K,V>[] helpTransfer()方法就是让线程加入协助扩容的。

每个加入协助扩容的线程会调用transfer方法,这个方法做的事主要是

第一步,计算当前线程能协助的迁移的最少节点数

第二步,加入到while循环去处理属于当前线程能处理的节点迁移

第三步,迁移链表或红黑树

java8中concurrentHashMap扩容的最大特色就是多线程加入协助扩容,而不是只控制并发,将其他线程拒之门外,这是设计精妙之处。

https://www.cnblogs.com/nullzx/p/8647220.html

f8a3ca3310b067b4dabb0313e0c42723f0a.jpg

总结

concurrentHashMap使用的并发控制方式有synchronized和CAS,

之所以用synchronized而不再用ReentrantLock,大概是因为锁的粒度降低了,由原来锁一个区间,变成现在只锁一个头节点。更多细节还需要进一步研究

 

 

 

 

 

 

 

转载于:https://my.oschina.net/u/3300976/blog/3026044

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值