JDK1.8 ConcurrentHashMap源码分析(一)

JDK1.8 ConcurrentHashMap源码分析(一)

前面几篇文章分析了HashMap和LongAdder的源码,因为是逐行分析的,我想我应该是讲明白了吧。这期主要来分析ConcurrentHashMap的源码,看完了HashMap源码的朋友应该会觉得其实也就那样,把其中位运算弄明白了之后基本没什么难的地方,也就一个resize()方法在扩容的时候略微复杂一点点,和ConcurrentHashMap比起来HashMap简直是个弟弟,阅读ConcurrentHashMap源码之前需要读完HashMap和LongAdder源码,不然理解起来会很吃力,这期依旧是逐行分析源码,一起来感受一下Doug Lea写的代码的魅力。

一、基本原理

ConcurrentHashMap在1.7的底层实现是Entry数组+链表+Segment数组,Segment其实就是分段锁,每一个Segment负责加锁一个或多个桶位,加锁方式是ReentrantLock。在1.8的底层实现是Node数组+链表/红黑树,锁机制是CAS + synchronize,synchronize锁住的是桶位的头节点。关于1.7我就不去具体分析了,重点还是1.8的源码。

如下图所示,ConcurrentHashMap在结构上和HashMap并没有太多的变化,这里重点需要关注的是Forwarding这个节点,ConcurrentHashMap中维护了table和nextTable两个变量,table就是真正存放元素的数组,nextTable是用于扩容时维护的一个新数组,因为单个线程是依次转移每个桶位的数据到新的数组上去,当转移完成后就会把该桶位的头节点设置为Forwarding节点,这样做的好处是,如果有其他线程这个时候来旧数组中查询元素,发现需要查询的桶位上面是ForwardingNode,则可以直接去新数组中查询而不用等待扩容完成。

ConcurrentHashMap还有一个机制是并发扩容,当数组正在扩容时,如果有其他的线程执行put操作,会一起参与进扩容中来,这样可以让ConcurrentHashMap的性能进一步提升,具体的操作留待后续中讲解。

jdk1.8

二、静态常量

同样的还是从类中的静态常量开始,因为这些常量都是会被后面方法反复用到的,我们需要先提前知道每个常量的含义。

/**
 * Map的最大容量2^30(一般不可能达到)
 */
private static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 默认初始容量16
 */
private static final int DEFAULT_CAPACITY = 16;

/**
 * table数组的最大长度
 */
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * 默认并发级别16(可同时参与扩容的线程数量)
 */
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

/**
 * 负载因子0.75
 */
private static final float LOAD_FACTOR = 0.75f;

/**
 * 链表转为红黑树的阈值(链表长度)
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 红黑树退化成链表的阈值(红黑树节点个数)
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 发生树化的最小数组长度64
 */
static final int MIN_TREEIFY_CAPACITY = 64;

/**
 * 扩容时每个线程负责的最小桶位数量为16
 */
private static final int MIN_TRANSFER_STRIDE = 16;

/**
 * 扩容标识戳
 */
private static int RESIZE_STAMP_BITS = 16;

/**
 * 计算的结果是65535,表示最大参与扩容的线程数量
 */
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

/**
 * 这个留着后面来看
 */
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

// 元素的hash值为-1时,表示当前节点是Forwarding节点
static final int MOVED     = -1;
// 元素的hash值为-2时,表示当前节点是Treebin节点,这个节点是用于代理操作红黑树的
static final int TREEBIN   = -2;
// 这个暂时不管
static final int RESERVED  = -3;
// 0x7fffffff -> 0111 1111 1111 1111 1111 1111 1111 1111
// 是用于按位与运算来把一个负数转为正数
static final int HASH_BITS = 0x7fffffff;
// CPU核心数
static final int NCPU = Runtime.getRuntime().availableProcessors();

三、内部基本方法

/**
 * 扰动函数,用于计算hashCode和hashCode高16的异或值,
 * 计算出的值和HASH_BITS进行按位与,这样可以保证得到一定是正数
 */
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

/**
 * 和HashMap一样的功能,得到比c大的最小2的整数次幂
 */
private static final int tableSizeFor(int c) {
    int n = c - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

/**
 * 获得table数组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);
}
/**
 * cas方式修改table数组i下标元素的值
 */
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);
}
/**
 * 修改table数组i下标元素并且立即同步至主存
 */
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

四、成员变量

/**
 * 当前存储元素的数组
 */
transient volatile Node<K,V>[] table;

/**
 * 扩容时的新数组,扩容结束后会赋值给table
 */
private transient volatile Node<K,V>[] nextTable;

/**
 * 对ConcurrentHashMap的操作次数
 */
private transient volatile long baseCount;

/**
 * sizeCtl = -1: 表示table正在初始化或扩容, sizeCtl = -1时表示正在初始化
 * sizeCtl < -1: 高16位表示扩容戳,低16位 = -(1 + n),n为参与扩容的线程数量
 * sizeCtl = 0: 表示table未初始化并且未传入初始化大小
 * sizeCtl > 0: 下一次的扩容阈值或table未初始化但制定了初始化大小
 */
private transient volatile int sizeCtl;

/**
 * 扩容时下一次需要迁移的桶位下标
 */
private transient volatile int transferIndex;

/**
 * 锁变量,用于对CounterCell[]初始化或扩容时加锁
 */
private transient volatile int cellsBusy;

/**
 * 和LongAdder中的cell[]是一个东西
 */
private transient volatile CounterCell[] counterCells;

五、构造方法

五个构造方法,其中细节会略微有差异,简单阅读一下即可。

/**
 * 创建一个map,table数组默认大小16
 */
public ConcurrentHashMap() {
}

/**
 * 指定初始化大小为initialCapacity
 */
public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    // 判断initialCapacity值是否超出最大限制
    // 使用tableSizeFor()方法时入参和HashMap有区别
    // HashMap的入参就是initialCapacity
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

/**
 * 使用ConcurrentHashMap容纳另一个map的所有元素
 */
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    // 先尝试以16来容纳
    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)
        initialCapacity = concurrencyLevel;
    // 根据负载因子重新计算初始大小
    long size = (long)(1.0 + (long)initialCapacity / loadFactor);
    // 使用tableSizeFor()得到2的整数次幂
    int cap = (size >= (long)MAXIMUM_CAPACITY) ?
        MAXIMUM_CAPACITY : tableSizeFor((int)size);
    // 将最后计算得出的数组初始化大小赋值给sizeCtl
    this.sizeCtl = cap;
}

以上是ConcurrentHashMap中需要提前了解的常量、变量、方法,下一期会分析重点方法put()。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容