爪哇中的ConcurrentHashMap

爪哇中的ConcurrentHashMap与HashTable非常相似,但是它提供了更好的并发级别。
您可能知道,您可以同步 哈希图 使用Collections.synchronizedMap(Map)。因此,ConcurrentHashMap和Collections.synchronizedMap(Map)之间有什么区别?对于Collections.synchronizedMap(Map),它锁定整个HashTable对象,但是在ConcurrentHashMap中,它仅锁定一部分对象。您将在以后的部分中了解它。
另一个区别是,如果我们尝试在迭代时修改ConcurrentHashMap,则ConcurrentHashMap不会引发ConcurrentModification异常。
爪哇示例中的ConcurrentHashMap:
1. 国家.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
包 组织.Arpit.爪哇2blog; 上市 类 国家 { 串 名称; 长 人口; 上市 国家(串 名称, 长 人口) { 超(); 这个.名称 = 名称; 这个.人口 = 人口; } 上市 串 得到Name() { 返回 名称; } 上市 虚空 setName(串 名称) { 这个.名称 = 名称; } 上市 长 得到Population() { 返回 人口; } 上市 虚空 setPopulation(长 人口) { 这个.人口 = 人口; } @覆写 上市 整型 杂凑Code() { 最后 整型 主要 = 31; 整型 结果 = 1; 结果 = 主要 * 结果 + ((名称 == 空值) ? 0 : 名称.杂凑Code()); 返回 结果; } @覆写 上市 布尔值 等于(目的 对象) { 国家 其他 = (国家) 对象; 如果 (名称.等于IgnoreCase((其他.名称))) 返回 真正; 返回 假; } } |
2. 并发哈希图Structure.java(主班)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
进口 爪哇.实用程序.哈希图; 进口 爪哇.实用程序.迭代器; 上市 类 并发哈希图Structure { / ** * @作者Arpit Mandliya * / 上市 静态的 虚空 主要(串[] args) { 国家 印度=新 国家(“印度”,1000); 国家 日本=新 国家(“日本”,10000); 国家 法国=新 国家(“法国”,2000); 国家 俄国=新 国家(“俄国”,20000); 并发哈希图<国家,串> 国家CapitalMap=新 并发哈希图<国家,串>(); 国家CapitalMap.放(印度,“德里”); 国家CapitalMap.放(日本,“东京”); 国家CapitalMap.放(法国,“巴黎”); 国家CapitalMap.放(俄国,“莫斯科”); 迭代器 国家CapitalIter=国家CapitalMap.键Set().迭代器();//将调试点放在这一行 而(国家CapitalIter.hasNext()) { 国家 国家Obj=国家CapitalIter.下一页(); 串 首都=国家CapitalMap.得到(国家Obj); 系统.出.打印(国家Obj.得到Name()+“ ----”+首都); } } } |
-
- 有一个称为segments的Segment []数组,其大小为16。
- 它还有两个变量,分别是segmentShift和segmentMask。
- 此细分存储细分类别’的对象。 并发哈希图类具有一个称为Segment的内部类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/ ** *段是哈希表的专用版本。 This * 重入锁的子类是偶然的,只是 *简化一些锁定,避免单独构造。 * / 静态的 最后 类 分割<K,V> 延伸 重入锁 实施 可序列化 { / ** *每个细分表。 */ 短暂的 易挥发的 哈希输入<K,V>[] 表; //其他方法和变量 } |
现在让我们展开位于索引3的线段对象。
在上图中,您可以看到每个Segment类在逻辑上都包含一个HashMap。
这是table [](HashEntry类的数组)的大小:2ˆk>=(容量/段数)
它在名为HashEntry的类中存储一个键值对,该类类似于HashMap中的Entry类。
1 2 3 4 5 6 7 8 |
静态的 最后 类 哈希输入<K,V> { 最后 K 键; 最后 整型 杂凑; 易挥发的 V 值; 最后 哈希输入<K,V> 下一页; } |
当我们说时,ConcurrentHashMap仅锁定它的一部分,实际上它锁定了Segment。因此,如果两个线程在同一ConcurrentHashMap中写入不同的段,则它允许写入操作而不会发生任何冲突。
因此,细分仅用于写操作。在读取操作的情况下,它允许完全并发,并使用易失性变量提供最新更新的值。
现在,当您了解ConcurrentHashMap的内部结构时,将更容易理解put函数。
并发级别的概念:
创建ConcurrentHashMap对象时,可以在构造函数中传递ConcurrencyLevel。 ConcurrencyLevel定义”估计要写入ConcurrentHashMap的线程数”。默认ConcurrencyLevel为16。这就是为什么在上面创建的ConcurrentHashMap中获得16个Segments对象的原因。
段的实际数量将等于ConcurrencyLevel中定义的2的下一个幂。
例如:
让s say you have defined ConcurrencyLevel as 5, so 8 分割s 对象ect will be created as 8=2^3 so 3 higher bits of 键will be used to find 指数 of the segment
另一个例子: 您希望10个线程应该能够同时访问ConcurrentHashMap,所以您将ConcurrencyLevel定义为10,因此将创建16个Segments为16 = 2 ^ 4,因此将使用Key的4个高位查找该分段的索引
输入:
放Entry的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
/ ** *将指定的键映射到此表中的指定值。 * Neither the 键 nor the 值 can be 空值. * *可以通过调用<tt>get</tt> method * with a 键 that is equal to the original 键. * * @param 键与指定值关联的键 * @param与指定键关联的值 * @返回与之关联的先前值<tt>key</tt>, or * <tt>null</tt>如果没有映射<tt>key</tt> * @throws 空指针异常如果指定的键或值是null * / 上市 V 放(K 键, V 值) { 如果 (值 == 空值) 扔 新 空指针异常(); 整型 杂凑 = 杂凑(键.杂凑Code()); 返回 segmentFor(杂凑).放(键, 杂凑, 值, 假); } / ** *返回应用于给定哈希值的键的段 * @param 杂凑 the 杂凑 code for the 键 * @返回段 * / 最后 分割 segmentFor(整型 杂凑) { 返回 段[(杂凑 >>> segmentShift) & segmentMask]; } //将方法放入细分中: V 放(K 键, 整型 杂凑, V 值, 布尔值 onlyIfAbsent) { 锁(); 尝试 { 整型 c = 计数; 如果 (c++ > 阈) //确保容量 重新整理(); 哈希输入[] 标签 = 表; 整型 指数 = 杂凑 & (标签.长度 - 1); 哈希输入 第一 = 标签[指数]; 哈希输入 e = 第一; 而 (e != 空值 && (e.杂凑 != 杂凑 || !键.等于(e.键))) e = e.下一页; V 旧值; 如果 (e != 空值) { 旧值 = e.值; 如果 (!onlyIfAbsent) e.值 = 值; } 其他 { 旧值 = 空值; ++modCount; 标签[指数] = 新 哈希输入(键, 杂凑, 第一, 值); 计数 = c; //写易失性 } 返回 旧值; } 最后 { 开锁(); } } |
当我们将任何键值对添加到ConcurrentHashMap时,将执行以下步骤:
- 在ConcurrentHashMap中,键不能为null。键’的hashCode方法用于计算哈希码
- 键‘的HashCode方法可能写得不好,因此Java开发人员又添加了一个方法hash(类似于HashMap),应用了另一个hash()函数并计算了hashcode。
- 现在我们需要首先找到线段的索引,要使用给定的键查找线段,可以使用上面的SegmentFor方法。
- 获得细分后,我们使用细分’put方法。将键值对放入Segment时,它获取锁,因此没有其他线程可以进入此块,然后使用哈希在HashEntry数组中找到索引&(tab.length-1).
- 如果您仔细观察,则细分’的put方法类似于HashMap’s 放 method.
放IfAbsent:
1 2 3 4 5 6 7 |
如果 (地图.得到(键)==空值) 返回 地图.放(键, 值); 其他 返回 地图.得到(键); |
- 线程1将值放入ConcurrentHashMap中。
- 同时,线程2试图从ConcurrentHashMap中读取(获取)值,并且可能返回null,因此它可能会覆盖线程1放入ConcurrentHashMap中的内容。
可能不需要上述行为,因此ConcurrentHashMap具有putIfAbsent方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/ * * {@inheritDoc} * * @返回与指定键关联的先前值, * or <tt>null</tt>如果没有映射the 键 * @throws 空指针异常如果指定的键或值是null */ 上市 V 放IfAbsent(K 键, V 值) { 如果 (值 == 空值) 扔 新 空指针异常(); 整型 杂凑 = 杂凑(键.杂凑Code()); 返回 segmentFor(杂凑).放(键, 杂凑, 值, 真正); } |
得到 Entry:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
/ ** * Returns the 值 to which the specified 键 is 地图ped, *或{@code 空值}(如果此映射不包含键的映射)。 * * More formally, 如果 这个 地图 contains a 地图ping from a 键 *将{@code k}设置为值{@code v},以使{@code 键.equals(k)}, *然后此方法返回{@code v};否则返回 * {@code 空值}. (There can be at most one such 地图ping.) * * @throws 空指针异常 如果 the specified 键 is 空值 * / 上市 V 得到(目的 键) { 整型 杂凑 = 杂凑(键.杂凑Code()); 返回 segmentFor(杂凑).得到(键, 杂凑); } / *地图方法的专门实现* / // 得到 method in 分割: V 得到(目的 键, 整型 杂凑) { 如果 (计数 != 0) { //易读 哈希输入<K,V> e = 得到First(杂凑); 而 (e != 空值) { 如果 (e.杂凑 == 杂凑 && 键.等于(e.键)) { V v = e.值; 如果 (v != 空值) 返回 v; 返回 readValueUnderLock(e); //重新检查 } e = e.下一页; } } 返回 空值; } |
从ConcurrentHashMap获得价值是直截了当的。
- Calculate 杂凑 using 键 ‘s Hashcode
- Use segmentFor to 得到 指数 of 分割.
- 使用区隔’s 得到函数获取对应于键的值。
- 如果在ConcurrentHashMap中找不到值,则它将锁定细分并再次尝试获取值。
最佳实践:
1 2 3 |
并发哈希图 ch=新 并发哈希图(16,0.75f,2); |
如果您的写入线程很少而读取线程很多,那么ConcurrentHashMap的性能会更好。
那’Java中的所有aboutConcurrentHashMap。
请通过 经验丰富的Java核心面试问题 了解更多面试问题。
Comments
这个主题的出色描述。
如果您能解释更多关于SegmentFor方法的原理
找到了您的博客。真的很棒 爪哇编程。对于初学者来说,这是一个很棒的教程。真的很喜欢。感谢您提供的所有信息。
真的很好,对面试很有帮助。
在Java 8之前,Java 并发哈希图的实现曾经具有Segment数组,每个段都有自己的锁,而Java 8对此锁进行了更改。’自己的内置同步监视器。在这里阅读更多 //netjs.blogspot.com/2016/01/concurrenthashmap-in-java.html