java集合—— ConcurrentHashmap源码解析

该系列都是基于JDK 8

一、基本常量和结构

/** 节点的hash值,这里有三种特殊的,正常的>0 */
static final int MOVED     = -1; // 表示该节点槽位正在扩容中
static final int TREEBIN   = -2; // 表示该节点是树节点
static final int RESERVED  = -3; // 这个类型本章基本操作没有用到不用管

// 默认容量大小
private static final int DEFAULT_CAPACITY = 16;
//默认扩容因子
private static final float LOAD_FACTOR = 0.75f;
//链表长度阈值 树化条件
static final int TREEIFY_THRESHOLD = 8;
//树中只有6个或一下转化成链表
static final int UNTREEIFY_THRESHOLD = 6;
//树化的条件之一 数组长度需要达到的值
static final int MIN_TREEIFY_CAPACITY = 64;
// 默认的容器数组
transient volatile Node<K,V>[] table;
// 辅助扩容时使用的数组
private transient volatile Node<K,V>[] nextTable;
//元素计数器
private transient volatile long baseCount;

//表初始化和大小调整控件 有4种情况
//1.sizeCtl为0,代表数组未初始化, 且数组的初始容量为16
//2.sizeCtl为正数,如果数组未初始化,那么其记录的是数组的初始容量,如果数组已经初始化,那么其记录的是数组的扩容阈值
//3.sizeCtl为-1,表示数组正在进行初始化
//4.sizeCtl小于0,并且不是-1,表示数组正在扩容
private transient volatile int sizeCtl;

// 扩容时使用 需要转移槽位的索引
private transient volatile int transferIndex;

// 在计算元素个数时,防并发的锁(CAS ),跟下面那个东东配合
private transient volatile int cellsBusy;
// 计算元素个数时使用(防止并发,并发时每个线程都会把当前操作的槽位节点数放入里面最后累计)
// 配合baseCount 使用
private transient volatile CounterCell[] counterCells;

大部分都是常规的常量,但是要记住sizeCtl节点的特殊hash值,这两者在下面的操作里面扮演着关键角色,从结构上来看基本和HashMap一致 数组+链表/红黑树,如下:

在这里插入图片描述

ConcurrentHashmap的操作思路与HashMap一致有很多地方雷同,所以建议先看看HashMap源码!

二、构造方法

这里只列举三个常用的构造,不过也基本是全部了(容量计算就不说了)

  • 无参构造: 啥也没有,注意此时sizeCtl 默认为0
  • 初始化容量大小的构造: sizeCtl 为计算后的容量,注意是扩大1.5倍后计算的
  • 完整的带参构造: 同样的 sizeCtl为计算后的容量

这时候就会有个疑问,不管怎么初始化就算了个容量?扩容因子、扩容阈值啥都没有,难道和HashMap一样在第一次添加操作的时候,在初始化数组里面完成的?那就直接去看数组的初始化!

源代码如下:

//无参构造
public ConcurrentHashMap() {}


//初始化容量大小的构造
public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0) {throw new IllegalArgumentException();}
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
               // 把传参扩大了1.5倍后计算容量(变成最近的2的n次方数)
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    //sizeCtl=容量
    this.sizeCtl = cap;
}


//完整的带参构造
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);
    //sizeCtl=容量
    this.sizeCtl = cap;
}
//计算容量的方法  往上找到最近的2的n次方数  比如:7变成8  10变成16
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;
}

三、initTable(容器初始化)

CAS + 自旋的方式保证初始化的线程安全:

  1. sizeCtl<0 就代表有其他线程正在扩容或者初始化,所以让出CPU ,让其他线程先上
  2. 尝试用CAS 把sizeCtl换成-1,失败就继续自旋
  3. 成功了就初始化容器与扩容阈值,有意思的是扩容阈值的计算(容量-1/4),这也就意味着在构造方法里面指定的扩容因子是不生效的,始终是0.75

此时sizeCtl为扩容阈值!

源代码如下:

private final Node<K,V>[] initTable() {
     Node<K,V>[] tab; int sc;
     // 自旋 只要容器数组为空 就不断循环
     while ((tab = table) == null || tab.length == 0) {
         //sizeCtl,代表着初始化资源或者扩容资源的锁,必须要获取到该锁才允许进行初始化或者扩容的操作
         if ((sc = sizeCtl) < 0)
             //放弃当前cpu的使用权,让出时间片,线程计入就绪状态参与竞争
             Thread.yield(); 
         //CAS 比较并尝试将sizeCtl替换成-1,如果失败则继续循环    
         else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
             try {
                 //进行一次double check 防止在进入分支前,容器发生了变更
                 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;
                     //容量-容量/4  ==  容量*3/4 == 扩容阈值(扩容因子0.75)
                     sc = n - (n >>> 2);
                 }
             } finally {
                 // 此时sizeCtl为扩容阈值
                 sizeCtl = sc;
             }
             break;
         }
     }
     return tab;
 }

四、put操作

整体逻辑和HashMap差不多:1.计算hash 2.是否初始化数组 3.是否直接插入 4. 是否插入链表/红黑树

但是加入了线程安全的操作保障(CAS+自旋+synchronized数组操作全是内存的偏移量 ):

  1. 计算hash值后,直接死循环(自旋)
  2. 判断容器数组是否为空,空则初始化容器数组
  3. 根据hash计算数组下标【(length-1)&hash】,再结合偏移量从数组中取值
  4. 值为空,说明槽位还没节点,所以可以直接放入该下标对应的槽位(CAS+自旋放入
  5. 值不
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值