多线程下HashMap安全问题-ConcurrentHashMap解析

Java1.5 引入了 java.util.concurrent 包,其中 Collection 类的实现允许在运行过程中修改集合对象。实际上, Java 的集合框架是[迭代器设计模式]的一个很好的实现。
为什么需要使用 ConcurrentHashMap ?
HashMap 不是线程安全的,因此多线程操作需要注意,通常使用 HashTable 或者 Collections.synchronizedMap() 来返回线程安全的 HashMap ,但是这两种方法都是对所有方法实现同步,导致读写性能比较低,而 ConcurrentHashMap 引入“分段锁”的概念,可以理解为把一个大的 Map 差分成小的 HashTable ,根据 key.hashCode() 来决定把 key 放到哪个 HashTable 中去。
就是把 Map 分成了N个 Segment , put 和 get 的时候,都是现根据 key.hashCode() 算出放到哪个Segment中:


图片.png
图片.png
图片.png
图片.png

对比:
ConcurrentHashMap 与 HashMap 很相似,但是它支持在运行时修改集合对象。
例子:
ConcurrentHashMapExample.java
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {

public static void main(String[] args) {

    //ConcurrentHashMap
    Map<String,String> myMap = new ConcurrentHashMap<String,String>();
    myMap.put("1", "1");
    myMap.put("2", "1");
    myMap.put("3", "1");
    myMap.put("4", "1");
    myMap.put("5", "1");
    myMap.put("6", "1");
    System.out.println("ConcurrentHashMap before iterator: "+myMap);
    Iterator<String> it = myMap.keySet().iterator();

    while(it.hasNext()){
        String key = it.next();
        if(key.equals("3")) myMap.put(key+"new", "new3");
    }
    System.out.println("ConcurrentHashMap after iterator: "+myMap);

    //HashMap
    myMap = new HashMap<String,String>();
    myMap.put("1", "1");
    myMap.put("2", "1");
    myMap.put("3", "1");
    myMap.put("4", "1");
    myMap.put("5", "1");
    myMap.put("6", "1");
    System.out.println("HashMap before iterator: "+myMap);
    Iterator<String> it1 = myMap.keySet().iterator();

    while(it1.hasNext()){
        String key = it1.next();
        if(key.equals("3")) myMap.put(key+"new", "new3");
    }
    System.out.println("HashMap after iterator: "+myMap);
}

}

输出如下:
ConcurrentHashMap before iterator: {1=1, 5=1, 6=1, 3=1, 4=1, 2=1}
ConcurrentHashMap after iterator: {1=1, 3new=new3, 5=1, 6=1, 3=1, 4=1, 2=1}
HashMap before iterator: {3=1, 2=1, 1=1, 6=1, 5=1, 4=1}
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextEntry(HashMap.java:793)
at java.util.HashMap$KeyIterator.next(HashMap.java:828)
at com.test.ConcurrentHashMapExample.main(ConcurrentHashMapExample.java:44)
明显 ConcurrentHashMap 可以支持向 map 中添加新元素,而 HashMap 则抛出了 ConcurrentModificationException。

介绍 ConcurrentHashMap:
ConcurrentHashMap (简称 CHM )是在 Java 1.5作为 Hashtable 的替代选择新引入的,是 concurrent 包的重要成员。在 Java 1.5之前,如果想要实现一个可以在多线程和并发的程序中安全使用的 Map ,只能在 HashTable 和 synchronized Map 中选择,因为 HashMap 并不是线程安全的。但再引入了 CHM 之后,我们有了更好的选择。 CHM 不但是线程安全的,而且比 HashTable 和 synchronizedMap 的性能要好。相对于 HashTable 和 synchronizedMap 锁住了整个 Map , CHM 只锁住部分 Map 。 CHM 允许并发的读操作,同时通过同步锁在写操作时保持数据完整性。

