解析ConcurrentHashMap:成员属性、内部类、构造方法分析

1、简介

ConcurrentHashMap是HashMap的线程安全版本,内部也是使用(数组 + 链表 + 红黑树)的结构来存储元素。相比于同样线程安全的HashTable来说,效率等各方面都有极大地提高。

在学习ConcurrentHashMap源码之前,这里默认大家已经读过HashMap源码,了解LongAdder原子类、红黑树。先简单介绍下

ConcurrentHashMap的整体流程:

整体流程跟HashMap比较类似,大致是以下几步:

(1)如果桶数组未初始化,则初始化;

(2)如果待插入的元素所在的桶为空,则尝试把此元素直接插入到桶的第一个位置;

(3)如果正在扩容,则当前线程一起加入到扩容的过程中;

(4)如果待插入的元素所在的桶不为空且不在迁移元素,则锁住这个桶(分段锁);

(5)如果当前桶中元素以链表方式存储,则在链表中寻找该元素或者插入元素;

(6)如果当前桶中元素以红黑树方式存储,则在红黑树中寻找该元素或者插入元素;

(7)如果元素存在,则返回旧值;

(8)如果元素不存在,整个Map的元素个数加1,并检查是否需要扩容;

添加元素操作中使用的锁主要有(自旋锁 + CAS + synchronized + 分段锁)。

为什么使用synchronized而不是ReentrantLock?

因为synchronized已经得到了极大地优化,在特定情况下并不比ReentrantLock差。

2、JDK1.8 ConcurrentHashMap结构图

3、成员属性

// 散列表数组最大容量值
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 散列表默认容量值16
private static final int DEFAULT_CAPACITY = 16;
// 最大的数组大小(非2的幂) toArray和相关方法需要(并不是核心属性)
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// jdk1.7遗留下来的,用来表示并发级别的属性
// jdk1.8只有在初始化的时候用到,不再表示并发级别了~ 1.8以后并发级别由散列表长度决定
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 负载因子:表示散列表的填满程度~ 在ConcurrentHashMap中,该属性是固定值0.75,不可修改~
private static final float LOAD_FACTOR = 0.75f;
// 树化阈值:散列表的一个桶中链表长度达到8时候,可能发生链表树化
static final int TREEIFY_THRESHOLD = 8;
// 反树化阈值:散列表的一个桶中的红黑树元素个数小于6时候,将红黑树转换回链表结构
static final int UNTREEIFY_THRESHOLD = 6;
// 散列表长度达到64,且某个桶位中的链表长度达到8,才会发生树化
static final int MIN_TREEIFY_CAPACITY = 64;
// 控制线程迁移数据的最小步长(桶位的跨度~)
private static final int MIN_TRANSFER_STRIDE = 16;
// 固定值16,与扩容相关,计算扩容时会根据该属性值生成一个扩容标识戳
private static int RESIZE_STAMP_BITS = 16;
// (1 << (32 - RESIZE_STAMP_BITS)) - 1 = 65535:1 << 16 -1
// 表示并发扩容最多容纳的线程数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 也是扩容相关属性,在扩容分析的时候会用到~
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// 当node节点的hash值为-1:表示当前节点是FWD(forwarding)节点(已经被迁移的节点)
static final int MOVED     = -1;
// 当node节点的hash值为-2:表示当前节点已经树化,且当前节点为TreeBin对象~,TreeBin对象代理操作红黑树
static final int TREEBIN   = -2;
// 当node节点的hash值为-3:
static final int RESERVED  = -3;
// 0x7fffffff 十六进制转二进制值为:1111111111111111111111111111111(31个1)
// 作用是将一个二进制负数与1111111111111111111111111111111 进行按位与(&)运算时,会得到一个正数,但不是取绝对值
static final int HASH_BITS = 0x7fffffff;
// 当前系统的CPU数量
static final int NCPU = Runtime.getRuntime().availableProcessors();
// JDK1.8 序列化为了兼容 JDK1.7的ConcurrentHashMap用到的属性 (非核心属性)
private static final ObjectStreamField[] serialPersistentFields = {
    new ObjectStreamField("segments", Segment[].class),
    new ObjectStreamField("segmentMask", Integer.TYPE),
    new ObjectStreamField("segmentShift", Integer.TYPE)
};
// 散列表table
transient volatile Node<K,V>[] table;
// 新表的引用:扩容过程中,会将扩容中的新table赋值给nextTable,(保持引用),扩容结束之后,这里就会被设置为NULL
private transient volatile Node<K,V>[] nextTable;
// 与LongAdder中的baseCount作用相同: 当未发生线程竞争或当前LongAdder处于加锁状态时,增量会被累加到baseCount
private transient volatile long baseCount;
// 表示散列表table的状态:
// sizeCtl<0时:
// 情况一、sizeCtl=-1: 表示当前table正在进行初始化(即,有线程在创建table数组),当前线程需要自旋等待...
// 情况二、表示当前table散列表正在进行扩容,高16位表示扩容的标识戳,低16位表示扩容线程数:(1 + nThread) 即,当前参与并发扩容的线程数量。
// sizeCtl=0时:表示创建table散列表时,使用默认初始容量DEFAULT_CAPACITY=16
// sizeCtl>0时:
// 情况一、如果table未初始化,表示初始化大小
// 情况二、如果table已经初始化,表示下次扩容时,触发条件(阈值)
private transient volatile int sizeCtl;
// 扩容过程中,记录当前进度。所有的线程都需要从transferIndex中分配区间任务,并去执行自己的任务
private transient volatile int transferIndex;
// LongAdder中,cellsBusy表示对象的加锁状态:
// 0: 表示当前LongAdder对象处于无锁状态
// 1: 表示当前LongAdder对象处于加锁状态
private transient volatile int cellsBusy;
// LongAdder中的cells数组,当baseCount发生线程竞争后,会创建cells数组,
// 线程会通过计算hash值,去取到自己的cell,将增量累加到指定的cell中
// 总数 = sum(cells) + baseCount
private transient volatile CounterCell[] counterCells;

