Connor学Java - JDK7中的ConcurrentHashMap是如何保证线程安全的

在这里插入图片描述

Learn && Live

虚度年华浮萍于世,勤学善思至死不渝

前言

Hey,欢迎阅读Connor学Java系列,这个系列记录了我的Java基础知识学习、复盘过程,欢迎各位大佬阅读斧正!原创不易,转载请注明出处:http://t.csdn.cn/vMvcI,话不多说我们马上开始!

JDK7中的ConcurrentHashMap是如何保证线程安全的?

分段锁技术
public class ConcurrentHashMap<K, V> 
    extends AbstractMap<K, V>
    implements ConcurrentMap<K, V>, Serializable {

    final Segment<K,V>[] segments;

    static final class Segment<K,V> 
        extends ReentrantLock 
        implements Serializable {
        transient volatile HashEntry<K,V>[] table;
        transient int count;
    }

    // 基本节点,存储Key, Value值
    static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;
    }
}

分段锁的思想就是:锁的时候不锁整个hash表,而是只锁一部分。

如何实现呢?这就用到了ConcurrentHashMap中最关键的Segment。

ConcurrentHashMap中维护着一个Segment数组,每个Segment可以看做是一个HashMap。

而Segment本身继承了ReentrantLock,它本身就是一个可重入锁。

在Segment中通过HashEntry数组来维护其内部的hash表。

每个HashEntry就代表了map中的一个K-V,用HashEntry可以组成一个链表结构,通过next字段引用到其下一个元素。

所以JDK7中ConcurrentHashMap的整体结构可以描述为下图:

在这里插入图片描述

put方法的线程安全

根据上述结构很容易想到,ConcurrentHashMap通过哈希值定位一个元素需要两次,第一次定位到Segment,第二次才是我们熟悉的HashMap定位到某一链表的头部。

ConcurrentHashMap的put方法源码如下:

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    // (1)
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    // (2)
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
        (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    // (3)
    return s.put(key, hash, value, false);
}

(1)计算key的哈希值

(2)根据key的哈希值定位一个Segment,懒加载:若Segment还未初始化,则调用ensureSegment初始化

(3)调用Segment的put方法

下面来看Segment内的put方法:

static final class Segment<K,V>
    extends ReentrantLock
    implements Serializable {
	final V put(K key, int hash, V value, boolean onlyIfAbsent) {
		HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
    	V oldValue;
    	...
	}

	private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    	HashEntry<K,V> first = entryForHash(this, hash);
    	HashEntry<K,V> e = first;
    	HashEntry<K,V> node = null;
    	int retries = -1; // negative while locating node
        // tryLock尝试获取锁,重复64次
    	while (!tryLock()) {
        	HashEntry<K,V> f; // to recheck first below
        	if (retries < 0) {
            	if (e == null) {
            		if (node == null) // speculatively create node
                		node = new HashEntry<K,V>(hash, key, value, null);
                	retries = 0;
            	}
            	else if (key.equals(e.key))
                	retries = 0;
            	else
                	e = e.next;
        	}
            // 重试超过64次,lock()阻塞线程直至获取到锁
        	else if (++retries > MAX_SCAN_RETRIES) {
            	lock();
           	 	break;
        	}
        	else if ((retries & 1) == 0 && (f = entryForHash(this, hash)) != first) {
            	e = first = f; // re-traverse if entry changed
            	retries = -1;
        	}
    	}
    	return node;
	}
}

(1)因为Segment本身就是一个锁,所以这里调用tryLock尝试获取锁

  • 如果获取成功,那么其他线程都无法再修改这个segment

  • 如果获取失败,会调用scanAndLockForPut方法根据key和hash尝试找到这个node,如果不存在,则创建一个node并返回,如果存在则返回null

(2)查看scanAndLockForPut源码会发现在查找的过程中会尝试获取锁,在多核CPU环境下,会尝试64次tryLock(),如果64次还没获取到,会直接调用lock(),也就是说这一步一定会获取到锁

rehash扩容的线程安全

HashMap的线程安全问题大部分出在扩容(rehash)的过程中。

ConcurrentHashMap的扩容只针对每个segment中的HashEntry数组进行扩容。

由上述put的源码可知,ConcurrentHashMap在rehash的时候是有锁的,所以在rehash的过程中,其他线程无法对segment的hash表做操作,这就保证了线程安全。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ConnorYan

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

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

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

打赏作者

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

抵扣说明:

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

余额充值