源码:JDK8并发工具类ConcurrentHashMap底层源码解读

本文详细解读了JavaConcurrentHashMap在JDK8中的实现,包括其线程安全机制、散列算法、初始化、put和get操作以及扩容过程。重点分析了putVal方法的并发控制和treeifyBin方法将链表转为红黑树的过程,展示了ConcurrentHashMap如何在保证并发性能的同时,实现高效的数据存储和检索。
摘要由CSDN通过智能技术生成

本文章对 java.util.concurrent.ConcurrentHashMap 类在实际生产开发中常用的方法进行解读,若解释有误,还望高手评论区斧正


一、介绍

ConcurrentHashMapJUC包下的一个线程安全类,ConcurrentHashMap并非锁住整个方法,而是通过原子操作和局部加锁的方法保证了多线程的安全访问,且尽可能地减少了性能损耗。

public class ConcurrentHashMap<K,V> 
		extends AbstractMap<K,V>
        implements ConcurrentMap<K,V>, Serializable {

二、源码解读

0. 基本属性

// 表初始化和调整大小控制。
// -1:表示table数组正在初始化
// <-1:table数组正在扩容,-2为1个扩容线程,-3为2个扩容线程...
// 0:表示table数组还未初始化
// >0:如果未初始化,表示要初始化的长度,已初始化,表示扩容的阈值
private transient volatile int sizeCtl;

1. 构造方法

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;
}

2. put相关方法

putVal:存储元素

put方法过程:

  1. 计算key的哈希值hash
  2. 如果table未初始化,则CAS进行初始化
  3. 如果hash在table对应位置的bucket为空,则CAS进行创建(赋值)
  4. 如果bucket在扩容中,则帮助其扩容
  5. synchronized锁住bucket的第一个元素,进行put操作
  6. bucket中的节点数大于等于8,进行table扩容或bucket链表转红黑树
  7. 调用addCount(),判断是否需要扩容
public V put(K key, V value) {
	return putVal(key, value, false);
}

