一、核心思想
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就可以了。
相关推荐
`ConcurrentHashMap`内部维护了一个`Segment`数组,每个`Segment`对象都是一个小型的`HashTable`。数组的长度通常是2的幂次方,以便于通过位运算快速定位到对应的`Segment`。 **2. HashEntry** `HashEntry`类表示...
ConcurrentHashMap源码级别的面试解析,适合0~2年的人员,附源码解读,下载即可拿到md的源文档
这篇文章的标题为“构造一个更好的hashmap”,该文章主要讨论了ConcurrentHashMap的数据结构及其在java.util.concurrent包中的实现方式。该话题主要涉及并发编程领域,尤其是Java语言在并发控制方面的应用。下面是对...
此外,JUC中的`ConcurrentHashMap`是一个线程安全的哈希表,适用于并发读写场景。它采用分段锁机制,每个分段独立,降低了锁竞争,提高了并发性能。`ConcurrentLinkedQueue`是一个无界线程安全队列,适合多生产者多...
HashMap是Java编程语言中最常用的集合类之一,尤其在面试中,HashMap的相关知识是考察...这套学习资料应该包含了HashMap的实例分析、源码解读、常见面试题以及实战演练等内容,确保你全面掌握这一核心Java数据结构。
通过源码,我们可以学习如何实现线程安全的数据结构,如ConcurrentHashMap,以及如何利用synchronized、volatile和java.util.concurrent包中的高级工具来控制并发访问。 I/O流是Java处理输入输出的重要部分。在源码...
08HashMap与ConcurrenthashMap源码解读 09MySQL深度原理解析 10Netty深度源码解读 11SpringCloud微服务框架源码解读 12彻底搞懂分布式锁架构设计原理 13分布式数据一致性设计原理 14分布式消息中间件 15实战新零售...
在Java面试中,源码解读是一项重要的能力,它考察了开发者对Java语言底层实现的理解以及问题解决的能力。这里我们将深入探讨三道常见的Java面试题,它们涵盖了基础、并发和集合框架等方面,帮助你提升对Java源码的...
hashmap源码解读,并且会对比JDK7和8实现的不同,已更新ConcurrentHashMap部分,且结合记录了多个视频的笔记。可以在https://blog.csdn.net/hancoder/article/details/105424922 获取最新笔记地址,下载过旧文件的...
如果需要线程安全的集合,可以考虑使用ConcurrentHashMap或者Collections.synchronizedSet()对HashSet进行包装。 总的来说,Java中的HashSet是一种高效且灵活的集合类,它依赖于HashMap实现快速的元素查找和操作。...
例如,深入理解HashMap和ConcurrentHashMap的实现,可以让我们更好地利用这些数据结构,避免性能瓶颈;阅读ArrayList和LinkedList的源码,有助于我们选择合适的数据结构以优化遍历和插入操作;分析LinkedList的迭代...
开发者可能采用了Java中的synchronized关键字、Lock接口或者并发集合类如ConcurrentHashMap来确保数据访问的一致性和完整性。 局域网通信通常涉及到套接字(Socket)编程,Java的java.net.Socket和ServerSocket类...
- **跳表**:通过构建多级索引来加速查找过程,类似于多层链表,每一层都是一个完整的链表,且下一层的节点数少于上一层。 ### 5. NP问题及其示例 **知识点解读:** NP问题是计算复杂性理论中的一个重要概念。如果...
《Java源码解读-ITG-JavaBook01: Java面试高频源码解读》是一部针对Java程序员面试准备的深入学习资料。在这个项目中,我们将会探索Java语言的一些核心概念和常用库的源代码,帮助开发者更好地理解Java的内部机制,...
HashMap、ConcurrentHashMap源码级解读,并且对比了JDK7和8实现的不同,进行了大量的解释,结合了多个学习视频
- `ConcurrentHashMap`是一种高性能的线程安全散列表,它通过分割技术来减少锁的竞争。 - `CopyOnWriteArrayList`通过在修改时创建新数组的方式来保证线程安全,适用于读多写少的场景。 ### 实战案例分析 为了更...
综上所述,《Java Util Concurrent中文版》详尽解读了这些关键概念和工具,帮助开发者深入理解Java并发编程,提升程序的并发性能和稳定性。通过学习这本书,开发者可以更好地应对多线程环境下的挑战,写出更加高效、...
在Java中,Segment类常被用作ConcurrentHashMap的一个组成部分,以实现线程安全的并发访问。 在描述中提到的“NULL博文链接:https://zhangmingwei.iteye.com/blog/1825889”,这可能是指一篇关于自定义Segment实现...
在IT行业中,优化数据访问速度和提升系统性能是至关重要的任务。自定义缓存是一种常见...通过对`MyCacheManager.java`的解读,我们可以学习到如何根据实际需求构建一个高效、安全的缓存系统,从而提升软件的运行效率。