Java8 ConcurrentHashMap的get()方法真的不需要加锁吗?
一、前言
我们都知道,ConcurrentHashmap这个并发集合框架是线程安全的,当我们看get()方法的源码时,会发现get操作全程没有加锁。但是真的是这样的吗?本文我们就深入的看看它为什么大家都说它不需要加锁?是不是真的不需要加锁?留个悬念,在一种特殊场景下是要加锁的。
二、ConCurrentHashMap#get()方法
1、流程:
- 使用扰动函数计算key的hash值,取到其所在的数组下标位置。
- 如果节点是首节点,直接返回。
- 如果节点的
hash
值eh
是负值,说明ConCurrentHashMap正在进行扩容,或该节点是一个树节点TreeBin。
- 如果eh=-1表示正在扩容,该节点是一个 ForwardingNode(树头节点),直接调用ForwardingNode的find方法去nextTable里找;
- 如果eh=-2,该节点是一个
TreeBin
,调用TreeBin的find()方法遍历红黑树,由于红黑树有可能在变色旋转
,所以find()里会有读写锁
。
- 最后如果eh>0说明节点是链表,遍历链接即可。
public V get(Object key) {Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;// 计算key的hash值int h = spread(key.hashCode());if ((tab = table) != null && (n = tab.length) > 0 &&(e = tabAt(tab, (n - 1) & h)) != null) { // 读取首节点的Node元素// 如果当前节点的hash值和首节点的Hash值相同,并且value也相同,直接返回。if ((eh = e.hash) == h) {if ((ek = e.key) == key || (ek != null && key.equals(ek)))return e.val;}// hash为负值,表示正在扩容。这时需要用到ForwardingNode的find()方法定位到nextTable。// 1) eh=-1,说明该节点是一个ForwardingNode,正在扩容迁移,此时调用ForwardingNode的find()方法去nextTable里找// 2) eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find()方法遍历红黑树,注意:由于红黑树可能正在旋转变色,所以find()方法里会加一个读写锁。else if (eh < 0)return (p = e.find(h, key)) != null ? p.val : null;// eh >=0 说明该节点是一个链表节点,直接遍历链表即可。while ((e = e.next) != null) {if (e.hash == h &&((ek = e.key) == key || (ek != null && key.equals(ek))))return e.val;}}return null;
}
2、get()方法如何保证读取到的数据不是脏数据?
get()方法常规情况下没有加锁,却保证读到的数据不是脏数据。这是因为volatile的作用:保证可见性。
关于volatile?
volatile是Java提供的一种轻量级的同步机制,能够保证线程可见性、禁止指令重排序。也就是当给一个非引用变量值加
volatile
修饰之后,能够保证一个线程对改变量值的改变,另一个线程可以即时看到。
3、我们看一下Node#find()方法
其有四个实现:
ForwardingNode表示一个因为扩容而正在移动中的节点;
ReservationNode表示一个空节点,加锁时使用;
TreeNode表示红黑树中普通的节点;
TreeBin表示红黑树的根节点,封装了红黑树中左旋、右旋、新增节点、删除节点等维护红黑树平衡的逻辑。
我们主要看一下TreeBin#find()方法。
该方法多了一个
基于CAS实现的读写锁
;
- 通过TreeBin查找某个节点时,如果当前
写锁被占用
或者有等待获取写锁的线程
,表示红黑树处于正在调整
的过程中,则遍历链表
查找;- 如果
写锁没有被占用且没有等待的线程
,则抢占读锁
,遍历红黑树
的节点来查找;读锁释放
时会判断是否所有的读锁都释放
了,如果都释放了且当前有等待获取写锁的线程
,则唤醒
正在等待中的线程。
/*** Returns matching node or null if none. Tries to search* using tree comparisons from root, but continues linear* search when lock not available.*/
final Node<K,V> find(int h, Object k) {if (k != null) {for (Node<K,V> e = first; e != null; ) {int s; K ek;// 如果当前读写锁的状态是WAITER或者WRITER,则通过链表遍历查找,此时红黑树正在调整的过程中。if (((s = lockState) & (WAITER|WRITER)) != 0) {if (e.hash == h &&((ek = e.key) == k || (ek != null && k.equals(ek))))return e;e = e.next;}// //如果不是上述两种状态,则将状态设置为READER,每次都会加上READER,表示正在遍历红黑树,此时就不能调整红黑树了else if (U.compareAndSwapInt(this, LOCKSTATE, s,s + READER)) {TreeNode<K,V> r, p;try {// 如果根节点为null,则返回null;否则通过根节点查找匹配的节点。注意:进入此方法根节点不可能为nullp = ((r = root) == null ? null :r.findTreeNode(h, k, null));} finally {Thread w;if (U.getAndAddInt(this, LOCKSTATE, -READER) == //所有的读锁都释放了(READER|WAITER) && (w = waiter) != null) //且有等待获取写锁的线程// 唤醒该线程LockSupport.unpark(w);}return p;}}}return null;
}
三、总结
1、ConcurrentHashMap的get方法需要加锁吗?
1)java8中ConcurrentHashMap的
get()
操作正常情况是不需要加锁
的,这也是它比其他并发集合,如hashtable、用Collections.synchronizedMap()包装的hashmap;效率高的原因之一。
2)正常情况get()
操作全程不需要加锁
是因为:
- get()方法访问的大多数变量是
volatile关键字修饰
的,比如:Node.val、Node.next、count,volatile保证了其值的修改对其他线程的可见性。
即:在多线程环境下线程A修改Node节点的val或者新增节点对线程B是可见的。- 像引用类型:数组
table
用volatile修饰,在数组扩容
的时候就保证了其引用的改变
对其他线程的可见性。3)但是当节点是
红黑树
的时候,如果树正在变色旋转
并且要查询的值不是红黑树的头节点
,会加一个读写锁
。
4)另外其迭代器iterator是弱一致性
的,因为在迭代过程中可以向map中添加元素;而HashMap是强一致性
的。
最后我们再聊一下ConcurrentHashMap中变量使用final和volatile修饰的作用。
2、ConcurrentHashMap中变量使用final和volatile修饰的作用?
- final域确保
变量初始化
的安全性
,初始化安全性让不可变对象不需要同步就可以被自由的访问和共享
。 - volatile关键字保证某个变量在内存的改变对其他线程即时可见;再配合CAS可以实现无锁并发操作。
- 而get()方法可以无锁,是由于Node的元素val和指针next都是volatile修改的,在多线程环境下线程A修改节点的val或者新增节点对线程B是可见的。
Java8 ConcurrentHashMap的get()方法真的不需要加锁吗?相关推荐
- Java8 ConcurrentHashMap详解
点个赞,看一看,好习惯!本文 GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了 3 个月总结的一线大厂 Java 面试总结,本 ...
- 【java8新特性】——方法引用(四)
一.简介 方法引用是java8的新特性之一, 可以直接引用已有Java类或对象的方法或构造器.方法引用与lambda表达式结合使用,可以进一步简化代码. 来看一段简单代码: public static ...
- 【JAVA8】Map新方法,别再重复造车轮了
文章目录 getOrDefault forEach compute computeIfAbsent computeIfPresent merge putIfAbsent remove(key,valu ...
- ConcurrentHashMap的扩容方法transfer源码详解
主要细节问题: 什么时候触发扩容?扩容阈值是多少? 扩容时的线程安全怎么做的? 其他线程怎么感知到扩容状态,从而一起进行扩容? 多个线程一起扩容时,怎么拆分任务,是不是任务粒度越小越好? Concur ...
- Java8读文件的方法
JDK7中引入了新的文件操作类java.nio.file.File,它包含了很多有用的方法来操作文件,比如检查文件是否为隐藏文件,或者是检查文件是否为只读文件.开发者还可以使用Files.readAl ...
- java8 接口调用默认方法_Java8接口里的默认方法特性
在没有默认方法特性时,当你往接口中添加新方法时,接口内部所有实现的类都要历经一些修改,这将导致上千行的代码修改工作量.为了避免这点,Java8引入了默认对象方法,亦即,如果你想要往现存的接口中添加任何 ...
- 使用Java8改造出来的模板方法真的是yyds
GitHub 21.3k Star 的Java工程师成神之路,不来了解一下吗! GitHub 21.3k Star 的Java工程师成神之路,真的不来了解一下吗! 我们在日常开发中,经常会遇到类似的场 ...
- Concurrenthashmap的putIfAbsent方法
@Test public void test() { ConcurrentHashMap<String, String> map = new ConcurrentHashMap<St ...
- Java8中String.join方法,让我们的代码更优美
强烈推荐一个大神的人工智能的教程:http://www.captainbed.net/zhanghan [前言] 距Java8(14年3月19日)发布马上就四年了:相信接触过java8的人,会对它的很 ...
最新文章
- OCCI入门(VC2010下配置)
- zsh: command not found: service
- 代码面试最常用的10大算法
- go语言基础之导入包的常用方法
- 好程序员web前端分享逻辑运算
- USB转串口线突然不好用了
- Office 2003如何打开后缀名为docx的Microsoft Word 文档
- 【网页】如何下载网页中mathplayer插件中的pdf文件
- 论文阅读 A SIMPLE BUT TOUGH-TO-BEAT BASELINE FOR SEN- TENCE EMBEDDINGS
- CGB2005 JT-4(聚合工程 阿里数据源,配置项目启动项,EasyUI,树形结构,页面跳转restFul,JSON串说明,vo po,分页查询,叶子类目,Ajax嵌套,windows端口号占用)
- 基于TI DRV8424驱动步进电机实现调速和行程控制
- 高端android手机,7月Android中高端手机性能榜出炉:华为高端落榜,中端没进前三!...
- CSDN 「Markdown」编辑器的优点、不足、使用技巧和新增功能|CSDN编辑器测评
- JMokit中的@Mocked与@Injectable区别
- linux用户读取文件过程,Python中读取写入文件并进行文件与用户交互的操作
- 做母婴微商怎么线上引流?做母婴产品如何线上引流?
- 什么是生命?什么是人工智能?
- java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed
- 如何用虚拟机VMware安装win10/win7(最详细图解)
- 面试不慌,拿这70张思维导图,怒怼面试官
热门文章
- 软工导第一节课 计算机软件工程学作一个简短的概述,回顾计算机系统发展简史 软件工程的基本原理和方法有概括的本质的认识,详细讲解生命周期相关知识讲解8种典型的软件过程模型
- 程序员自由工作平台国内外汇总篇
- linux挂载img镜像文件,如何挂载.img格式的镜像
- 制作arch linux安装u盘,制作 Arch Linux 内存系统启动盘
- an tu tu html5 test,法语TEF基础阶段测试题和答案(下)
- ISP概述、工作原理及架构
- 虾皮API接口—获取商品详情
- (Talking face) EVP
- 学习笔记19—dpabi错误集
- 什么是你的核心竞争力之六正视你的弱点