【java】ConcurrentHashMap1.7源码详解

本文详细探讨了ConcurrentHashMap1.7的源码,解释了其为何优于HashTable,采用分段锁实现线程安全。文章分析了Segment类、HashEntry类的核心属性和方法,包括put、remove等操作,揭示了其高效并发的秘密。
摘要由CSDN通过智能技术生成

前言

随着高并发时代的到来,原有的HashMap已经不能满足基本的需求,在HashMap1.7中,多线程下可能出现的的死循环是致命的。但在java api的juc包中有这样一个类:ConcurrentHashMap,它基于HashMap1.7且线程是安全的,本篇博文会仔细对它进行讲解。强烈建议,在阅读本篇博文前,先阅读 HashMap1.7源码详解

博主默认读者是了解HashMap1.7基本原理的。因此,对于它们相同的部分将不再赘述。在下面所提到的HashMap和ConcurrentHashMap,如无特殊说明,都基于JDK1.7。

HashTable与ConcurrentHashMap

HashTable为保证线程安全付出的代价太大,get()、put()等方法都是synchronized的,这相当于给整个哈希表加了一把大锁。在并发调用HashTable的方法时就会造成大量的时间损耗。
ConcurrentHashMap的设计就显得非常巧妙,它采用分段加锁的方式保证线程安全,而不是将整个哈希表进行加锁,减少了线程阻塞的损耗时间。

数据结构

Segment + HashEntry

每个HashEntry结构都相当于HashMap中的一个哈希表(数组+链表)
在这里插入图片描述
ConcurrentHashMap采用分段锁的机制,用Segment来分割整张哈希表;在对不同分段进行操作时,可以做到互不干扰,避免加锁。

Segment类

static final class Segment<K,V> extends ReentrantLock implements Serializable {}

Segment类继承了ReentrantLock类,ReentrantLock和synchronized都是可重入的独占锁,只允许线程互斥的访问临界区,这就验证了ConcurrentHashMap是基于Segment段来加锁的。它实现了Serializable接口,可进行对象的序列化与反序列化。

属性
// 在强制加锁前的最大尝试次数
// availableProcessors()返回java虚拟机的可用处理器数
static final int MAX_SCAN_RETRIES =     		
    Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
// 每个分段锁里有一个数组
transient volatile HashEntry<K,V>[] table;
// 数组结点数
transient int count;
transient int modCount;
// 扩容阈值
transient int threshold;
// 加载因子
final float loadFactor;

从上面来看,Segment类几乎涵盖了HashMap里的所有核心属性,这也意味着每个Segment对象都相当于一个HashMap,这也就是分段思想的核心。需要强调的是MAX_SCAN_RETRIESmodCount,在ConcurrentHashMap中,一些方法在执行时不是直接加锁,而是通过连续的多次遍历来确定原哈希表是否被别的线程修改了。判断的依据就是modCount是否改变,而循环遍历的次数也不能没有限制,MAX_SCAN_RETRIES就是确定加锁前最大尝试次数的。具体的使用将在代码中进行讲解。

put(K key, int hash, V value, boolean onlyIfAbsent)
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
	// tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,
	// 则返回true,如果获取失败(即锁已被其他线程获取),则返回false,
	// 也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;
        // 取得数组下标
        int index = (tab.length - 1) & hash;
        // 得到链表首结点
        HashEntry<K,V> first = entryAt(tab, index);
        // 查找是否有相同的key,若有,则根据onlyIfAbsent来确定是否进行替换
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k;
                // key相同或满足equals条件
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    // onlyIfAbsent: 如果key不存在才增加
                    if (!onlyIfAbsent) {
                        e.value = value;
                        // 修改次数加1
                        ++modCount;
                    }
                    break;
                }
                // 如果当前结点不为空,查询下一结点
                e = e.next;
            }
            // 链表为空,或到了末结点并未找到目标key
            else {
           		// 结点不为空,说明scanAndLockForPut()有返回值
                if (node != null)
               		// 结点前插法,hashmap1.7也用的前插法
                    node.setNext(first);
                // 第一句中tryLock()成功
                else
               		// 前插添加结点,将first作为node的下一结点
                    node = new HashEntry<K,V>(hash, key, value, first);
                int c = count + 1;
                // 达到扩容阈值且未到最大容量,进行扩容
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                // 将新的头结点组成的链表放到数组中
                else
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    // 保证无论出现任何情况都要解锁
    } finally {
        unlock();
    }
    return oldValue;
}

scanAndLockForPut(K key, int hash, V value)

// 方法作用:创建新结点或找到key相同的结点并加锁,为添加做准备
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
	// 找到该hashcode对应的链表的头结点
    HashEntry<K,V> first = entryForHash(this, hash);
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    int retries = -1; // 定位节点为负
    // 循环获取锁,线程安全
    while (!tryLock()) {
        HashEntry<K,V> f; // 请在下面重新检查
        if (retries < 0) {
       		// 链表为空或遍历完链表未找到key相同的结点
            if (e == null) {
           		// 在最下面的else if中可能会将retries置为-1
           		// 所以还可能再次进入这里,需要判断node是否为空
           		// 这里虽然创建了新的结点,但是并没有链在链表上,e依然为空             		
                if (node == null) // 创建新结点
                    node 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值