java8中ConcurrentHashMap
HashMap的线程不安全主要体现在resize时的死循环及使用迭代器时的fast-fail上。
Fast-fail:
fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
在使用迭代器的过程中如果HashMap被修改,那么ConcurrentModificationException将被抛出,也即Fast-fail策略。
当HashMap的iterator()方法被调用时,会构造并返回一个新的EntryIterator对象,并将EntryIterator的expectedModCount设置为HashMap的modCount(该变量记录了HashMap被修改的次数)。
在通过该Iterator的next方法访问下一个Entry时,它会先检查自己的expectedModCount与HashMap的modCount是否相等,如果不相等,说明HashMap被修改,直接抛出ConcurrentModificationException
。
该Iterator的remove方法也会做类似的检查。
解决办法:
多线程条件下,可使用Collections.synchronizedMap
方法构造出一个同步Map,或者直接使用线程安全的ConcurrentHashMap。
java7中的ConcurrentHashMap:
基于散列的Map,并不是将每个方法都在同一个锁上同步使得每次只能有一个线程访问线程,而使用一种更细粒度的加锁机制来实现更大程度的共享。这种机制称为分段锁(Lock Striping)。在这种机制中,任意数量的读取线程可以并发地访问map,执行读取操作的线程和执行写入操作的线程可以并发地访问map,并且一定数量的写入线程可以并发地修改map。ConcurrentHashMap带来的结果是,在并发访问的环境下将实现更高的吞吐量,而在单线程环境中只损失非常小的性能。
ConcurrentHashMap与其他并发容器一起增强了同步容器类:它们提供的迭代器不会抛出ConcurrentModificationException,因此不需要在迭代过程中对容器加锁。
ConcurrentHashMap返回的迭代器具有弱一致性。弱一致性的迭代器可以容忍并发的修改,当创建迭代器时会遍历已有的元素,并可以(但是不保证)在迭代器被构造后将修改操作反应给容器。
与 Hashtable 和 synchronizedMap 相比,ConcurrentHashMap 有更多的优势以及更少的劣势。因此在大多数情况下,用 ConcurrentHashMap 来代替同步 Map 能进一步提高代码的可伸缩性。只有当应用程序需要加锁Map 以进行独占访问时,才能放弃使用 ConcurrentHashMap。
ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
concurrencyLevel:并行级别(Segment 的数量),默认是 16。
也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。
这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。
Segment :
count:Segment 里的键值对数量。
loadFactor:负载因子,默认0.75.
modCount:结构发生变化的次数。
threshold:临界值,当Segment 里的键值对数量超过这个值时,引发Segment的扩容。
put方法:
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
// 1. 计算 key 的 hash 值
int hash = hash(key);
// 2. 根据 hash 值找到 Segment 数组中的位置 j
// hash 是 32 位,无符号右移 segmentShift(28) 位
// 然后和 segmentMask(15) 做一次与操作,也就是说 j 是 hash 值的最后 4 位,也就是槽的数组下标
int j = (hash >>> segmentShift) & segmentMask;
// 刚刚说了,初始化的时候初始化了 segment[0],但是其他位置还是 null,
// ensureSegment(j) 对 segment[j] 进行初始化
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
// 3. 插入新值到 槽 s 中
return s.put(key, hash, value, false);
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 在往该 segment 写入前,需要先获取该 segment 的独占锁
// 先看主流程,后面还会具体介绍这部分内容
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
// 这个是 segment 内部的数组
HashEntry<K,V>[] tab = table;
// 再利用 hash 值,求应该放置的数组下标
int index = (tab.length - 1) & hash;
// first 是数组该位置处的链表的表头
HashEntry<K,V> first = entryAt(tab, index); // 下面这串 for 循环虽然很长,不过也很好理解,想想该位置没有任何元素和已经存在一个链表这两种情况
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
// 覆盖旧值
e.value = value;
++modCount;
}
break;
}
// 继续顺着链表走
e = e.next;
}
else {
// node 到底是不是 null,这个要看获取锁的过程,不过和这里都没有关系。
// 如果不为 null,那就直接将它设置为链表表头;如果是null,初始化并设置为链表表头。
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first); int c = count + 1;
// 如果超过了该 segment 的阈值,这个 segment 需要扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node); // 扩容后面也会具体分析
else
// 没有达到阈值,将 node 放到数组 tab 的 index 位置,
// 其实就是将新的节点设置成原链表的表头
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
// 解锁
unlock();
}
return oldValue;
}
put会将新的值插入到链表头
初始化ensureSegment:
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
// 这里看到为什么之前要初始化 segment[0] 了,
// 使用当前 segment[0] 处的数组长度和负载因子来初始化 segment[k]
// 为什么要用“当前”,因为 segment[0] 可能早就扩容过了
Segment<K,V> proto = ss[0];
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf); // 初始化 segment[k] 内部的数组
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // 再次检查一遍该槽是否被其他线程初始化了。 Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
// 使用 while 循环,内部用 CAS,当前线程成功设值或其他线程成功设值后,退出
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
获取写入锁scanAndLockForPut:
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 // 循环获取锁 失败则循环 成功则跳出循环
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
// 进到这里说明数组该位置的链表是空的,没有任何元素
// 当然,进到这里的另一个原因是 tryLock() 失败,所以该槽存在并发,不一定是该位置
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
// 顺着链表往下走
e = e.next;
}
// 重试次数如果超过 MAX_SCAN_RETRIES(单核1多核64),那么不抢了,进入到阻塞队列等待锁
// lock() 是阻塞方法,直到获取锁后返回
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
// 这个时候是有大问题了,那就是有新的元素进到了链表,成为了新的表头
// 所以这边的策略是,相当于重新走一遍这个 scanAndLockForPut 方法
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
获取锁时,并不直接使用lock来获取,因为该方法获取锁失败时会挂起。
如果tryLock获取锁失败,说明锁被其它线程占用,此时通过循环再次以tryLock的方式申请锁。
如果在循环过程中该Key所对应的链表头被修改,则重置retry次数。如果retry次数超过一定值,则使用lock方法申请锁。
扩容rehash:
put 的时候,如果判断该值的插入会导致该 segment 的元素个数超过阈值,那么先进行扩容,再插值。
扩容后,容量为原来的 2 倍。
get方法:
get 过程中是没有加锁的,除非读到的值是空的才会加锁重读.
ConcurrentHashMap使用如下方法保证可见性,取得最新的Segment:Segment<K,V> s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)
get方法里将要使用的共享变量都定义成volatile,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值
之所以不会读到过期的值,是根据java内存模型的happen before原则,对volatile字段的写入操作先于读操作
size方法:
ConcurrentHashMap会在不上锁的前提逐个Segment计算3次size,如果某相邻两次计算,所有Segment的更新次数之和相等,说明这两次计算过程中无更新操作,则这两次计算出的总size相等,可直接作为最终结果返回。
如果这三次计算过程中Map有更新,则对所有Segment加锁重新计算Size。
ConcurrentHashMap与HashMap相比,有以下不同点:
ConcurrentHashMap线程安全,而HashMap非线程安全
HashMap允许Key和Value为null,而ConcurrentHashMap不允许
HashMap不允许通过Iterator遍历的同时通过HashMap修改,而ConcurrentHashMap允许该行为,并且该更新对后续的遍历可见
Java 8基于CAS的ConcurrentHashMap:
Java 7引入了Segment这一结构,实现了分段锁,理论上最大并发度与Segment个数相等。
Java 8为进一步提高并发性,摒弃了分段锁的方案,而是直接使用一个大的数组。
同时为了提高哈希碰撞下的寻址性能,Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(long(N)))。
put方法:
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// 得到 hash 值
int hash = spread(key.hashCode());
// 用于记录相应链表的长度
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 如果数组"空",进行数组初始化
if (tab == null || (n = tab.length) == 0)
// 初始化数组,后面会详细介绍
tab = initTable(); // 找该 hash 值对应的数组下标,得到第一个节点 f
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 如果数组该位置为空,
// 用一次 CAS 操作将这个新值放入其中即可,这个 put 操作差不多就结束了,可以拉到最后面了
// 如果 CAS 失败,那就是有并发操作,进到下一个循环就好了
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// hash 居然可以等于 MOVED,这个需要到后面才能看明白,不过从名字上也能猜到,肯定是因为在扩容
else if ((fh = f.hash) == MOVED)
// 帮助数据迁移,这个等到看完数据迁移部分的介绍后,再理解这个就很简单了
tab = helpTransfer(tab, f); else { // 到这里就是说,f 是该位置的头结点,而且不为空 V oldVal = null;
// 获取数组该位置的头结点的监视器锁
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) { // 头结点的 hash 值大于 0,说明是链表
// 用于累加,记录链表的长度
binCount = 1;
// 遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 如果发现了"相等"的 key,判断是否要进行值覆盖,然后也就可以 break 了
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
// 到了链表的最末端,将这个新值放到链表的最后面
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) { // 红黑树
Node<K,V> p;
binCount = 2;
// 调用红黑树的插值方法插入新节点
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// binCount != 0 说明上面在做链表操作
if (binCount != 0) {
// 判断是否要将链表转换为红黑树,临界值和 HashMap 一样,也是 8
if (binCount >= TREEIFY_THRESHOLD)
// 这个方法和 HashMap 中稍微有一点点不同,那就是它不是一定会进行红黑树转换,
// 如果当前数组的长度小于 64,那么会选择进行数组扩容,而不是转换为红黑树
// 具体源码我们就不看了,扩容部分后面说
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//
addCount(1L, binCount);
return null;
}
对于put操作,如果Key对应的数组元素为null,则通过CAS操作将其设置为当前值。
如果Key对应的数组元素(也即链表表头或者树的根元素)不为null,则对该元素使用synchronized关键字申请锁,然后进行操作。
如果该put操作使得当前链表长度超过一定阈值,则将该链表转换为树,从而提高寻址效率。
get方法:
对于读操作,由于数组被volatile关键字修饰,因此不用担心数组的可见性问题。
同时每个元素是一个Node实例(Java 7中每个元素是一个HashEntry),它的Key值和hash值都由final修饰,不可变更,无须关心它们被修改后的可见性问题。
而其Value及对下一个元素的引用由volatile修饰,可见性也有保障。
size方法:
put方法和remove方法都会通过addCount方法维护Map的size。
size方法通过sumCount获取由addCount方法维护的Map的size。
java8中ConcurrentHashMap的更多相关文章
- Java8 中 ConcurrentHashMap工作原理的要点分析
简介: 本文主要介绍Java8中的并发容器ConcurrentHashMap的工作原理,和其它文章不同的是,本文重点分析了不同线程的各类并发操作如get,put,remove之间是如何同步的,以及这些 ...
- Java7与Java8中的HashMap和ConcurrentHashMap知识点总结
JAVA7 Java7的ConcurrentHashMap里有多把锁,每一把锁用于其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率呢.这 ...
- Java 8 中 ConcurrentHashMap工作原理的要点分析
简介: 本文主要介绍Java8中的并发容器ConcurrentHashMap的工作原理,和其它文章不同的是,本文重点分析了对不同线程的各类并发操作如get,put,remove之间是如何同步的,以及这 ...
- Java7 和 Java8 中的 ConcurrentHashMap 原理解析
Java7 中 ConcurrentHashMap ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些. 整个 ConcurrentHash ...
- java7,java8 中HashMap和ConcurrentHashMap简介
一:Java7 中的HashMap 结构: HashMap 里面是一个数组,然后数组中每个元素是一个单向链表.链表中每个元素称为一个Entry 实例,Entry 包含四个属性:key, value, ...
- java8中map的meger方法的使用
java8中map有一个merge方法使用示例: /** * 打印出包含号码集的label的集合 * * @param args */ public static void main(String[] ...
- 020-并发编程-java.util.concurrent之-jdk6/7/8中ConcurrentHashMap、HashMap分析
一.概述 哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表. 是根据关键码值(Key ...
- Java8中的HashMap分析
本篇文章是网上多篇文章的精华的总结,结合自己看源代码的一些感悟,其中线程安全性和性能测试部分并未做实践测试,直接是“拿来”网上的博客的. 哈希表概述 哈希表本质上一个数组,数组中每一个元素称为一个箱子 ...
- java8中CAS的增强
注:ifeve.com的同名文章为本人所发,此文在其基础做了些调整.转载请注明出处! 一.java8中CAS的增强 前些天,我偶然地将之前写的用来测试AtomicInteger和synchronize ...
随机推荐
- BZOJ2458 Beijing2011最小三角形(分治)
类似于平面最近点对,考虑分治,即分别计算分割线两侧的最小三角形再考虑跨过线的三角形. 复杂度证明也是类似的,对于某一个点,在另一侧可能与其构成最小三角形的点在一个d*d/2的矩形内(两边之和大于第三边 ...
- MT【93】二次函数衣服一件
注:最后一行中$f(\dfrac{-x_1}{2})$应改为$f(\dfrac{-a}{2})$.有空再重新编辑.
- 【刷题】LOJ 2587 「APIO2018」铁人两项
题目描述 比特镇的路网由 \(m\) 条双向道路连接的 \(n\) 个交叉路口组成. 最近,比特镇获得了一场铁人两项锦标赛的主办权.这场比赛共有两段赛程:选手先完成一段长跑赛程,然后骑自行车完成第二段 ...
- 【贪心策略】USACO 越野跑
问题 H: 越野跑[贪心策略] [题面] 为了能在下一次跑步比赛中有好的发挥,贝茜在一条山路上开始了她的训练.贝茜希望能在每次训练中跑得尽可能远,不过她也知道农场中的一条规定:奶牛独自进山的时间不得超 ...
- 洛谷 P2542 [AHOI2005]航线规划 解题报告
P2542 [AHOI2005]航线规划 题目描述 对Samuel星球的探险已经取得了非常巨大的成就,于是科学家们将目光投向了Samuel星球所在的星系--一个巨大的由千百万星球构成的Samuel星系 ...
- 解题:NOI 2010 航空管制
题面 常见的套路与不常见的套路 第一问是常见的套路,建反边用优先队列跑拓扑排序 第二问是不常见的套路,如何判断一个点最早什么时候起飞?先不加它来拓扑排序,直到拓扑排序不能进行下去了,这个时刻就是它必须 ...
- svn cleanup
SVN 本地更新时,由于一些操作中断更新,如磁盘空间不够,用户取消. 可能会造成本地文件被锁定的情况.一般出现这种情况的解决方法: 1.可以使用SVN clean up来清除锁定. 2.如果不是本目录 ...
- opencv ---getRotationMatrix2D函数
getRotationMatrix2D函数 主要用于获得图像绕着 某一点的旋转矩阵 Mat getRotationMatrix2D(Point2f center, double angle, dou ...
- git 创建空提交
git commit --allow-empty -m "Empty Commit to setup deployments"
- keepalived 配置文件参数详解
global_defs 全局配置vrrpd 1. vrrp_script添加一个周期性执行的脚本.脚本的退出状态码会被调用它的所有的VRRP Instance记录. 2. vrrp_sync_grou ...