Java - 线程安全的 HashMap 实现方法及原理
转载自 http://liqianglv2005.iteye.com/blog/2025016
Java HashMap 是非线程安全的。在多线程条件下,容易导致死循环,具体表现为CPU使用率100%。因此多线程环境下保证 HashMap 的线程安全性,主要有如下几种方法:
- 使用 java.util.Hashtable 类,此类是线程安全的。
- 使用 java.util.concurrent.ConcurrentHashMap,此类是线程安全的。
- 使用 java.util.Collections.synchronizedMap() 方法包装 HashMap object,得到线程安全的Map,并在此Map上进行操作。
- 自己在程序的关键方法或者代码段加锁,保证安全性,当然这是严重的不推荐。
为什么 HashMap 非线程安全, 可以参考大神陈皓(weibo账号:左耳朵耗子)在他自己技术网站Coolshell上的文章,写的非常详细。文章链接(直通车):http://coolshell.cn/articles/9606.html
这里重点分析下上面列举的几种方法实现并行安全性的原理:
(一)java.util.Hashtable类:类的主要数据结构如下:
- /**
- * The hash table data.
- */
- private transient Entry[] table;
- private static class Entry<K,V> implements Map.Entry<K,V> {
- int hash;
- K key;
- V value;
- Entry<K,V> next;
可见,Hashtable 的实现是一个数组,每个数组元素是一个LinkList结构,因此类的数据实际上保存在一个散列表中。这个实现和 HashMap 的实现是一致的。数据结构如下:
那么Hashtable如何保证线程安全性的哪?下面是 Hashtable的源码:
- public synchronized V get(Object key) {
- Entry tab[] = table;
- …… //此处省略,具体的实现请参考 jdk实现
- }
- public synchronized V put(K key, V value) {
- …… //具体实现省略,请参考jdk实现
- }
- public synchronized V remove(Object key) {
- …… //具体实现省略,请参考jdk实现
- }
- public synchronized void putAll(Map<? extends K, ? extends V> t) {
- for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
- put(e.getKey(), e.getValue());
- }
- public synchronized void clear() {
- …… //具体实现省略,请参考jdk实现
- }
上面是 Hashtable 提供的几个主要方法,包括 get(), put(), remove() 等。注意到每个方法本身都是 synchronized 的,不会出现两个线程同时对数据进行操作的情况,因此保证了线程安全性,但是也大大的降低了执行效率。因此是不推荐的。
(二)使用 java.util.Collections.synchronizedMap(Map<K,V>) 方法进行封装。 方法源代码如下:
- public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
- return new SynchronizedMap<K,V>(m);
- }
- private static class SynchronizedMap<K,V>
- implements Map<K,V>, Serializable {
- // use serialVersionUID from JDK 1.2.2 for interoperability
- private static final long serialVersionUID = 1978198479659022715L;
- private final Map<K,V> m; // Backing Map
- final Object mutex; // Object on which to synchronize
- SynchronizedMap(Map<K,V> m) {
- if (m==null)
- throw new NullPointerException();
- this.m = m;
- mutex = this;
- }
- SynchronizedMap(Map<K,V> m, Object mutex) {
- this.m = m;
- this.mutex = mutex;
- }
- public int size() {
- synchronized(mutex) {return m.size();}
- }
- public boolean isEmpty(){
- synchronized(mutex) {return m.isEmpty();}
- }
- public boolean containsKey(Object key) {
- synchronized(mutex) {return m.containsKey(key);}
- }
从实现源代码可以发现,其封装的本质和 Hashtable 的实现是完全一致的,即对原Map本身的方法进行加锁,加锁的对象或者为外部指定共享对象mutex,或者为包装后的线程安全的Map本身。Hashtable 可以理解为 SynchronizedMap mutex=null 时候的特殊情况。因此这种同步方式的执行效率也是很低的。
既然已经有了Hashtable, 为什么还需要Collections 提供的这种静态方法包装哪?很简单,这种包装是Java Collection Framework提供的统一接口,除了用于 HashMap 外,还可以用于其他的Map。当然 除了对Map进行封装,Collections工具类还提供了对 Collection(比如Set,List)的线程安全实现封装方法,具体请参考 java.util.Colletions 实现,其原理和 SynchronizedMap 是一致的。
(三) 使用 java.util.concurrent.ConcurrentHashMap 类。并发编程大师 Doug Lea 出品,绝对精品。这是 HashMap 的线程安全版,同 Hashtable 相比,ConcurrentHashMap 不仅保证了访问的线程安全性,而且在效率上有较大的提高。
ConcurrentHashMap的数据结构如下(引用图片地址http://www.yupoo.com/photos/goldendoc/81556254/):
可以看出,相对 HashMap 和 Hashtable, ConcurrentHashMap 增加了Segment 层,每个Segment 原理上等同于一个 Hashtable, ConcurrentHashMap 为 Segment 的数组。下面是 ConcurrentHashMap 的 put 和 get 方法:
- final Segment<K,V> segmentFor(int hash) {
- return segments[(hash >>> segmentShift) & segmentMask];
- }
- public V put(K key, V value) {
- if (value == null)
- throw new NullPointerException();
- int hash = hash(key.hashCode());
- return segmentFor(hash).put(key, hash, value, false);
- }
- public V get(Object key) {
- int hash = hash(key.hashCode());
- return segmentFor(hash).get(key, hash);
- }
向 ConcurrentHashMap 中插入数据或者读取数据,首先都要讲相应的 Key 映射到对应的 Segment,因此不用锁定整个类, 只要对单个的 Segment 操作进行上锁操作就可以了。理论上如果有 n 个 Segment,那么最多可以同时支持 n 个线程的并发访问,从而大大提高了并发访问的效率。另外 rehash() 操作也是对单个的 Segment 进行的,所以由 Map 中的数据量增加导致的 rehash 的成本也是比较低的。
单个 Segment 的进行数据操作的源码如下:
- V put(K key, int hash, V value, boolean onlyIfAbsent) {
- lock();
- try {
- int c = count;
- if (c++ > threshold) // ensure capacity
- rehash();
- …… // 代码省略,具体请查看源码
- } finally {
- unlock();
- }
- }
- V replace(K key, int hash, V newValue) {
- lock();
- try {
- HashEntry<K,V> e = getFirst(hash);
- …… // 代码省略,具体请查看源码
- } finally {
- unlock();
- }
- }
可见对 单个的 Segment 进行的数据更新操作都是 加锁的,从而能够保证线程的安全性。
ConcurrentHashMap 的更具体实现和分析见(直通车) http://www.iteye.com/topic/1103980, 非常详细。
几种线程同步实现方法的效率比较,可以参考(直通车) http://blog.sina.com.cn/s/blog_734a77160100yku1.html
Java - 线程安全的 HashMap 实现方法及原理相关推荐
- java 线程安全的3种方法
java 线程安全的3种方法: 1:互斥同步 临界区:syncronized,,重入锁 信号量 semaphore 互斥量 mutex 2: 非阻塞同步 CAS(unsafe 类提供的基于CPU硬件技 ...
- 多线程下ArrayList类线程不安全的解决方法及原理
多线程下ArrayList类线程不安全的解决方法及原理 参考文章: (1)多线程下ArrayList类线程不安全的解决方法及原理 (2)https://www.cnblogs.com/fangting ...
- java future 线程 状态_手把手带你了解Java线程的实现方式及生命周期原理
前言 我们在工作中线程技术很多情况下都能用的到,而且我们在面试的时候,线程技术基本上也是必问的.今天我来从线程的实现方式以及线程的生命周期做一个全面的讲解与分析,帮助大家能更好的去了解线程技术. 概念 ...
- java thread 线程销毁_手把手带你了解Java线程的实现方式及生命周期原理
前言 我们在工作中线程技术很多情况下都能用的到,而且我们在面试的时候,线程技术基本上也是必问的.今天我来从线程的实现方式以及线程的生命周期做一个全面的讲解与分析,帮助大家能更好的去了解线程技术. 概念 ...
- Hashmap扩容方法机制原理
Hashmap扩容方法,机制原理 1.7版本: 数组额定容量为16,元素数量超过负载因子(一般为0.75)后会扩容至原有数组大小*2. 遍历老数组每个位置上的链表每个元素. 取每个元素的key根据链表 ...
- Java几种遍历集合的方法(原理,复杂度,适用场合)
目录 1. 顺序存储和链式存储 1.1 顺序存储 1.2 链式存储 1.3 区别 2. Java常用的遍历方式 2.1 for循环遍历 2.2 Iterator迭代器 2.3 foreach循环 3. ...
- JAVA线程池 之 Executors (二) 原理分析
一.线程池状态 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));private static final ...
- java线程同步的五种方法
2019独角兽企业重金招聘Python工程师标准>>> 1.同步方法 即有synchronized关键字修饰的方法. 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, ...
- java线程同步的7种方法
为何要使用同步? java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有 ...
最新文章
- The Long-Term Stability of Ecosystems
- 防止程序启动两次的方法CreateMutex()
- 天天沉迷于皇上本宫的都是sb
- P4198 楼房重建
- javascript类型注意事项
- java获取当前年月
- MFC、OpenCV初探 —— PictureControl中图片的缩放
- php mysql 简单留言板_php+mysql 最简单的留言板_PHP教程
- Keil5配置GCC编译器编译STM32工程
- linux 下防火墙开启端口
- 面向对象的三大特性之三:封装
- Java —— 正则表达式
- 30多岁的程序员仍然奋斗在一线
- ubuntu安装mysql添加密码
- c语言范式编程之lsearch
- Django__WSGI
- 全国产化 飞腾 vpx板卡
- 千人千面系统,这样搞比较靠谱【干货】
- linux 编辑模式使用sed,sed命令的用法和vim编辑器的使用
- 林轩田之机器学习课程笔记( embedding numerous feature之support vector regression)(32之22)
热门文章
- Duilib教程-简单介绍
- UDT协议实现分析——UDT初始化和销毁
- python爬虫 爬取有道翻译详解
- Kafka日志清理之Log Deletion
- [八]RabbitMQ-客户端源码之ChannelN
- MOSN 多协议扩展开发实践
- MPEG创始人、主席:MEPG商业模式已经破裂
- 鹅厂开源先锋,日均计算量超30万亿,全力打破数据墙
- nginx模块开发—HTTP初始化之listen
- error: RPC failed; curl 56 GnuTLS recv error (-54): Error in the pull function.