前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java - 线程安全的 HashMap 实现方法及原理

Java - 线程安全的 HashMap 实现方法及原理

作者头像
allsmallpig
发布2021-02-12 09:54:13
2.8K0
发布2021-02-12 09:54:13
举报
文章被收录于专栏:allsmallpi博客

转载自 http://liqianglv2005.iteye.com/blog/2025016

Java HashMap 是非线程安全的。在多线程条件下,容易导致死循环,具体表现为CPU使用率100%。因此多线程环境下保证 HashMap 的线程安全性,主要有如下几种方法:

  1. 使用 java.util.Hashtable 类,此类是线程安全的。
  2. 使用 java.util.concurrent.ConcurrentHashMap,此类是线程安全的。
  3. 使用 java.util.Collections.synchronizedMap() 方法包装 HashMap object,得到线程安全的Map,并在此Map上进行操作。
  4. 自己在程序的关键方法或者代码段加锁,保证安全性,当然这是严重的不推荐。

为什么 HashMap 非线程安全, 可以参考大神陈皓(weibo账号:左耳朵耗子)在他自己技术网站Coolshell上的文章,写的非常详细。文章链接(直通车):http://coolshell.cn/articles/9606.html

这里重点分析下上面列举的几种方法实现并行安全性的原理:

(一)java.util.Hashtable类:类的主要数据结构如下:

Java代码  

收藏代码
收藏代码
  1. /**
  2.      * The hash table data.
  3.      */
  4. private transient Entry[] table;  
  5. private static class Entry implements Map.Entry {  
  6. int hash;  
  7.     K key;  
  8.     V value;  
  9.     Entry next;  

       可见,Hashtable 的实现是一个数组,每个数组元素是一个LinkList结构,因此类的数据实际上保存在一个散列表中。这个实现和 HashMap 的实现是一致的。数据结构如下:

      那么Hashtable如何保证线程安全性的哪?下面是 Hashtable的源码:

Java代码  

收藏代码
收藏代码
  1. public synchronized V get(Object key) {  
  2.     Entry tab[] = table;  
  3.     …… //此处省略,具体的实现请参考 jdk实现
  4.     }  
  5. public synchronized V put(K key, V value) {  
  6.     …… //具体实现省略,请参考jdk实现
  7.     }  
  8. public synchronized V remove(Object key) {  
  9.     …… //具体实现省略,请参考jdk实现
  10.     }  
  11. public synchronized void putAll(Mapextends K, ? extends V> t) {  
  12. for (Map.Entryextends K, ? extends V> e : t.entrySet())  
  13.             put(e.getKey(), e.getValue());  
  14.     }  
  15. public synchronized void clear() {  
  16.     …… //具体实现省略,请参考jdk实现
  17.     }  

     上面是 Hashtable 提供的几个主要方法,包括 get(), put(), remove() 等。注意到每个方法本身都是 synchronized 的,不会出现两个线程同时对数据进行操作的情况,因此保证了线程安全性,但是也大大的降低了执行效率。因此是不推荐的。

(二)使用 java.util.Collections.synchronizedMap(Map) 方法进行封装。 方法源代码如下:

Java代码  

收藏代码
收藏代码
  1. public static  Map synchronizedMap(Map m) {  
  2. return new SynchronizedMap(m);  
  3.     }  
  4. private static class SynchronizedMap
  5. implements Map, Serializable {  
  6. // use serialVersionUID from JDK 1.2.2 for interoperability
  7. private static final long serialVersionUID = 1978198479659022715L;  
  8. private final Map m;     // Backing Map
  9. final Object      mutex;    // Object on which to synchronize
  10.     SynchronizedMap(Map m) {  
  11. if (m==null)  
  12. throw new NullPointerException();  
  13. this.m = m;  
  14.             mutex = this;  
  15.         }  
  16.     SynchronizedMap(Map m, Object mutex) {  
  17. this.m = m;  
  18. this.mutex = mutex;  
  19.         }  
  20. public int size() {  
  21. synchronized(mutex) {return m.size();}  
  22.         }  
  23. public boolean isEmpty(){  
  24. synchronized(mutex) {return m.isEmpty();}  
  25.         }  
  26. public boolean containsKey(Object key) {  
  27. synchronized(mutex) {return m.containsKey(key);}  
  28.         }  

       从实现源代码可以发现,其封装的本质和 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 方法:

Java代码  

收藏代码
收藏代码
  1. final Segment segmentFor(int hash) {  
  2. return segments[(hash >>> segmentShift) & segmentMask];  
  3.     }  
  4. public V put(K key, V value) {  
  5. if (value == null)  
  6. throw new NullPointerException();  
  7. int hash = hash(key.hashCode());  
  8. return segmentFor(hash).put(key, hash, value, false);  
  9.     }  
  10. public V get(Object key) {  
  11. int hash = hash(key.hashCode());  
  12. return segmentFor(hash).get(key, hash);  
  13.     }  

向 ConcurrentHashMap 中插入数据或者读取数据,首先都要讲相应的 Key 映射到对应的 Segment,因此不用锁定整个类, 只要对单个的 Segment 操作进行上锁操作就可以了。理论上如果有 n 个 Segment,那么最多可以同时支持 n 个线程的并发访问,从而大大提高了并发访问的效率。另外 rehash() 操作也是对单个的 Segment 进行的,所以由 Map 中的数据量增加导致的 rehash 的成本也是比较低的。

单个 Segment 的进行数据操作的源码如下:

Java代码  

收藏代码
收藏代码
  1. V put(K key, int hash, V value, boolean onlyIfAbsent) {  
  2.             lock();  
  3. try {  
  4. int c = count;  
  5. if (c++ > threshold) // ensure capacity
  6.                     rehash();  
  7.                  …… // 代码省略,具体请查看源码                
  8.             } finally {  
  9.                 unlock();  
  10.             }  
  11.         }  
  12. V replace(K key, int hash, V newValue) {  
  13.             lock();  
  14. try {  
  15.                 HashEntry e = getFirst(hash);  
  16.                  …… // 代码省略,具体请查看源码   
  17.             } finally {  
  18.                 unlock();  
  19.             }  
  20.         }  

   可见对 单个的 Segment 进行的数据更新操作都是 加锁的,从而能够保证线程的安全性。

ConcurrentHashMap 的更具体实现和分析见(直通车) http://www.iteye.com/topic/1103980, 非常详细。

几种线程同步实现方法的效率比较,可以参考(直通车)  http://blog.sina.com.cn/s/blog_734a77160100yku1.html

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018/01/31 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档