JUC源码解析-ConcurrentHashMap1.8

本文详细解析了Java 1.8中ConcurrentHashMap的设计与实现,包括其如何利用CAS和volatile保证并发安全性,以及在初始化、扩容、并发操作中的关键细节。文章介绍了在并发环境下,ConcurrentHashMap如何通过分散锁和无锁机制提高性能,以及在put、扩容、size计算等操作中的线程安全策略。同时,文章探讨了sizeCtl变量在整个并发控制中的核心作用及其状态转换,以及helpTransfer方法在协助扩容中的作用。
摘要由CSDN通过智能技术生成

前言

1.8后的ConcurrentHashMap与之前有截然不同的设计,之前是分段锁的思想,通过采用分段锁Segment减少热点域来提高并发效率。1.8利用CAS+Synchronized来保证并发更新的安全,底层采用数组+链表+红黑树的存储结构。


在此再一次膜拜Doug Lea大神,高山仰止。1.8的ConcurrentHashMap有6313行代码,之前大概是1000多行。
这篇文章也只是概括了部分功能。

本文多介绍的是并发部分,对于底层的散列表操作,红黑树操作,这在之前分析HashMap的文章时已有详细介绍,ConcurrentHashMap关于这些是与HashMap相通的。

HashMap源码分析

红黑树

HashMap源码解析-红黑树操作

这里先来说一下CAS+volatile的组合,这两个是整个JUC包的基石。volatile 读的内存语义:当读一个volatile变量时,JMM会把线程u敌营的本地内存置为无效,线程接下来将从主内存中读取值。volatile写的内存语义:当写一个volatile变量时,JMM会到主内存中取读值。JSR-133增强volatile内存语义:之前旧的JMM允许volatile变量与普通变量重排序,之后严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile写-读与锁的释放-获取有相同语义。也就是说:写线程A在写这个volatile变量之前所有可见的共享变量的值都将立即变得对读线程B可见。A写一个volatile变量,随后B读到这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。而CAS同时具有volatile读/写的内存语义,以Intel X86来说,就是利用在CMPXCHG指令前添加lock前缀来实现。

volatile的读写和CAS可以实现线程之间的通信,整合到一起就实现了Concurrent包得以实现的基石。在阅读JUC下的类时会发现一个通用的模式:volatile的共享变量,CAS原子更新实现线程同步,二者搭配来实现线程之间的通信。很多操作都是先读volatile变量此时的最新值,赋给局部变量,然后一顿操作,最后CAS进行同步,若过程中共享变量被其它线程更改则会导致CAS失败,重新尝试。

相关概念

table

所有数据都被存放在table数组中,大小是2的整数次幂,存储的元素分为三种类型

  • TreeBin 用于包装红黑树结构的结点类型 ,它继承了Node,代表它也是个节点,hash值为-2
  • ForwardingNode 扩容时存放的结点类型,并发扩容的实现关键之一 ,是一个标记,代表此处已完成扩容,hash值为-1。
  • Node 普通结点类型

     


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

value和next都用volatile修饰,保证并发的可见性
ForwardingNode

    static final class ForwardingNode<K,V> extends Node<K,V> {
        final Node<K,V>[] nextTable;
        ForwardingNode(Node<K,V>[] tab) {
            super(MOVED, null, null, null);
            this.nextTable = tab;
        }

ForwardingNode作用在扩容期间,hash值为MOVED 值为-1,当table[ i ] 是个ForwardingNode节点时,代表该位置节点已经移至新数组。

nextTable

扩容时新生成的数组,其大小为原数组的两倍

sizeCtl

    /**
     * Table initialization and resizing control.  When negative, the
     * table is being initialized or resized: -1 for initialization,
     * else -(1 + the number of active resizing threads).  Otherwise,
     * when table is null, holds the initial table size to use upon
     * creation, or 0 for default. After initialization, holds the
     * next element count value upon which to resize the table.
     */
    private transient volatile int sizeCtl;

用于table数组初始化扩容控制,下面来看看它是如何控制的:


接下来的思路是沿着sizeCtl来跟踪源码,但是很多方法具有多种功能,比如addCount...会牵扯很多其它的概念,这里就把他们先剔除出去,先沿着一条简单的线来解读


sizeCtl在初始化与扩容中的作用

1,初始化:ConcurrentHashMap有五个构造器,不考虑构造时指定集合的,其他四个都没有在初始化期间创建table数组对象,而是将这一操作下放到第一次调用put插入键值对时。sizeCtl决定了table数组的大小,无参构造器则sizeCtl为默认值0,若传入了初始值大小,经过tableSizeFor将sizeCtl改为比传入值大的最小2的n次幂,如传15返回16,17返回32。

    final V putVal(K key, V value, boolean onlyIfAbsent) {
.......省略
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
......省略

initTable会被调用:这里利用 循环CAS 确保只有一个线程能够初始化table数组

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

读取voaltile的sizeCtl 值赋给局部变量 sc = sizeCtl,之后当一个线程cas设置sizeCtl值为-1成功,之后的线程都将被拒绝,通过执行Thread.yield()。也就是说只允许一个线程执行初始化table 数组操作。sc == 0则table大小为16,否则就为sizeCtl大小的值。数组创建完成后sizeCtl = n - (n >>> 2),相当于原先值的0.75,这之后sizeCtl代表阀值

2,接下来看看它在扩容上的控制: 扩容-是transfer方法

先从put入手

put

    public V put(K key, V value) {
        return putVal(key, value, false);
    }

    // onlyIfAbsent为true:相当于putIfAbsent,即key存在就不更换value
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        // key与value都不能为null
        if (key == null || value == null) throw new NullPointerException();
        //(h ^ (h >>> 16)) & HASH_BITS;//0x7fffffff;保证了hash >= 0.
        int hash = spread(key.hashCode()); //计算出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)
 // 初始化table数组操作,若sizeCtl为0则table大小为16,否则为sizeCtl大小
                tab = initTable();
//通过hash得到数组下标,若该位置为null,新建节点放在该位置上
            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
            }
//MOVED为-1,代表该位置的头节点为forwarding nodes,表明该位置正在进行扩容
//helpTransfer,让该线程帮助进行扩容操作。
            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,
         
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值