深入了解ConcurrentHashMap
在上一篇文章【简单了解系列】从基础的使用来深挖HashMap里,我从最基础的使用中介绍了HashMap,大致是JDK1.7和1.8中底层实现的变化,和介绍了为什么在多线程下可能会造成死循环,扩容机智是什么样的。感兴趣的可以先看看。
我们知道,HashMap是非线程安全的容器,那么为什么ConcurrentHashMap能够做到线程安全呢?
底层结构
首先看一下ConcurrentHashMap的底层数据结构,在Java8中,其底层的实现方式与HashMap一样的,同样是数组、链表再加红黑树,具体的可以参考上面的HashMap的文章,下面所有的讨论都是基于Java 1.8。
transient volatile Node<K,V>[] table;
volatile关键字
对比HashMap的底层结构可以发现,table的定义中多了一个volatile关键字。这个关键字是做什么的呢?我们知道所有的共享变量都存在主内存中,就像table。
而线程对变量的所有操作都必须在线程自己的工作内存中完成,而不能直接读取主存中的变量,这是JMM的规定。所以每个线程都会有自己的工作内存,工作内存中存放了共享变量的副本。而正是因为这样,才造成了可见性的问题。
ABCD四个线程同时在操作一个共享变量X,此时如果A从主存中读取了X,改变了值,并且写回了内存。那么BCD线程所得到的X副本就已经失效了。此时如果没有被volatile修饰,那么BCD线程是不知道自己的变量副本已经失效了。继续使用这个变量就会造成数据不一致的问题。
内存可见性
而如果加上了volatile关键字,BCD线程就会立马看到最新的值,这就是内存可见性。你可能想问,凭什么加了volatile的关键字就可以保证共享变量的内存可见性?
那是因为如果变量被volatile修饰,在线程进行写操作时,会直接将新的值写入到主存中,而不是线程的工作内存中;而在读操作时,会直接从主存中读取,而不是线程的工作内存。
基础使用
首先这个使用与HashMap没有任何区别,只是实现改成了ConcurrentHashMap。
Map<String, String> map = new ConcurrentHashMap<>();
map.put("微信搜索", "SH的全栈笔记");
map.get("微信搜索"); // SH的全栈笔记
取值
首先我们来看一下get方法的使用,源码如下。
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
大概解释一下这个过程发生了什么,首先根据key计算出哈希值,如果找到了就直接返回值。如果是红黑树的话,就在红黑树中查找值,否则就按照链表的查找方式查找。
这与HashMap也差不多的,元素会首先以链表的方式进行存储,如果该桶中的元素数量大于TREEIFY_THRESHOLD的值,就会触发树化。将当前的链表转换为红黑树。因为如果数量太多的话,链表的查询效率就会变得非常低,时间复杂度为O(n),而红黑树的查询时间复杂度则为O(logn),这个阈值在Java 1.8中的默认值为8,定义如下。
static final int TREEIFY_THRESHOLD = 8;
赋值
put的源码就不放出来了,放在这大家估计也不会一行一行的去看。所以我就简单的解释一下put的过程发生了什么事,并贴上关键代码就好了。
整个过程,除开并发的一些细节,大致的流程和1.8中的HashMap是差不多的。
首先会根据传入的key计算出hashcode,如果是第一次被赋值,那自然需要进行初始化table 如果这个key没有存在过,直接用CAS在当前槽位的头节点创建一个Node,会用自旋来保证成功 如果当前的Node的hashcode是否等于-1,如果是则证明有其它的线程正在执行扩容操作,当前线程就加入到扩容的操作中去 且如果该槽位(也就是桶)上的数据结构如果是链表,则按照链表的插入方式,直接接在当前的链表的后面。如果数量大于了树化的阈值就会转为红黑树。 如果这个key存在,就会直接覆盖。 判断是否需要扩容
看到这你可能会有一堆的疑问。
例如在多线程的情况下,几个线程同时来执行put操作时,怎么保证只执行一次初始化,或者怎么保证只执行一次扩容呢?万一我已经写入了数据,另一个线程又初始化了一遍,岂不是造成了数据不一致的问题。同样是多线程的情况下, 怎么保证put值的时候不会被其他线程覆盖。CAS又是什么?
接下来我们就来看一下在多线程的情况下,ConcurrentHashMap是如何保证线程安全的。
初始化的线程安全
首先我们来看初始化的源码。
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
可以看到有一个关键的变量,sizeCtl,其定义如下。
private transient volatile int sizeCtl;
sizeCtl使用了关键字volatile修饰,说明这是一个多线程的共享变量,可以看到如果是首次初始化,第一个判断条件if ((sc = sizeCtl) < 0)是不会满足的,正常初始化的话sizeCtl的值为0,初始化设定了size的话sizeCtl的值会等于传入的size,而这两个值始终是大于0的。
CAS
然后就会进入下面的U.compareAndSwapInt(this, SIZECTL, sc, -1)方法,这就是上面提到的CAS,Compare and Swap(Set),比较并交换,Unsafe是位于sun.misc下的一个类,在Java底层用的比较多,它让Java拥有了类似C语言一样直接操作内存空间的能力。
例如可以操作内存、CAS、内存屏障、线程调度等等,但是如果Unsafe类不能被正确使用,就会使程序变的不安全,所以不建议程序直接使用它。
compareAndSwapInt的四个参数分别是,实例、偏移地址、预期值、新值。偏移地址可以快速帮我们在实例中定位到我们要修改的字段,此例中便是sizeCtl。如果内存当中的sizeCtl是传入的预期值,则将其更新为新的值。这个Unsafe类的方法可以保证这个操作的原子性。当你在使用parallelStream进行并发的foreach遍历时,如果涉及到修改一个整型的共享变量时,你肯定不能直接用i++,因为在多线程下,i++每次操作不能保证原子性。所以你可能会用到如下的方式。
AtomicInteger num = new AtomicInteger();
arr.parallelStream().forEach(item -> num.getAndIncrement());
你可能会好奇,为什么使用了AtomicInteger就可以保证原子性,跟Unsafe类和CAS又有什么关系,让我们接着往下,看getAndIncrement方法的底层实现。
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
可以看到,底层调用的是Unsafe类的方法,这不就联系上了吗,而getAndIncrement的实现又长这样。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
没错,这里底层调用了compareAndSwapInt方法。可以看到这里加了while,如果该方法返回false就一直循环,直到成功为止。这个过程有个
深入了解ConcurrentHashMap的更多相关文章
- Java集合---ConcurrentHashMap原理分析
集合是编程中最常用的数据结构.而谈到并发,几乎总是离不开集合这类高级数据结构的支持.比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap).这篇文章主 ...
- ConcurrentHashMap
ConcurrentHashMap是Java5中新增加的一个线程安全的Map集合,可以用来替代HashTable.对于ConcurrentHashMap是如何提高其效率的,可能大多人只是知道它使用了多 ...
- ConcurrentHashMap内存泄漏问题
问题背景 上周,同事写了一段ConcurrentHashMap的测试代码,说往map里放了32个元素就内存溢出了,我大致看了一下他的代码及运行的jvm参数,觉得很奇怪,于是就自己捣鼓了一下.首先上一段 ...
- Example of ConcurrentHashMap in Java--转
原文地址:http://www.concretepage.com/java/example_concurrenthashmap_java On this page we will provide ex ...
- Java ConcurrentHashMap Example and Iterator--转
原文地址:http://www.journaldev.com/122/java-concurrenthashmap-example-iterator#comment-27448 Today we wi ...
- 【JUC】JDK1.8源码分析之ConcurrentHashMap(一)
一.前言 最近几天忙着做点别的东西,今天终于有时间分析源码了,看源码感觉很爽,并且发现ConcurrentHashMap在JDK1.8版本与之前的版本在并发控制上存在很大的差别,很有必要进行认真的分析 ...
- ConcurrentHashMap和HashMap的一点区别
HashMap不是线程安全的,ConcurrentHashMap则在某一个方法的执行上是线程安全的. package testMap; import java.util.HashMap; public ...
- 【转】HashMap、TreeMap、Hashtable、HashSet和ConcurrentHashMap区别
转自:http://blog.csdn.net/paincupid/article/details/47746341 一.HashMap和TreeMap区别 1.HashMap是基于散列表实现的,时间 ...
- HashMap与ConcurrentHashMap的区别
从JDK1.2起,就有了HashMap,正如前一篇文章所说,HashMap不是线程安全的,因此多线程操作时需要格外小心. 在JDK1.5中,伟大的Doug Lea给我们带来了concurrent包,从 ...
- Java集合——ConcurrentHashMap
集合是编程中最常用的数据结构.而谈到并发,几乎总是离不开集合这类高级数据结构的支持.比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap).这篇文章主 ...
随机推荐
- P1364 医院设置(树型结构)
传送门闷闷闷闷闷闷 ~~放一个可爱的输入框.~~ 考虑在O(n)的时间内求数以每个节点为医院的距离和. \(设想一下,如果我们已知以1为根节点的距离和f[1],如何求出子节点呢?\) 当医院从1转换到 ...
- Spring官网阅读(十一)ApplicationContext详细介绍(上)
文章目录 ApplicationContext 1.ApplicationContext的继承关系 2.ApplicationContext的功能 Spring中的国际化(MessageSource) ...
- SSM家庭财务管理系统
包含[项目源码+论文]:http://mp.toutiao.com/preview_article/?pgc_id=6805534721838154254
- leetcode485——最大连续1的个数(easy)
一.题目描述 给定一个二进制数组, 计算其中最大连续1的个数. 示例 1: 输入: [1,1,0,1,1,1] 输出: 3 解释: 开头的两位和最后的三位都是连续1,所以最大连续1的个数是 3. 注意 ...
- 多线程测试时的辅助类--CountDownLatch
多线程时,很多时候由于mian线程与多线程结束时间不可控,造成无法测试 辅助测试类---CountDownLatch 我看的视频教程匿名内部类无法使用外部的变量,所以CountDownLatch定义为 ...
- 进程和线程—Python多线程编程
进程和线程 进程 进程是一个执行中的程序.每个进程都拥有自己的地址空间.内存.数据栈以及其它用于跟踪执行的辅助数据. 一个程序运行就是一个进程(比如 QQ.微信或者其它软件): 进程可以通过派生新的进 ...
- CTR学习笔记&代码实现5-深度ctr模型 DeepCrossing -> DCN
之前总结了PNN,NFM,AFM这类两两向量乘积的方式,这一节我们换新的思路来看特征交互.DeepCrossing是最早在CTR模型中使用ResNet的前辈,DCN在ResNet上进一步创新,为高阶特 ...
- python解析excel中图片+提取图片
解析表格是常用的技术.但是有些表各里面有图片怎么办?我想获得表格里面的图片,值得注意的是,图片没有位置信息,所以最好给图片进行编号,编号代表位置. 下面附上提取表格里面图片的代码.只要输出表格地址,和 ...
- CSS像素与绝对像素
之前在电视的webview上投放广告页面时,遇到了个问题,就是视窗大小和文档大小不一致.最后发现原来有CSS Pixel这个概念,搜集了一些资料,希望能把这个问题捋捋清楚. 首先提出一个大家常常会忽略 ...
- 当Tomcat遇上Netty
故事背景 嘀嘀嘀~,生产事故,内存泄漏! 昨天下午,突然收到运维的消息,分部某系统生产环境内存泄漏了,帮忙排查一下. 排查过程 第一步,要日志 分部给到的异常日志大概是这样(鉴于公司规定禁止截图禁止拍 ...