4、静态属性

// Unsafe 类
private static final sun.misc.Unsafe U;
// 表示sizeCtl属性在ConcurrentHashMap中内存的偏移地址
private static final long SIZECTL;
// 表示transferIndex属性在ConcurrentHashMap中内存的偏移地址
private static final long TRANSFERINDEX;
// 表示baseCount属性在ConcurrentHashMap中内存的偏移地址
private static final long BASECOUNT;
// 表示cellsBusy属性在ConcurrentHashMap中内存的偏移地址
private static final long CELLSBUSY;
// 表示cellsValue属性在ConcurrentHashMap中内存的偏移地址
private static final long CELLVALUE;
// 表示数组第一个元素的偏移地址
private static final long ABASE;
// 该属性用于数组寻址,请继续往下阅读
private static final int ASHIFT;

5、静态代码块

static {
    try {
        U = sun.misc.Unsafe.getUnsafe();
        Class<?> k = ConcurrentHashMap.class;
        SIZECTL = U.objectFieldOffset
            (k.getDeclaredField("sizeCtl"));
        TRANSFERINDEX = U.objectFieldOffset
            (k.getDeclaredField("transferIndex"));
        BASECOUNT = U.objectFieldOffset
            (k.getDeclaredField("baseCount"));
        CELLSBUSY = U.objectFieldOffset
            (k.getDeclaredField("cellsBusy"));
        Class<?> ck = CounterCell.class;
        CELLVALUE = U.objectFieldOffset
            (ck.getDeclaredField("value"));
        Class<?> ak = Node[].class;
        // 拿到数组第一个元素的偏移地址
        ABASE = U.arrayBaseOffset(ak);
        // 表示数组中每一个单元所占用的空间大小,即scale表示Node[]数组中每一个单元所占用的空间
        int scale = U.arrayIndexScale(ak);
        // (scale & (scale - 1)) != 0:判断scale的数值是否是2的次幂数
        // java语言规范中,要求数组中计算出的scale必须为2的次幂数
        // 1 0000 % 0 1111 = 0
        if ((scale & (scale - 1)) != 0)
            throw new Error("data type scale not a power of two");
        // numberOfLeadingZeros(scale) 根据scale,返回当前数值转换为二进制后,从高位到地位开始统计,统计有多少个0连续在一块:eg, 8转换二进制=>1000 则 numberOfLeadingZeros(8)的结果就是28,为什么呢?因为Integer是32位,1000占4位,那么前面就有32-4个0,即连续最长的0的个数为28个
        // 4转换二进制=>100 则 numberOfLeadingZeros(8)的结果就是29
        // ASHIFT = 31 - Integer.numberOfLeadingZeros(4) = 2 那么ASHIFT的作用是什么呢?其实它有数组寻址的一个作用:
        // 拿到下标为5的Node[]数组元素的偏移地址(存储地址):假设此时 根据scale计算得到的ASHIFT = 2
        // ABASE + (5 << ASHIFT) == ABASE + (5 << 2) == ABASE + 5 * scale,就得到了下标为5的数组元素的偏移地址
        ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
    } catch (Exception e) {
        throw new Error(e);
    }
}