Java 中 ConcurrentHashMap 的实现:
CHM 引入了分割,并提供了 HashTable 支持的所有的功能。在 CHM 中,支持多线程对 Map 做读操作,并且不需要任何的 blocking --因为 CHM 将 Map 分割成了不同的部分,在执行风行操作时只锁住一部分。根据默认的并发级别, Map 被分割成16个部分,并且由不同的锁控制。这意味着,同时最多可以有16个写线程操作 Map 。
另外一个重点是在迭代遍历 CHM 时, keySet 返回的 iterator 是弱一直和 fail-safe 的,可能不会返回某些最近的改变,并且在遍历中,如果已经遍历的数组上的内容发生了变化,是不会抛出 ConcurrentModificationException 的异常。

什么时候使用 ConcurrentHashMap ?(待求证)
CHM 适用于读者数量超过写者时,当写者数量大于等于读者时, CHM 的性能是低于 Hashtable 和 synchronized Map 的。这是因为当锁住了整个Map时,读操作要等待对同一部分执行写操作的线程结束。 CHM 适用于做 cache ,在程序启动时初始化,之后可以被多个请求线程访问。正如 Javadoc 说明的那样, CHM 是 HashTable 一个很好的替代,但要记住, CHM 的比 HashTable 的同步性稍弱。

ConcurrentHashMap 小总结:

*** CHM 允许并发的读和线程安全的更新操作
***在执行写操作时, CHM 只锁住部分的 Map 
***并发的更新是通过内部根据并发级别将 Map 分割成小部分实现的
***高的并发级别会造成时间和空间的浪费,低的并发级别在写线程多时会引起线程间的竞争
*** CHM 的所有操作都是线程安全
*** CHM 返回的迭代器是弱一致性, fail-safe 并且不会抛出 ConcurrentModificationException 异常
*** CHM 不允许 null 的键值
***可以使用 CHM 代替 HashTable,但要记住 CHM 不会锁住整个 Map

HashMap 与 ConcurrentHashMap对比:

HashMap 的结构:

image

简单来说, HashMap 是一个Entry对象的数组。数组中的每一个 Entry 元素,又是一个链表的头节点。

Hashmap 不是线程安全的。在高并发环境下做插入操作,有可能出现下面的环形链表:

image

Segment 是什么呢? Segment 本身就相当于一个 HashMap 对象。

同 HashMap 一样, Segment 包含一个 HashEntry 数组,数组中的每一个 HashEntry 既是一个键值对,也是一个链表的头节点。

单一的 Segment 结构如下:

image

像这样的 Segment 对象,在 ConcurrentHashMap 集合中有多少个呢?有2的N次方个,共同保存在一个名为 segments 的数组当中。

因此整个ConcurrentHashMap的结构如下:

image

可以说,ConcurrentHashMap 是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表。

这样的二级结构,和数据库的水平拆分有些相似。

