ConcurrentHashMap底层原理

5.6 ConcurrentHashMap底层原理

5.6.1 jdk1.7

5.6.1.1 数组结构

数据结构是数组+segment对象,采用segment分段锁和CAS保证并发。

JDK1.7中的ConcurrentHashMap是由 segment数组结构和 HashEntry 数组结构组成,即
ConcurrentHashMap把哈希桶切分成小数组(Segment ),每个小数组有n个 HashEntry组成。其中,Segment继承了ReentrantLock,所以Segment是一种可重入锁,扮演锁的角色;HashEntry用于存储键值对数据。

5.6.1.2 put操作流程

 /**
         * ConcurrentHashMap
         */
        ConcurrentHashMap<String,String> conMap = new ConcurrentHashMap<String, String>();
        conMap.put("k","v");
        System.out.println(conMap.get("k"));

  1. 构造方法

    private static final int DEFAULT_CAPACITY = 16;//segment数组的长度
    private static final float DEFAULT_LOAD_FACTOR = 0.75f; //负载因子(与扩容有关)
    DEFAULT_CONCURRENCY_LEVEL; //总entry的数目
    
    假设new ConcurrentHashMap(16,0.75f,16)
    说明segment数组大小为16,而每个segment对象内部的数组大小为1(16/16),表示只存一个entry。(默认最低2个)
    
    假设new ConcurrentHashMap(8,0.75f,16)
    说明segment数组大小为8,而每个segment对象内部的数组大小为2(16/8),表示存2个entry。
    
    

    传入的concurrencyLevel
    8---->8
    9---->16
    17---->32
    保证是大于等于concurrencyLevel的2的幂次
    
    
  2. 获取segment对象

    1.第一个if是判断当前位置是否有segment对象,没有则往下执行。
    2.获取segment数组的第一个位置的segment对象的信息。
    3.假设此时有多个线程执行到,这里的if就是判断是否有别的线程执行到了。
    4.各线程生成自己的segment对象。
    5.if之后进入while循环,如果当前位置没有segment对象,则执行cas指令,将自己生成的segment对象s赋给ss大数组的第u个位置。(cas:当第u个位置为null时,处理器会将null更新为seg,处理过程不会呗别的线程中断)
        
    补充:
        CAS指令需要有三个操作数,分别是内存位置V,旧的预期值A,准备设置的新值B。CAS指令执行时,当且仅当V符合A时,处理器才会用B更新V的值,否者它就不执行更新。但是,不管是否更新了V的值,都会返回V的旧值,上诉的处理过程是一个原子操作,执行期间不会被其他线程中断。
    
  3. 向segment对象里的小数组加入entry对象

1.第一个框:
	trylock():非阻塞加锁
	lock():阻塞加锁
	如果调用trylock()加锁失败,则继续调用scanAndLockForPut()加锁,直到加到锁。scanAndLockForPut()核心代码是while(!trylock()){},主要工作就是一直加锁,直到加到锁。
	
2.第二个框:遍历segment里面的数组的某个位置的链表
	first:获取链表第一个节点
	如果遇到相同的key,则覆盖value,否则生成一个entry对象,加入。

5.6.2 jdk1.8

jdk1.8 的ConcurrentHashMap舍弃了1.7的segment分段锁,采用了CAS+sycronized来并发。
数据结构和1.8的HashMap一样,是数组+链表+红黑树。
将锁的级别控制在了更细粒度的哈希桶元素级别,也就是说只需要锁住这个链表头结点(红黑树的根节点),就不会影响其他的哈希桶元素的读写,大大提高了并发度。

5.6.2.1 计算hash值

static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS; //HASH_BITS=0x7fffffff
    }
除了高16位和低16位或操作之外,最后还和HASH_BITS相与,其值为0x7fffffff,表示int的最大值01111111...。它的作用主要是使hash值为正数。在ConcurrentHashMap中,Hash值为负数有特别的意义,如-1表示ForwardingNode结点,-2表示TreeBin结点。


    static final int MOVED     = -1; // 表示正在扩容
    static final int TREEBIN   = -2; // 树的根节点
    static final int RESERVED  = -3; // 临时保留的

5.6.2.2 volatile修饰

使用volatile修饰来保证某个变量内存的改变对其他线程即时可见。可以配合CAS实现不加锁对并发操作的支持。
ConcurrentHashMap的get操作可以无锁,正式由于Node的元素val和指针
next是使用volatile修饰的,在多线程环境下,A线程修改节点val或者新增节点对B线程都是即时可见的,保证了数据的一致性。

5.6.2.3 put操作

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) {
    //ConcurrentHashMap 不允许插入null键,HashMap允许插入一个null键
    if (key == null || value == null) throw new NullPointerException();
    //计算key的hash值
    int hash = spread(key.hashCode());//看5.6.1
    int binCount = 0;
    //for循环的作用:因为更新元素是使用CAS机制更新,需要不断的失败重试,直到成功为止。
    for (Node<K,V>[] tab = table;;) {
        // f:链表或红黑二叉树头结点,向链表中添加元素时,需要synchronized获取f的锁。
        Node<K,V> f; int n, i, fh;
        //判断Node[]数组是否初始化,没有则进行初始化操作
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //通过hash定位Node[]数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头结点),添加失败则进入下次循环。
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//看注1
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null))) //看注2
                break;                   // no lock when adding to empty bin
        }
        //检查到内部正在移动元素(Node[] 数组扩容)
        else if ((fh = f.hash) == MOVED)
            //帮助它扩容
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            //锁住链表或红黑二叉树的头结点,保证并发
            synchronized (f) {
                //判断f是否是链表的头结点
                if (tabAt(tab, i) == f) {
                    //如果fh>=0 是链表节点
                    if (fh >= 0) {
                        binCount = 1;//包括头节点,初始化为1
                        //遍历链表所有节点
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //如果节点存在,则更新value
                            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;
                            }
                        }
                    }
                    //TreeBin是红黑二叉树节点
                    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;
            }
        }
    }
    //将当前ConcurrentHashMap的size数量+1
    addCount(1L, binCount);
    return null;
}

注1:

//返回tab数组的第i个元素
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);
    }

注2:

/**
	使用CAS指令,当tab数组的第i个位置是c(null)时,则更新c为v
**/
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }

总结:

1.判断Node数组是否初始化,没有则进行初始化。
2.通过hash & (length-1) 定位索引,判断此位置是否有节点,没有则使用CAS加入,用for进行失败重试机制。有则往下执行。
3.判断数组是否在扩容,是则帮助扩容,否则往下执行。
4.锁住头节点,保证并发,判断此位置是链表还是红黑树,如果是链表,则遍历链表,判断是否有相同key的entry对象,有则覆盖value值,否则插入(尾插法)。若是红黑树,则添加一个TreeBin节点。
5.上述操作完成后,加入是在链表中插入的,则还要判断当前链表节点是否大于8,是则要转化为红黑树。

5.6.3 JDK1.7与JDK1.8的区别

1.数据结构:
	1.7:segment分段锁
    1.8:数组+链表+红黑树    
2.线程安全机制:
	1.7:采用Segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。
    1.8:采用CAS+Synchronized保证线程安全.
3.锁的粒度:
	1.7:对需要进行数据操作的Segment加锁.
    1.8:对每个数组的头节点进行加锁(Node).
4.查询时间复杂度:
	1.7:遍历链表O(n).
    1.8:遍历红黑树O(logn)。

posted @ 2022-10-11 16:39  湘summer  阅读(563)  评论(0编辑  收藏  举报