6、内部类

6.1 Node节点

static class Node<K,V> implements Map.Entry<K,V> {
    // hash值
    final int hash;
    // key
    final K key;
    // value
    volatile V val;
    // 后驱节点
    volatile Node<K,V> next;
    Node(int hash, K key, V val, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.val = val;
        this.next = next;
    }
    public final K getKey()       { return key; }
    public final V getValue()     { return val; }
    public final int hashCode()   { return key.hashCode() ^ val.hashCode();
    public final String toString(){ return key + "=" + val; }
    public final V setValue(V value) {
        throw new UnsupportedOperationException();
    }
    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;
    }
}

6.2 ForwardingNode节点

这个内部类在之后分析扩容的文章中会再仔细去探究,这里先熟悉一下~

// 如果是一个写的线程(eg:并发扩容线程),则需要为创建新表贡献一份力
// 如果是一个读的线程,则调用该内部类的find(int h, Object k)方法
static final class ForwardingNode<K,V> extends Node<K,V> {
    // nextTable表示新散列表的引用
    final Node<K,V>[] nextTable;
    ForwardingNode(Node<K,V>[] tab) {
        super(MOVED, null, null, null);
        this.nextTable = tab;
    }
    // 到新表上去读数据
    Node<K,V> find(int h, Object k) {
        // loop to avoid arbitrarily deep recursion on forwarding nodes
        outer: for (Node<K,V>[] tab = nextTable;;) {
            Node<K,V> e; int n;
            if (k == null || tab == null || (n = tab.length) == 0 ||
                (e = tabAt(tab, (n - 1) & h)) == null)
                return null;
            for (;;) {
                int eh; K ek;
                if ((eh = e.hash) == h &&
                    ((ek = e.key) == k || (ek != null && k.equals(ek))))
                    return e;
                if (eh < 0) {
                    if (e instanceof ForwardingNode) {
                        tab = ((ForwardingNode<K,V>)e).nextTable;
                        continue outer;
                    }
                    else
                        return e.find(h, k);
                }
                if ((e = e.next) == null)
                    return null;
            }
        }
    }
}

6.3 TreeNode节点

TreeBin中需要用到该节点,之后会细说~

static final class TreeNode<K,V> extends Node<K,V> {
    // 父节点
    TreeNode<K,V> parent;  // red-black tree links
    // 左子节点
    TreeNode<K,V> left;
    // 右节点
    TreeNode<K,V> right;
    // 前驱节点
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    // 节点有红、黑两种颜色~
    boolean red;
    TreeNode(int hash, K key, V val, Node<K,V> next,
             TreeNode<K,V> parent) {
        super(hash, key, val, next);
        this.parent = parent;
    }
    Node<K,V> find(int h, Object k) {
        return findTreeNode(h, k, null);
    }
    /**
     * Returns the TreeNode (or null if not found) for the given key
     * starting at given root.
     */
    final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {
        if (k != null) {
            TreeNode<K,V> p = this;
            do  {
                int ph, dir; K pk; TreeNode<K,V> q;
                TreeNode<K,V> pl = p.left, pr = p.right;
                if ((ph = p.hash) > h)
                    p = pl;
                else if (ph < h)
                    p = pr;
                else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
                    return p;
                else if (pl == null)
                    p = pr;
                else if (pr == null)
                    p = pl;
                else if ((kc != null ||
                          (kc = comparableClassFor(k)) != null) &&
                         (dir = compareComparables(kc, k, pk)) != 0)
                    p = (dir < 0) ? pl : pr;
                else if ((q = pr.findTreeNode(h, k, kc)) != null)
                    return q;
                else
                    p = pl;
            } while (p != null);
        }
        return null;
    }
}