附上 ConcurrentHashMap 遍历方式:
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**

  • Java 中遍历 Map 的四种方式,这里使用的是 ConcurrentHashMap,

  • 可以替换为 HashMap
    */
    public class IteratorMap {
    public static void main(String[] args) {
    Map<String, String> map = new ConcurrentHashMap<String, String>();
    init(map);

     //方式一:在 for-each 循环中使用 entries 来遍历  
     System.out.println("方式一:在 for-each 循环中使用 entries 来遍历");  
     for(Map.Entry<String, String> entry: map.entrySet()) {  
          System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());  
     }  
       
     //方法二:在 for-each 循环中遍历 keys 或 values ,这种方式适用于需要值或者键的情况,方法二比方法一快了10%  
     System.out.println("方法二:在 for-each 循环中遍历 keys 或 values ,这种方式适用于需要值或者键的情况");  
     //遍历键  
     for(String key : map.keySet()) {  
         System.out.println("key = " + key);  
     }  
       
     //遍历值  
     for(String value : map.values()) {  
         System.out.println("value = " + value);  
     }  
       
     //方法三:使用 Iterator 遍历,使用并发集合不会报异常,性能类似于方法二  
     //使用泛型  
     Iterator<Map.Entry<String, String>> entries = map.entrySet().iterator();    
     System.out.println("使用 Iterator 遍历,并且使用泛型:");  
     while (entries.hasNext()) {    
         
         Map.Entry<String, String> entry = entries.next();    
         
         System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());    
           
         //注意这里操作了集合,下面的的遍历不会再打印0  
         <strong><span style="color:#cc0000;">if("0".equals(entry.getKey())) {  
             map.remove(entry.getKey());  
         }</span></strong>  
     }    
       
     //不使用泛型  
     Iterator entrys = map.entrySet().iterator();    
     System.out.println("使用 Iterator 遍历,并且不使用泛型");  
     while (entrys.hasNext()) {    
         
         Map.Entry entry = (Map.Entry) entrys.next();    
         
         String key = (String)entry.getKey();    
         
         String value = (String)entry.getValue();    
         
         System.out.println("Key = " + key + ", Value = " + value);    
         
     }    
       
     //方式四:通过键找值遍历,该方法效率相当低,不建议使用  
     System.out.println("方式四:通过键找值遍历");  
     for (String key : map.keySet()) {    
             
         String value = map.get(key);    
         
         System.out.println("Key = " + key + ", Value = " + value);    
         
     }    
    

    }

    /**

    • 初始化 Map
    • @param map
      */
      private static void init(Map<String, String> map) {
      if(map == null) {
      throw new RuntimeException("参数为空,无法执行初始化");
      }
      for(int i = 0; i < 10; i ++) {
      map.put(String.valueOf(i), String.valueOf(i));
      }
      }

}

运行结果:

方式一:在 for-each 循环中使用 entries 来遍历
Key = 0, Value = 0
Key = 1, Value = 1
Key = 2, Value = 2
Key = 3, Value = 3
Key = 4, Value = 4
Key = 5, Value = 5
Key = 6, Value = 6
Key = 7, Value = 7
Key = 8, Value = 8
Key = 9, Value = 9
方法二:在 for-each 循环中遍历 keys 或 values ,这种方式适用于需要值或者键的情况
key = 0
key = 1
key = 2
key = 3
key = 4
key = 5
key = 6
key = 7
key = 8
key = 9
value = 0
value = 1
value = 2
value = 3
value = 4
value = 5
value = 6
value = 7
value = 8
value = 9
使用Iterator遍历,并且使用泛型:
Key = 0, Value = 0
Key = 1, Value = 1
Key = 2, Value = 2
Key = 3, Value = 3
Key = 4, Value = 4
Key = 5, Value = 5
Key = 6, Value = 6
Key = 7, Value = 7
Key = 8, Value = 8
Key = 9, Value = 9
使用Iterator遍历,并且不使用泛型
Key = 1, Value = 1
Key = 2, Value = 2
Key = 3, Value = 3
Key = 4, Value = 4
Key = 5, Value = 5
Key = 6, Value = 6
Key = 7, Value = 7
Key = 8, Value = 8
Key = 9, Value = 9
方式四:通过键找值遍历
Key = 1, Value = 1
Key = 2, Value = 2
Key = 3, Value = 3
Key = 4, Value = 4
Key = 5, Value = 5
Key = 6, Value = 6
Key = 7, Value = 7
Key = 8, Value = 8
Key = 9, Value = 9
实现可以参考:https://blog.csdn.net/xuefeng0707/article/details/40834595

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,695评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,569评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,130评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,648评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,655评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,268评论 1 309
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,835评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,740评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,286评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,375评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,505评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,185评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,873评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,357评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,466评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,921评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,515评论 2 359

推荐阅读更多精彩内容

  • /Library/Java/JavaVirtualMachines/jdk-9.jdk/Contents/Home...
    光剑书架上的书阅读 3,890评论 2 8
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,701评论 18 139
  • 实际上,HashSet 和 HashMap 之间有很多相似之处,对于 HashSet 而言,系统采用 Hash 算...
    曹振华阅读 2,515评论 1 37
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,664评论 18 399
  • 高尔基曾经说过,“书籍是人类进步的阶级”,书籍从书简、帛书,到后来的纸质书,再到现在的电子书……书籍在进步,它推动...
    呵呵哒hgf阅读 327评论 0 0