`

ConcurrentHashMap 解读(一)

阅读更多

一、核心思想

 1、锁分离技术:

ConcurrentHashMap首先将数据分成一段一段(segment)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

 2、 final 关键字保证HashEntery 对象的不变性,来降低执行读操作的线程在遍历链表期间对加锁的需求:

ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。HashEntry 中的 key,hash,next 都声明为 final 型。这意味着,不能把节点添加到链接的中间和尾部,也不能在链接的中间和尾部删除节点。这个特性可以保证:在访问某个节点时,这个节点之后的链接不会被改变。这个特性可以大大降低处理链表时的复杂性。

同时,HashEntry 类的 value 域被声明为 Volatile 型, 保证其内存可见性。在 ConcurrentHashMap 中,不允许用 unll 作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象,需要加锁后重新读入这个 value 值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。

 3、 Volatile 保证内存可见性:

由于内存可见性问题,未正确同步的情况下,写线程写入的值可能并不为后续的读线程可见,通过Volatile 变量可以保证其内存可见性问题。

 

二、类图


说明:

1、ConcurrentHashMap是由Segment数组结构组成。

 2、Segment是由HashEntry数组结构组成,并且Segment本身继承了可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色。

3、HashEntry是真正用于存储键值对数据的地方,HashEntry是一个链表结构。

三、 内部结构图:

 


 

 

说明:

一个ConcurrentHashMap里包含一个Segment数组,每一个Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

 

ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长,但是带来的好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上),所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高。

四、核心代码解读

从上面的类图可以看到整个ConcurrentHashMap提供了非常丰富的方法,下面将对核心的方法进行解读,其他的未涉及的代码,均只是核心代码的变体而已。

1、初始化:

先看一下ConcurrentHashMap中提供的常量,这些常量部分是默认值,可通过初始化的参数覆盖,剩下部分是真正意义上的常量

 

	// 默认初始容量
    static final int DEFAULT_INITIAL_CAPACITY = 16;
 	// 默认增长因子,当 table 中包含元素的个数超过了 table 数组的长度与装载因子的乘积时,将进行一次扩容。
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
	// 默认的并发等级,即是并发数量。
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;
	// 允许的最大容量,由于要求该值必须是2的指数,并且是int类型,所以1<<30是其能用的最大值。
    static final int MAXIMUM_CAPACITY = 1 << 30;
	// 默认每一个segment 的容量,必须是2的指数,至少为2, 否则分段锁失效。
    static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
	// 最大的 segment  数量,必须小于2的24次方,
    static final int MAX_SEGMENTS = 1 << 16; 
	// 在加锁前重试的次数
    static final int RETRIES_BEFORE_LOCK = 2;

 

接下去是真正需要初始化的变量,其中前三个是初始化的对象,会根据参数或者上面定义的常量来初始化,剩下三个其实是冗余的变量。都可以通过segment 数组得到。

 

	// segments的掩码值,也就是用来选择segments的key的hash值的高位
    final int segmentMask;
	// segments的偏移量
    final int segmentShift;
	// segments 数组
    final Segment<K,V>[] segments;

    transient Set<K> keySet;
    transient Set<Map.Entry<K,V>> entrySet;
    transient Collection<V> values;

 

  接下去就是初始化的代码了。。

    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // Find power-of-two sizes best matching arguments
        int sshift = 0;
        int ssize = 1;
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
        // create segments and segments[0]
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

 

可以看初始化方法是通过initialCapacity,loadFactor, concurrencyLevel三个参数来初始化segments数组,segment偏移量segmentShift,segment掩码segmentMask和每个segment里的HashEntry数组,当然这是哪个参数都有对应的默认值,所以可以缺少任意一个。

 

  • ssize:也就是segments数组的大小,取值为大于或等于concurrencyLevel的最小的2的N次方值。为了保证并发数量为concurrencyLevel,所以必须大于等于concurrencyLevel,为什么必须2的N次方在后面解释。注意concurrencyLevel的最大值是1<<16,意味着ssize最大为1<<16。
  • segmentShift:segmentShift变量在定位segment时的哈希算法里需要使用,取值为32-(ssize从1向左移位的次数)。
  • segmentMask:segmentMask变量在定位segment时的哈希算法里需要使用,是哈希运算的掩码,取值为ssize-1。

接下去先解释一下这三个数据的关系,也回答上面ssize为什么必须是2的N次方问题:这个要结合怎么去定位segment来说。先看定位segment代码,其中h表示key的hash值,为int类型,最大32位。

 

((h >>> segmentShift) & segmentMask)

 

假设ssize=2的k次方。那么segmentShift=32-k,segmentMask=(2的k次方-1);它们有什么关系呢?

h>>> segmentShift,就表示保留h的高k位的值。segmentMask=(2的k次方-1) 就意味segmentMask是由k位1组成的数,两者做&运算,结果便是segment数组的下标。当h的高K位全部为1的时候,运算结果最大=segmentMask=2的k次方-1。而segment数组的最大长度,为ssize=2的k次方,那么下标的最大值为2的k次方-1。是完全可以对应起来的。。到这里初始化了Segment的数组。接下去是初始化第一个Segment。

 

  • cap :表示一个Segment中HashEntry数组的大小。取值为大于或等于将最大容量平均到每一个Segment里面时,单个Segment最小需要包含值数量的最小的2的N次方值。
  • ConcurrentLevel一经指定,不可改变,后续如果ConcurrentHashMap的元素数量增加导致ConrruentHashMap需要扩容,ConcurrentHashMap不会增加Segment的数量,而只会增加Segment中链表数组的容量大小,这样的好处是扩容过程不需要对整个ConcurrentHashMap做rehash,而只需要对Segment里面的元素做一次rehash就可以了。
未完待续。。
 
 
 
 
参考文献
http://www.infoq.com/cn/articles/ConcurrentHashMap
http://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/index.html?ca=drs-
http://www.goldendoc.org/2011/06/juc_concurrenthashmap/
 
 
本站支持 pay for your wishes
  • 大小: 178.2 KB
  • 大小: 11.4 KB
分享到:
评论

相关推荐

    ConcurrentHashMap源码剖析

    `ConcurrentHashMap`内部维护了一个`Segment`数组,每个`Segment`对象都是一个小型的`HashTable`。数组的长度通常是2的幂次方,以便于通过位运算快速定位到对应的`Segment`。 **2. HashEntry** `HashEntry`类表示...

    ConcurrentHashMap源码级别的面试解析,适合0~2年的人员,附源码解读

    ConcurrentHashMap源码级别的面试解析,适合0~2年的人员,附源码解读,下载即可拿到md的源文档

    Brian Goetz - 构造一个更好的hashmap

    这篇文章的标题为“构造一个更好的hashmap”,该文章主要讨论了ConcurrentHashMap的数据结构及其在java.util.concurrent包中的实现方式。该话题主要涉及并发编程领域,尤其是Java语言在并发控制方面的应用。下面是对...

    JUC最详细思维导图,一次了解读写锁,可重入锁,Cas原理,volatile 关键字原理

    此外,JUC中的`ConcurrentHashMap`是一个线程安全的哈希表,适用于并发读写场景。它采用分段锁机制,每个分段独立,降低了锁竞争,提高了并发性能。`ConcurrentLinkedQueue`是一个无界线程安全队列,适合多生产者多...

    深入解读大厂java面试必考点之HashMap全套学习资料

    HashMap是Java编程语言中最常用的集合类之一,尤其在面试中,HashMap的相关知识是考察...这套学习资料应该包含了HashMap的实例分析、源码解读、常见面试题以及实战演练等内容,确保你全面掌握这一核心Java数据结构。

    java源码解读-java-src:java源码解读

    通过源码,我们可以学习如何实现线程安全的数据结构,如ConcurrentHashMap,以及如何利用synchronized、volatile和java.util.concurrent包中的高级工具来控制并发访问。 I/O流是Java处理输入输出的重要部分。在源码...

    蚂蚁java架构师(第七/八期含项目) |课件完整|完结无秘

    08HashMap与ConcurrenthashMap源码解读 09MySQL深度原理解析 10Netty深度源码解读 11SpringCloud微服务框架源码解读 12彻底搞懂分布式锁架构设计原理 13分布式数据一致性设计原理 14分布式消息中间件 15实战新零售...

    java面试题_源码解读(3题)

    在Java面试中,源码解读是一项重要的能力,它考察了开发者对Java语言底层实现的理解以及问题解决的能力。这里我们将深入探讨三道常见的Java面试题,它们涵盖了基础、并发和集合框架等方面,帮助你提升对Java源码的...

    HashMap、ConcurrenyHashMap源码解读

    hashmap源码解读,并且会对比JDK7和8实现的不同,已更新ConcurrentHashMap部分,且结合记录了多个视频的笔记。可以在https://blog.csdn.net/hancoder/article/details/105424922 获取最新笔记地址,下载过旧文件的...

    Java中HashSet的解读_.docx

    如果需要线程安全的集合,可以考虑使用ConcurrentHashMap或者Collections.synchronizedSet()对HashSet进行包装。 总的来说,Java中的HashSet是一种高效且灵活的集合类,它依赖于HashMap实现快速的元素查找和操作。...

    Java相关技术总结,包括redis,MySQL,RabbitMq,面试题总结,源码解读

    例如,深入理解HashMap和ConcurrentHashMap的实现,可以让我们更好地利用这些数据结构,避免性能瓶颈;阅读ArrayList和LinkedList的源码,有助于我们选择合适的数据结构以优化遍历和插入操作;分析LinkedList的迭代...

    一个java即时通讯软件的雏形V1.1

    开发者可能采用了Java中的synchronized关键字、Lock接口或者并发集合类如ConcurrentHashMap来确保数据访问的一致性和完整性。 局域网通信通常涉及到套接字(Socket)编程,Java的java.net.Socket和ServerSocket类...

    网易笔试题

    - **跳表**:通过构建多级索引来加速查找过程,类似于多层链表,每一层都是一个完整的链表,且下一层的节点数少于上一层。 ### 5. NP问题及其示例 **知识点解读:** NP问题是计算复杂性理论中的一个重要概念。如果...

    java源码解读-ITG-JavaBook01:Java面试高频源码解读

    《Java源码解读-ITG-JavaBook01: Java面试高频源码解读》是一部针对Java程序员面试准备的深入学习资料。在这个项目中,我们将会探索Java语言的一些核心概念和常用库的源代码,帮助开发者更好地理解Java的内部机制,...

    16 解析HashMap.txt

    HashMap、ConcurrentHashMap源码级解读,并且对比了JDK7和8实现的不同,进行了大量的解释,结合了多个学习视频

    龙果学院java并发编程完整视频

    - `ConcurrentHashMap`是一种高性能的线程安全散列表,它通过分割技术来减少锁的竞争。 - `CopyOnWriteArrayList`通过在修改时创建新数组的方式来保证线程安全,适用于读多写少的场景。 ### 实战案例分析 为了更...

    java_util_concurrent中文版pdf

    综上所述,《Java Util Concurrent中文版》详尽解读了这些关键概念和工具,帮助开发者深入理解Java并发编程,提升程序的并发性能和稳定性。通过学习这本书,开发者可以更好地应对多线程环境下的挑战,写出更加高效、...

    封装好的可以自定义的segment

    在Java中,Segment类常被用作ConcurrentHashMap的一个组成部分,以实现线程安全的并发访问。 在描述中提到的“NULL博文链接:https://zhangmingwei.iteye.com/blog/1825889”,这可能是指一篇关于自定义Segment实现...

    自定缓存, 在查询修改频率较少的数据,存入缓存中,有帮助提高效率

    在IT行业中,优化数据访问速度和提升系统性能是至关重要的任务。自定义缓存是一种常见...通过对`MyCacheManager.java`的解读,我们可以学习到如何根据实际需求构建一个高效、安全的缓存系统,从而提升软件的运行效率。

Global site tag (gtag.js) - Google Analytics