7、构造方法

public ConcurrentHashMap() {
}
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;
}
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    this.sizeCtl = DEFAULT_CAPACITY;
    putAll(m);
}
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, 1);
}
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;
}

构造方法与HashMap对比可以发现,没有了HashMap中的thresholdloadFactor,而是改用了sizeCtl来控制,而且只存储了容量在里面,那么它是怎么用的呢?官方给出的解释如下:

(1)-1,表示有线程正在进行初始化操作。

(2)-(1 + nThreads),表示有n个线程正在一起扩容。

(3)0,默认值,后续在真正初始化的时候使用默认容量。

(4)> 0,初始化或扩容完成后下一次的扩容门槛 。

8、总结

文章会不定时更新,有时候一天多更新几篇,如果帮助您复习巩固了知识点,后续会亿点点的更新!希望大家多多关注我们的其他内容!

(0)

相关推荐

  • Java源码解析之ConcurrentHashMap

    早期 ConcurrentHashMap,其实现是基于: 分离锁,也就是将内部进行分段(Segment),里面则是 HashEntry 的数组,和 HashMap 类似,哈希相同的条目也是以链表形式存放. HashEntry 内部使用 volatile 的 value 字段来保证可见性,也利用了不可变对象的机制以改进利用 Unsafe 提供的底层能力,比如 volatile access,去直接完成部分操作,以最优化性能,毕竟 Unsafe 中的很多操作都是 JVM intrinsic 优化过的

  • Java7和Java8中的ConcurrentHashMap原理解析

    Java7 中 ConcurrentHashMap ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些. 整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表"部分"或"一段"的意思,所以很多地方都会将其描述为分段锁.注意,行文中,我很多地方用了"槽"来代表一个 segment. 简单理解就是,ConcurrentHashMap 是一个 Segm

  • Java源码解析ConcurrentHashMap的初始化

    首先看一下代码 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

  • 基于Java并发容器ConcurrentHashMap#put方法解析

    jdk1.7.0_79 HashMap可以说是每个Java程序员用的最多的数据结构之一了,无处不见它的身影.关于HashMap,通常也能说出它不是线程安全的.这篇文章要提到的是在多线程并发环境下的HashMap--ConcurrentHashMap,显然它必然是线程安全的,同样我们不可避免的要讨论散列表,以及它是如何实现线程安全的,它的效率又是怎样的,因为对于映射容器还有一个Hashtable也是线程安全的但它似乎只出现在笔试.面试题里,在现实编码中它已经基本被遗弃. 关于HashMap的线程不

  • 解析ConcurrentHashMap:成员属性、内部类、构造方法分析

    1.简介 ConcurrentHashMap是HashMap的线程安全版本,内部也是使用(数组 + 链表 + 红黑树)的结构来存储元素.相比于同样线程安全的HashTable来说,效率等各方面都有极大地提高. 在学习ConcurrentHashMap源码之前,这里默认大家已经读过HashMap源码,了解LongAdder原子类.红黑树.先简单介绍下 ConcurrentHashMap的整体流程: 整体流程跟HashMap比较类似,大致是以下几步: (1)如果桶数组未初始化,则初始化: (2)如果

  • 解析ConcurrentHashMap:成员属性、内部类、构造方法

    目录 1.简介 2.JDK1.8 ConcurrentHashMap结构图 3.成员属性 4.静态属性 5.静态代码块 6.内部类 6.1 Node节点 6.2 ForwardingNode节点 6.3 TreeNode节点 7.构造方法 8.总结 1.简介 ConcurrentHashMap是HashMap的线程安全版本,内部也是使用(数组 + 链表 + 红黑树)的结构来存储元素.相比于同样线程安全的HashTable来说,效率等各方面都有极大地提高. 在学习ConcurrentHashMap

  • 解析ConcurrentHashMap: put方法源码分析

    上一章:预热(内部一些小方法分析) put()方法是并发HashMap源码分析的重点方法,这里涉及到并发扩容,桶位寻址等等- JDK1.8 ConcurrentHashMap结构图: 1.put方法源码解析 // 向并发Map中put一个数据 public V put(K key, V value) { return putVal(key, value, false); } // 向并发Map中put一个数据 // Key: 数据的键 // value:数据的值 // onlyIfAbsent:

  • 解析ConcurrentHashMap: get、remove方法分析

    前面几篇文章分析了并发HashMap的put方法及其相关方法,transfer方法,那么接下来本篇文章相对之前几篇难度会小一些. 本篇文章介绍ConcurrentHashMap的get方法和remove方法. 1.get方法 get方法:获取元素,根据目标key所在桶的第一个元素的不同采用不同的方式获取元素,关键点在于find()方法的重写. public V get(Object key) { // tab 引用map.table // e 当前元素(用于循环遍历) // p 目标节点 //

  • 解析ConcurrentHashMap: transfer方法源码分析(难点)

    上一篇文章介绍过put方法以及其相关的方法,接下来,本篇就介绍一下transfer这个方法(比较难),最好能动手结合着源码进行分析,并仔细理解前面几篇文章的内容~ 注:代码分析的注释中的CASE0.CASE1- ,这些并没有直接关联关系,只是为了给每个if逻辑判断加一个标识,方便在其他逻辑判断的地方进行引用而已. 再复习一下并发Map的结构图: 1.transfer方法 transfer方法的作用是:迁移元素,扩容时table容量变为原来的两倍,并把部分元素迁移到其它桶nextTable中.该方

  • 解析ConcurrentHashMap: 预热(内部一些小方法分析)

    前面一篇文章中介绍了并发HashMap的主要成员属性,内部类和构造函数,下面在正式分析并发HashMap成员方法之前,先分析一些内部类中的字方法函数: 首先来看下ConcurrentHashMap内部类Node中的hash成员属性值的计算方法spread(int h): static class Node<K,V> implements Map.Entry<K,V> { final int hash;// 该属性是通过spread(int h)方法计算得到~ final K key

  • 深入解析Swift编程中的构造方法

    一.引言 构造方法是一个类创建对象最先也是必须调用的方法,在Objective-C中,开发者更习惯称这类方法为初始化方法.在Objective-C中的初始化方法与普通函数相比除了要以init抬头外并无太严格的分界,而在Swift语言体系中,构造方法与普通的方法分界十分严格,从格式写法上就有不同,普通方法函数要以func声明,构造方法统一为init命名,不需要func关键字声明,不同的构造方法采用方法重载的方式创建. 二.构造方法的复写与重载 在Objective-C中,不同的初始化方法就是不同的

  • 解析Mybatis判断表达式源码分析

    在我们开发过程中用 Mybatis 经常会用到下面的例子 Mapper如下 Map<String ,String > testArray(@Param("array") String [] array); XMl中的sql如下 <select id="testArray" resultType="map"> select * from t_ams_ac_pmt_dtl where cpt_pro=#{cptProp} &l

  • php静态成员方法和静态的成员属性的使用方法

    php静态成员方法和静态的成员属性的使用方法 静态成员方法和静态的成员属性 如下使用: class wan { public static $time = '1天'; public static function xxx() { echo '这就是一个静态的成员方法'; //在类的内部调用静态的成员属性的时候要使用self或者类名关键词,推荐在类的内部使用self echo self::$time; echo wan::$time; //在类的内部调用静态的成员方法的时候,也要使用self或者类

  • 深入解析Java中的内部类

    概述 最近学习python,发现python是支持多继承的,这让我想起Java是通过内部类实现的这套机制.这篇文章不是讲如何通过内部类实现多继承,而是总结一下内部类的类型和使用方法. Java内部类分为: 非静态内部类 静态内部类 局部内部类 匿名内部类 内部类在Android源码中被大量的使用,先介绍一下这四种内部类的共同点: 内部类仍然是一个独立的类,在编译之后内部类会被编译成独立的.class文件,但是前面冠以外部类的类名和$符号. 内部类不能用普通的方式访问.内部类是外部类的一个成员,因

随机推荐