JAVA源代码-java.util.concurrent 包--ConcurrentHashMap

本文详细介绍了Java中的并发集合类ConcurrentHashMap,分析了其多线程安全的数据结构(数组+链表+红黑树),并讲解了putVal的实现过程。文章还探讨了为何ConcurrentHashMap在高并发下表现快速,包括1.7版的分段技术以及1.8版的CAS+Synchronized优化和红黑树的使用。同时提到了扩容策略和读写操作的并发控制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

java.util.concurrent包下有众多的线程安全类实现包括:ConcurrentHashMap、ArrayBlockingQueue、ConcurrentLinkedQueue、CopyOnWriteArrayList、ThreadPoolExecutor等等。今天我们来详细介绍ConcurrentHashMap & CopyOnWriteArrayList & ThreadPoolExecutor的底层实现。

ConcurrentHashMap是多线程安全的,其底层数据与HashMap的数据结构一样。ConcurrentHashMap的数据结构(数组+链表+红黑树),桶中的结构可能是链表也可能是红黑树,用红黑树主要是为了提高查询效率。

ConcurrentHashMap成员介绍说明:

    // 表的最大容量
    private static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认表的大小
    private static final int DEFAULT_CAPACITY = 16;
    // 最大数组大小
    static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    // 默认并发数
    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;
    // 转化为红黑树的表的最小容量
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 每次进行转移的最小值
    private static final int MIN_TRANSFER_STRIDE = 16;
    // 生成sizeCtl所使用的bit位数
    private static int RESIZE_STAMP_BITS = 16;
    // 进行扩容所允许的最大线程数
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
    // 记录sizeCtl中的大小所需要进行的偏移位数
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;    
    // 一系列的标识
    static final int MOVED     = -1; // hash for forwarding nodes
    static final int TREEBIN   = -2; // hash for roots of trees
    static final int RESERVED  = -3; // hash for transient reservations
    static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
    // 
    /** Number of CPUS, to place bounds on some sizings */
    // 获取可用的CPU个数
    static final int NCPU = Runtime.getRuntime().availableProcessors();
    // 
    /** For serialization compatibility. */
    // 进行序列化的属性
    private static final ObjectStreamField[] serialPersistentFields = {
        new ObjectStreamField("segments", Segment[].class),
        new ObjectStreamField("segmentMask", Integer.TYPE),
        new ObjectStreamField("segmentShift", Integer.TYPE)
    };
    
    // 表
    transient volatile Node<K,V>[] table;
    // 下一个表
    private transient volatile Node<K,V>[] nextTable;
    //
    /**
     * Base counter value, used mainly when there is no contention,
     * but also as a fallback during table initialization
     * races. Updated via CAS.
     */
    // 基本计数
    private transient volatile long baseCount;
    //
    /**
     * 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;
    
    /**
     * The next table index (plus one) to split while resizing.
     */
    // 扩容下另一个表的索引
    private transient volatile int transferIndex;

    /**
     * Spinlock (locked via CAS) used when resizing and/or creating CounterCells.
     */
    // 旋转锁
    private transient volatile int cellsBusy;

    /**
     * Table of counter cells. When non-null, size is a power of 2.
     */
    // counterCell表
    private transient volatile CounterCell[] counterCells;

    // views
    // 视图
    private transient KeySetView<K,V> keySet;
    private transient ValuesView<K,V> values;
    private transient EntrySetView<K,V> entrySet;
    
    // Unsafe mechanics
    private static final sun.misc.Unsafe U;
    private static final long SIZECTL;
    private static final long TRANSFERINDEX;
    private static final long BASECOUNT;
    private static final long CELLSBUSY;
    private static final long CELLVALUE;
    private static final long ABASE;
    private static final int ASHIFT;

putVal函数的实现:

/** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());//键的hash值通过spread方法计算而得
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh; K fk; V fv;
            if (tab == null || (n = tab.length) == 0)//表为空,则进行初始化
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//表不为空并且表的长度大于0,并且该桶不为空
                if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))//比较并且交换值,如tab的第i项为空则生成新的node替换
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)//如果该结点的hash值为MOVED,则进行节点的转移
                tab = helpTransfer(tab, f);
            else if (onlyIfAbsent // check first node without acquiring lock
                     && fh == hash
                     && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                     && (fv = f.val) != null) //JDK12新逻辑
                return fv;
            else {
                V oldVal = null;
                synchronized (f) {//加锁同步
                    if (tabAt(tab, i) == f) {//找到tab下标为i的节点
                        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, value);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) { //如果是红黑树节点类型
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {//将hashcode,key,value放到红黑树中
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                        else if (f instanceof ReservationNode)//如果是ReservationNode这种类型直接抛错
                            throw new IllegalStateException("Recursive update");
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)//如果tree的值大于红黑树转换的阀值则进行转换
                        treeifyBin(tab, i);
                    if (oldVal != null)//旧值不为空则返回旧值
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }

现在重新解释一遍putVal的过程:

  1. 判断存储的key,value是否为空,为空则抛出异常
  2. 计算key的hash值,随后进行循环,该循环可以确保成功插入数据,或table的初始化为0则进行初始化table表。
  3. 根据key的hash值取出table表中的元素,或取出的结点为空,则用CAS将key,value,hash值生成的结点放入桶中
  4. 其该结点的hash值为MOVED,则对桶中的数据进行转置。
  5. 对桶中的第一个节点进行加锁,对该桶进行遍历,桶中的结点的hash&value值是否与给定的hash&value值一致,不一致则进行更新,如果遍历完了依旧没有找到相等的元素,则在最后一个结点创建新的结点。
  6. 若binCount达到红黑树最大阀值,则将桶中的元素转换为红黑树存储,最后增加bitCount值

最后解释一下concurrenthashMap为什么快?

jdk1.7 的concurrentHashMap采用分段技术,将concurrentHashMap的容器分段存储,每一段数据分配一个segment(锁),当一个线程占用其中的一个segment的时候,其它线程可以继续访问其它的segment。segment采用的ReentrantLock实现的。

1.7的扩容是在插入之前判断table的存储元素是否超过了当前的threshhold=table.length * 0.75.concurrentHashMap扩容默认用的是table的0.75倍(居然用这个公式算0.75(n - (n >>> 2)))

jdk1.8有以下同点改进

  1. 采用CAS+Synchronized来保证并发的安全性问题,1.8取消了segment而用volatile修饰折table进行数据存储,这样采用table存储的数据元素作为锁,从而实现对每一行数据进行加锁,进一步减少并发冲突的概率。
  2. 将原来数组+链表的存储方式改为数据+链表+红黑树,对于hash表来说,最重要是能够将key hash均匀分布,对于链表个数超过为8的便转化为红黑树,查询效率由原来的O(n)提升为O(log(n)) 以改善性能。

 

concurrentHashMap上述的代码中可以看出在putVal中是有加锁操作的,但是读为何不用加锁呢?get的操作元素node是用volatile修饰的,volatile在同步正确的前提下,都对get可见。

1.8扩容是先插入,然后判断下一次的++size大小是否会超过当前的阀值,超过则扩容。

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值