《码出高效》学习:ConcurrentHashMap

本文详细介绍了Java中的ConcurrentHashMap,从简介到内部实现,包括初始化、插入元素、链表转红黑树、协助扩容和删除节点的机制。JDK8开始,ConcurrentHashMap取消分段锁,引入红黑树和更高效的计数方法,以提高并发性能。
摘要由CSDN通过智能技术生成

目录

 

简介

基本认识

初始化:initTable方法

插入元素:实际为putVal方法

链表进化:treeifyBin方法

协助扩容:helpTransfer & Transfer

删除节点:实际为replaceNode方法

计数方法的改进


简介

 

ConcurrentHashMap主要应用于高并发环境下,使用了大量的lock-free技术来减轻锁竞争导致的性能下降。从JDK5开始引入,最初使用锁分段技术,即并发时不是对整个Map加锁,而是对数据所处的Segment进行加锁:(图片来自网络,侵删)

ConcurrentHashMap初始化时,计算出Segment数组的大小ssize和每个Segment中HashEntry数组的大小cap,并初始化Segment数组的0元素。Segment使用ReentrantLock可重入锁加锁。

JDK8开始,ConcurrentHashMap进行了以下三个主要改进:

  • 取消分段锁机制,改用CAS
  • 引入红黑树结构,元素数量达到阈值后自动进化
  • 引入mappingCount方法,能够统计更多数量的元素(2^{63}-1

基本认识

首先看成员变量:

    //实际存放数据的数组,默认null,大小总是2的幂
    //第一次插入数据时才会初始化
    transient volatile Node<K,V>[] table;

    //扩容时生成,大小时原数组的2倍
    private transient volatile Node<K,V>[] nextTable;

    //直接存储的元素数,通过CAS更新
    private transient volatile long baseCount;

    //用来控制table初始化和扩容
    //-1:代表正在初始化
    //-n:代表n-1个线程正在进行扩容
    //0:默认值,将使用默认容量进行初始化
    //>0:代表初始化或扩容中需要使用的容量
    private transient volatile int sizeCtl;

    //扩容时转移数据使用
    private transient volatile int transferIndex;

    //一个用于扩容的自旋锁对象
    private transient volatile int cellsBusy;

    //计数单元的数组,非空时数量是2的幂
    //Map存储的元素真实数量等于baseCount加上每个counterCell的值
    private transient volatile CounterCell[] counterCells;

    // 用来支持 keySet()、entrySet()、values()等方法的视图
    private transient KeySetView<K,V> keySet;
    private transient ValuesView<K,V> values;
    private transient EntrySetView<K,V> entrySet;

以及内部类:

  • Node<K,V>:继承了Map.Entry<K,V>,是数据节点,有四个子类,分别重写了find方法:
    • TreeBin:不实际存储数据,而是TreeNode的桶,维护了桶内红黑树的读写锁和节点引用,hash值固定为-2
    • TreeNode:实际存储数据的节点
    • ForwardingNode:扩容转发节点,,用来把原有哈希槽的操作转发到nextTable(内部记录了nextTable),hash值固定为-1。正在put的线程遇到它的find方法,就不得不协助扩容
    • ReservationNode:占位加锁节点,某些方法(computeIfAbsent)用它进行加锁,hash值固定为-3

还有静态变量:

    //单个table最大容量,为什么不是32呢?因为有两个bit用作哈希控制
    private static final int MAXIMUM_CAPACITY = 1 << 30;

    //table初始化大小
    private static final int DEFAULT_CAPACITY = 16;

    //toArray之后最大大小
    static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    //用来与旧版本的ConcurrentHashMap保持兼容性
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

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

    //进化阈值,如果桶内节点数超过阈值则从链表进化为红黑树
    static final int TREEIFY_THRESHOLD = 8;

    //退化阈值,节点数少于这个值就退化为链表
    static final int UNTREEIFY_THRESHOLD = 6;

    //进化阈值,进化时必须保证table容量到达该值,否则只扩容不进化
    static final int MIN_TREEIFY_CAPACITY = 64;

    //要求每个线程至少调整多少个桶,用来控制大小调整时线程数量,防止过度竞争
    private static final int MIN_TRANSFER_STRIDE = 16;

    //用于扩容时生成一个戳,下面有用到
    private static int RESIZE_STAMP_BITS = 16;

    //扩容线程最多有多少个
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

    //通过对RESIZE_STAMP_BITS移位,用来生成sizeCtl
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

存储结构如下:(图拍自书)

初始化:initTable方法

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    //如果表为空才初始化
    while ((tab = table) == null || tab.length == 0) {
        //sizeCtl<0说明已经有线程正在进行初始化操作,就不用本线程插手了
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        //否则修改sizeCtl,然后开工
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.le
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值