/**
 * 如果table或bucket未初始化,则不加锁,通过CAS保证并发安全性
 * 其他情况加synchronized锁,锁的是bucket[0]元素
 *
 * @param key 键
 * @param value 新值
 * @param onlyIfAbsent true:不存在时才会put
 * @return 旧值
 */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    // 1.计算key的哈希值hash
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
    	// f:bucket存储的根节点数据(链表或红黑树)
    	// n:table数组长度
    	// i:key对应的数组索引
    	// fh:f对应的hash值
        ConcurrentHashMap.Node<K,V> f; int n, i, fh;
        // 2.如果:table尚未初始化
        if (tab == null || (n = tab.length) == 0)
            // 初始化table
            tab = initTable(); // 初始化表无锁
        // 3.如果:bucket尚未使用
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 如果:CAS创建bucket成功
            if (casTabAt(tab, i, null,
                    new ConcurrentHashMap.Node<K,V>(hash, key, value, null)))
                // 退出循环,put完成
                break;
        }
        // 4.如果:table[i]处于扩容rehash状态中
        else if ((fh = f.hash) == MOVED)
            // 帮助table[i]进行扩容,并将tab指向newTable
            tab = helpTransfer(tab, f);
        else {
        	// 解决哈希冲突,将元素添加到到链表或红黑树中
            V oldVal = null;
            // 5.锁住bucket的第一个元素(头节点、根节点),进行put操作
            synchronized (f) {
                // double check
                if (tabAt(tab, i) == f) {
                	// 如果:bucket为链表结构
                    if (fh >= 0) {
                        binCount = 1;
                        for (ConcurrentHashMap.Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 如果:该key已存在
                            if (e.hash == hash &&
                                    ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                // 根据onlyIfAbsent选择性更新该value
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            ConcurrentHashMap.Node<K,V> pred = e;
                            // 如果:遍历到了bucket最后一个node
                            if ((e = e.next) == null) {
                            	// 创建新node并链接在其尾部
                                pred.next = new ConcurrentHashMap.Node<K,V>(hash, key, value, null);
                                break;
                            }
                        }
                    }
                	// 如果:bucket为树结构
                    else if (f instanceof ConcurrentHashMap.TreeBin) {
                        ConcurrentHashMap.Node<K,V> p;
                        binCount = 2;
                        if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
            	// 6.如果:bucket中的节点数大于等于8
                if (binCount >= TREEIFY_THRESHOLD)
                	// 扩容或转为红黑树
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 7.
    addCount(1L, binCount);
    return null;
}

spread:散列算法,获取key的哈希值

// 通过spread方法,获取key的hash值
int hash = spread(key.hashCode());
// spread方法实现
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}
// 根据hash值,获取索引位置
int i = (n - 1) & hash

将key的hashcode值,高16位于低16位按 ^ 运算,将结果与数组长度-1进行&运算,得到当前数据需要添加到哪个索引位置

00000110 00011000 00111010 00001110    -h
00000000 00000000 00000110 00011000    -h >>> 16
00000000 00000000 00000000 00000000    -15
01111111 11111111 11111111 11111111   -HASH_BITSHASH_BITS进行&运算,最终保证:key的hash值为一个正数
因为负数有特殊含义:
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

initTable:初始化数组

private final Node<K,V>[] initTable() {
   Node<K,V>[] tab; int sc;
   // 判断:数组是否初始化
   while ((tab = table) == null || tab.length == 0) { 
   	   // 判断:当前table是否正在初始化或扩容
       if ((sc = sizeCtl) < 0)
           Thread.yield();
       // 以CAS方式,将sizeCtl设置为-1(-1表示当前table数组正在初始化)
       else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
           try {
           	   // Double Check
               if ((tab = table) == null || tab.length == 0) {
               	   // sizeCtl > 0,则作为长度,sizeCtl == 0,默认16长度
                   int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                   Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                   // table初始化完成
                   table = tab = nt;
                   // 计算下次扩容的阈值,赋值给sc
                   sc = n - (n >>> 2);
               }
           } finally {
           	   // sc赋值给sizeCtl
               sizeCtl = sc;
           }
           break;
       }
   }
   return tab;
}

treeifyBin:将bucket的链表转为红黑树

/**
 * 替换给定索引处 bin 中的所有链接节点,除非表太小,在这种情况下,改为调整大小。
 */
private final void treeifyBin(ConcurrentHashMap.Node<K,V>[] tab, int index) {
    ConcurrentHashMap.Node<K,V> b; int n, sc;
    if (tab != null) {
        // 1.如果:table长度小于64
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            // 扩容为2倍长度
            tryPresize(n << 1);
        // 2.如果:bucket不为空且为链表结构
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            // 锁住bucket第一个节点
            synchronized (b) {
                if (tabAt(tab, index) == b) {
                    ConcurrentHashMap.TreeNode<K,V> hd = null, tl = null;
                    // 3.将此bucket的所有链表节点转为树节点,并按原顺序将这些树节点组织成双向链表
                    for (ConcurrentHashMap.Node<K,V> e = b; e != null; e = e.next) {
                        ConcurrentHashMap.TreeNode<K,V> p =
                                new ConcurrentHashMap.TreeNode<K,V>(e.hash, e.key, e.val, null, null);
                        if ((p.prev = tl) == null)
                            hd = p;
                        else
                            tl.next = p;
                        tl = p;
                    }
                    // 4.将TreeNode由链表结构转化为红黑树结构
                    setTabAt(tab, index, new ConcurrentHashMap.TreeBin<K,V>(hd));
                }
            }
        }
    }
}

tryPresize:扩容

private final void tryPresize(int size) {
	// 对扩容数组长度作判断,保证其不超过阈值,并且是2的n次幂
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? 
    	MAXIMUM_CAPACITY :
        tableSizeFor(size + (size >>> 1) + 1);
    int sc;
    while ((sc = sizeCtl) >= 0) {
    	// 两种可能:1.未初始化数组(putAll方法)2.初始化数组
        Node<K,V>[] tab = table; int n;
        // 判断:数组是否初始化
        if (tab == null || (n = tab.length) == 0) {
        	// 进行初始化
            n = (sc > c) ? sc : c;
            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if (table == tab) {
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
            }
        }
        // 如果扩容长度小于扩容阈值
        // 数组长度已经大于等于最大长度
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            break;
        // 扩容
        else if (tab == table) {
        	// 获取扩容戳(32位数值,高16位为扩容标识,低16位为扩容线程数)
            int rs = resizeStamp(n);
            // sc小于0,当前正在扩容
            if (sc < 0) {
            	// 帮助扩容
                Node<K,V>[] nt;
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            // 当前没有正在扩容,设置扩容标识,开始扩容
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
        }
    }
}

3. get相关方法

public V get(Object key) {
    Node<K,V> e;
    // 计算key的哈希值,并进行查找相应value节点,找不到则返回null
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 如果:表已初始化,且hash对应bucket不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
        // 如果:bucket第一个元素与该key相等
        if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
            // 直接返回第一个元素
            return first;
        if ((e = first.next) != null) {
        	// 如果是树结构
            if (first instanceof TreeNode)
            	// 以红黑树的检索方式get
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 普通链表结构进行遍历查找
            do {
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    // 不存在该key
    return null;
}

三、其他

关于扩容的概述

在JDK 1.8的ConcurrentHashMap中,迁移元素是扩容过程中的关键步骤,它涉及将旧数组(或称为table)中的元素重新哈希并移动到新的、容量更大的数组中。以下是迁移元素的详细过程:

  1. 创建新数组:首先,ConcurrentHashMap会创建一个新的数组,其长度通常是旧数组的两倍。这个新数组将用于存储扩容后的所有元素。

  2. 分配迁移任务:由于ConcurrentHashMap支持并发扩容,多个线程可以同时参与迁移过程。因此,系统会根据当前可用的CPU核心数(NCPU)来分配迁移任务。每个线程负责迁移旧数组中的一段元素,这个段的长度用stride(步长)来表示。为了降低资源竞争频率,stride的最小范围长度通常是固定的,如16。

  3. 记录迁移进度:在迁移过程中,ConcurrentHashMap使用一个变量(如transferIndex)来记录整个数组扩容的进度。每个参与迁移的线程会根据其负责的段和stride来更新transferIndex的值,以表示当前已经迁移了哪些元素。

  4. 重新哈希并迁移元素:每个线程会遍历其负责的旧数组段,并对每个元素进行以下操作:

    • 计算元素在新数组中的索引位置:这通常是通过重新哈希元素的键来完成的。
    • 使用CAS(Compare-And-Swap)或其他原子操作将元素添加到新数组的正确位置。由于多个线程可能同时尝试迁移同一个元素,因此需要使用原子操作来确保数据的一致性。
    • 如果CAS操作失败(即有其他线程已经成功迁移了该元素),则当前线程会重试或放弃该元素的迁移。
  5. 处理冲突和链表:在迁移过程中,如果两个或多个元素哈希到新数组的同一个位置(即发生哈希冲突),它们将被组织成一个链表。如果链表长度过长(超过某个阈值,如8),ConcurrentHashMap可能会将其转换为红黑树以提高查询效率。

  6. 更新引用和清理:当所有元素都成功迁移到新数组后,ConcurrentHashMap会更新其内部对数组的引用,使其指向新的数组。同时,它会释放与旧数组相关的资源,如内存等。

需要注意的是,由于ConcurrentHashMap的扩容过程涉及多个线程并发执行,因此在迁移元素的过程中可能会出现一些竞争条件和数据不一致的问题。为了解决这些问题,ConcurrentHashMap在迁移过程中使用了一系列复杂的并发控制策略,如CAS操作、自旋锁等,以确保数据的一致性和并发访问的效率。


更多ConcurrentHashMap源码解读,推荐查看文章:https://cloud.tencent.com/developer/article/2209609

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

自传丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值