本文转自:http://www.importnew.com/21396.html

面试时被问到HashMap是否是线程安全的,如何在线程安全的前提下使用HashMap,其实也就是HashMapHashtableConcurrentHashMapsynchronized Map的原理和区别。当时有些紧张只是简单说了下HashMap不是线程安全的;Hashtable线程安全,但效率低,因为是Hashtable是使用synchronized的,所有线程竞争同一把锁;而ConcurrentHashMap不仅线程安全而且效率高,因为它包含一个segment数组,将数据分段存储,给每一段数据配一把锁,也就是所谓的锁分段技术。当时忘记了synchronized Map和解释一下HashMap为什么线程不安全,现在总结一下:

为什么HashMap是线程不安全的

总说HashMap是线程不安全的,不安全的,不安全的,那么到底为什么它是线程不安全的呢?要回答这个问题就要先来简单了解一下HashMap源码中的使用的存储结构(这里引用的是Java 8的源码,与7是不一样的)和它的扩容机制

HashMap的内部存储结构

下面是HashMap使用的存储结构:

  1. transient Node<K,V>[] table;
  2.  
  3. static class Node<K,V> implements Map.Entry<K,V> {
  4. final int hash;
  5. final K key;
  6. V value;
  7. Node<K,V> next;
  8. }

可以看到HashMap内部存储使用了一个Node数组(默认大小是16),而Node类包含一个类型为Node的next的变量,也就是相当于一个链表,所有hash值相同(即产生了冲突)的key会存储到同一个链表里,大概就是下面图的样子(顺便推荐个在线画图的网站Creately)。
HashMap内部存储结果

需要注意的是,在Java 8中如果hash值相同的key数量大于指定值(默认是8)时使用平衡树来代替链表,这会将get()方法的性能从O(n)提高到O(logn)。具体的可以看我的另一篇博客Java 8中HashMap和LinkedHashMap如何解决冲突

HashMap的自动扩容机制

HashMap内部的Node数组默认的大小是16,假设有100万个元素,那么最好的情况下每个hash桶里都有62500个元素,这时get(),put(),remove()等方法效率都会降低。为了解决这个问题,HashMap提供了自动扩容机制,当元素个数达到数组大小loadFactor后会扩大数组的大小,在默认情况下,数组大小为16,loadFactor为0.75,也就是说当HashMap中的元素超过16\0.75=12时,会把数组大小扩展为2*16=32,并且重新计算每个元素在新数组中的位置。如下图所示(图片来源,权侵删)。

自动扩容

从图中可以看到没扩容前,获取EntryE需要遍历5个元素,扩容之后只需要2次。

为什么线程不安全

个人觉得HashMap在并发时可能出现的问题主要是两方面,首先如果多个线程同时使用put方法添加元素,而且假设正好存在两个put的key发生了碰撞(hash值一样),那么根据HashMap的实现,这两个key会添加到数组的同一个位置,这样最终就会发生其中一个线程的put的数据被覆盖。第二就是如果多个线程同时检测到元素个数超过数组大小*loadFactor,这样就会发生多个线程同时对Node数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给table,也就是说其他线程的都会丢失,并且各自线程put的数据也丢失。
关于HashMap线程不安全这一点,《Java并发编程的艺术》一书中是这样说的:

HashMap在并发执行put操作时会引起死循环,导致CPU利用率接近100%。因为多线程会导致HashMap的Node链表形成环形数据结构,一旦形成环形数据结构,Node的next节点永远不为空,就会在获取Node时产生死循环。

哇塞,听上去si不si好神奇,居然会产生死循环。。。。google了一下,才知道死循环并不是发生在put操作时,而是发生在扩容时。详细的解释可以看下面几篇博客:

如何线程安全的使用HashMap

了解了HashMap为什么线程不安全,那现在看看如何线程安全的使用HashMap。这个无非就是以下三种方式:

  • Hashtable
  • ConcurrentHashMap
  • Synchronized Map

例子:

  1. //Hashtable
  2. Map<String, String> hashtable = new Hashtable<>();
  3.  
  4. //synchronizedMap
  5. Map<String, String> synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>());
  6.  
  7. //ConcurrentHashMap
  8. Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();

依次来看看。

Hashtable

先稍微吐槽一下,为啥命名不是HashTable啊,看着好难受,不管了就装作它叫HashTable吧。这货已经不常用了,就简单说说吧。HashTable源码中是使用synchronized来保证线程安全的,比如下面的get方法和put方法:

  1. public synchronized V get(Object key) {
  2. // 省略实现
  3. }
  4. public synchronized V put(K key, V value) {
  5. // 省略实现
  6. }

所以当一个线程访问HashTable的同步方法时,其他线程如果也要访问同步方法,会被阻塞住。举个例子,当一个线程使用put方法时,另一个线程不但不可以使用put方法,连get方法都不可以,好霸道啊!!!so~~,效率很低,现在基本不会选择它了。

ConcurrentHashMap

ConcurrentHashMap(以下简称CHM)是JUC包中的一个类,Spring的源码中有很多使用CHM的地方。之前已经翻译过一篇关于ConcurrentHashMap的博客,如何在java中使用ConcurrentHashMap,里面介绍了CHM在Java中的实现,CHM的一些重要特性和什么情况下应该使用CHM。需要注意的是,上面博客是基于Java 7的,和8有区别,在8中CHM摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法,有时间会重新总结一下。

SynchronizedMap

看了一下源码,SynchronizedMap的实现还是很简单的。

  1. // synchronizedMap方法
  2. public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
  3. return new SynchronizedMap<>(m);
  4. }
  5. // SynchronizedMap类
  6. private static class SynchronizedMap<K,V>
  7. implements Map<K,V>, Serializable {
  8. private static final long serialVersionUID = 1978198479659022715L;
  9.  
  10. private final Map<K,V> m; // Backing Map
  11. final Object mutex; // Object on which to synchronize
  12.  
  13. SynchronizedMap(Map<K,V> m) {
  14. this.m = Objects.requireNonNull(m);
  15. mutex = this;
  16. }
  17.  
  18. SynchronizedMap(Map<K,V> m, Object mutex) {
  19. this.m = m;
  20. this.mutex = mutex;
  21. }
  22.  
  23. public int size() {
  24. synchronized (mutex) {return m.size();}
  25. }
  26. public boolean isEmpty() {
  27. synchronized (mutex) {return m.isEmpty();}
  28. }
  29. public boolean containsKey(Object key) {
  30. synchronized (mutex) {return m.containsKey(key);}
  31. }
  32. public boolean containsValue(Object value) {
  33. synchronized (mutex) {return m.containsValue(value);}
  34. }
  35. public V get(Object key) {
  36. synchronized (mutex) {return m.get(key);}
  37. }
  38.  
  39. public V put(K key, V value) {
  40. synchronized (mutex) {return m.put(key, value);}
  41. }
  42. public V remove(Object key) {
  43. synchronized (mutex) {return m.remove(key);}
  44. }
  45. // 省略其他方法
  46. }

从源码中可以看出调用synchronizedMap()方法后会返回一个SynchronizedMap类的对象,而在SynchronizedMap类中使用了synchronized同步关键字来保证对Map的操作是线程安全的。

性能对比

这是要靠数据说话的时代,所以不能只靠嘴说CHM快,它就快了。写个测试用例,实际的比较一下这三种方式的效率(源码来源),下面的代码分别通过三种方式创建Map对象,使用ExecutorService来并发运行5个线程,每个线程添加/获取500K个元素。

  1. public class CrunchifyConcurrentHashMapVsSynchronizedMap {
  2.  
  3. public final static int THREAD_POOL_SIZE = 5;
  4.  
  5. public static Map<String, Integer> crunchifyHashTableObject = null;
  6. public static Map<String, Integer> crunchifySynchronizedMapObject = null;
  7. public static Map<String, Integer> crunchifyConcurrentHashMapObject = null;
  8.  
  9. public static void main(String[] args) throws InterruptedException {
  10.  
  11. // Test with Hashtable Object
  12. crunchifyHashTableObject = new Hashtable<>();
  13. crunchifyPerformTest(crunchifyHashTableObject);
  14.  
  15. // Test with synchronizedMap Object
  16. crunchifySynchronizedMapObject = Collections.synchronizedMap(new HashMap<String, Integer>());
  17. crunchifyPerformTest(crunchifySynchronizedMapObject);
  18.  
  19. // Test with ConcurrentHashMap Object
  20. crunchifyConcurrentHashMapObject = new ConcurrentHashMap<>();
  21. crunchifyPerformTest(crunchifyConcurrentHashMapObject);
  22.  
  23. }
  24.  
  25. public static void crunchifyPerformTest(final Map<String, Integer> crunchifyThreads) throws InterruptedException {
  26.  
  27. System.out.println("Test started for: " + crunchifyThreads.getClass());
  28. long averageTime = 0;
  29. for (int i = 0; i < 5; i++) {
  30.  
  31. long startTime = System.nanoTime();
  32. ExecutorService crunchifyExServer = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
  33.  
  34. for (int j = 0; j < THREAD_POOL_SIZE; j++) {
  35. crunchifyExServer.execute(new Runnable() {
  36. @SuppressWarnings("unused")
  37. @Override
  38. public void run() {
  39.  
  40. for (int i = 0; i < 500000; i++) {
  41. Integer crunchifyRandomNumber = (int) Math.ceil(Math.random() * 550000);
  42.  
  43. // Retrieve value. We are not using it anywhere
  44. Integer crunchifyValue = crunchifyThreads.get(String.valueOf(crunchifyRandomNumber));
  45.  
  46. // Put value
  47. crunchifyThreads.put(String.valueOf(crunchifyRandomNumber), crunchifyRandomNumber);
  48. }
  49. }
  50. });
  51. }
  52.  
  53. // Make sure executor stops
  54. crunchifyExServer.shutdown();
  55.  
  56. // Blocks until all tasks have completed execution after a shutdown request
  57. crunchifyExServer.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
  58.  
  59. long entTime = System.nanoTime();
  60. long totalTime = (entTime - startTime) / 1000000L;
  61. averageTime += totalTime;
  62. System.out.println("2500K entried added/retrieved in " + totalTime + " ms");
  63. }
  64. System.out.println("For " + crunchifyThreads.getClass() + " the average time is " + averageTime / 5 + " ms\n");
  65. }
  66. }

测试结果:

  1. Test started for: class java.util.Hashtable
  2. 2500K entried added/retrieved in 2018 ms
  3. 2500K entried added/retrieved in 1746 ms
  4. 2500K entried added/retrieved in 1806 ms
  5. 2500K entried added/retrieved in 1801 ms
  6. 2500K entried added/retrieved in 1804 ms
  7. For class java.util.Hashtable the average time is 1835 ms
  8.  
  9. Test started for: class java.util.Collections$SynchronizedMap
  10. 2500K entried added/retrieved in 3041 ms
  11. 2500K entried added/retrieved in 1690 ms
  12. 2500K entried added/retrieved in 1740 ms
  13. 2500K entried added/retrieved in 1649 ms
  14. 2500K entried added/retrieved in 1696 ms
  15. For class java.util.Collections$SynchronizedMap the average time is 1963 ms
  16.  
  17. Test started for: class java.util.concurrent.ConcurrentHashMap
  18. 2500K entried added/retrieved in 738 ms
  19. 2500K entried added/retrieved in 696 ms
  20. 2500K entried added/retrieved in 548 ms
  21. 2500K entried added/retrieved in 1447 ms
  22. 2500K entried added/retrieved in 531 ms
  23. For class java.util.concurrent.ConcurrentHashMap the average time is 792 ms

以上可以发现:CHM性能是明显优于Hashtable和SynchronizedMap的,CHM花费的时间比前两个的一半还少。

如何线程安全的使用HashMap的更多相关文章

  1. HashMap、HashTable、ConcurrentHashMap、HashSet区别 线程安全类

    HashMap专题:HashMap的实现原理--链表散列 HashTable专题:Hashtable数据存储结构-遍历规则,Hash类型的复杂度为啥都是O(1)-源码分析 Hash,Tree数据结构时 ...

  2. 如何使用线程安全的HashMap

    转载:https://blog.csdn.net/qq_31493821/article/details/78855069 HashMap为什么线程不安全 导致HashMap线程不安全的原因可能有两种 ...

  3. HashMap和Hashtable 线程安全性

    HashMap和Hashtable的比较是Java面试中的常见问题,用来考验程序员是否能够正确使用集合类以及是否可以随机应变使用多种思路解决问题.HashMap的工作原理.ArrayList与Vect ...

  4. Java并发基础08. 造成HashMap非线程安全的原因

    在前面我的一篇总结(6. 线程范围内共享数据)文章中提到,为了数据能在线程范围内使用,我用了 HashMap 来存储不同线程中的数据,key 为当前线程,value 为当前线程中的数据.我取的时候根据 ...

  5. HashMap实现原理及源码分析

    哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常出 ...

  6. JAVA中的线程安全与非线程安全

    原文:http://blog.csdn.net/xiao__gui/article/details/8934832 ArrayList和Vector有什么区别?HashMap和HashTable有什么 ...

  7. hashmap实现原理浅析

    看了下JAVA里面有HashMap.Hashtable.HashSet三种hash集合的实现源码,这里总结下,理解错误的地方还望指正 HashMap和Hashtable的区别 HashSet和Hash ...

  8. (转)Java集合框架:HashMap

    来源:朱小厮 链接:http://blog.csdn.net/u013256816/article/details/50912762 Java集合框架概述 Java集合框架无论是在工作.学习.面试中都 ...

  9. 【转】HashMap、TreeMap、Hashtable、HashSet和ConcurrentHashMap区别

    转自:http://blog.csdn.net/paincupid/article/details/47746341 一.HashMap和TreeMap区别 1.HashMap是基于散列表实现的,时间 ...

随机推荐

  1. delphi中Case语法的使用方法

    Case 语句If...Then…Else 语句适合选项较少的情况,如果有很多选项的话利用If 语句就比较麻烦,在这种情况下,Case 语句就容易多了.Case 语句的语法如下: case <表 ...

  2. 使用springBoot进行快速开发

    springBoot项目是spring的一个子项目,使用约定由于配置的思想省去了以往在开发过程中许多的配置工作(其实使用springBoot并不是零配置,只是使用了注解完全省去了XML文件的配置),达 ...

  3. fluentValidation集成到autofac

    废话不说直接上代码 // 首先实现ValidatorFactory public class DependencyResolverValidatorFactory : ValidatorFactory ...

  4. ehcache加载配置文件ehcache.xml的源码

    package net.sf.ehcache.config; public final class ConfigurationFactory { public static Configuration ...

  5. Javaweb程序打包或exe执行文件

    java程序的打包与发布 这里主要是讲解一下怎样将 Java程序打包成独立运行的exe程序包,以下这种方法应该是最佳的解决方案了.NetDuke的EXE程序包了是使用这种方案制作的.在操作步骤上还是比 ...

  6. JavaIOC框架篇之Spring Framework

    欢迎查看Java开发之上帝之眼系列教程,如果您正在为Java后端庞大的体系所困扰,如果您正在为各种繁出不穷的技术和各种框架所迷茫,那么本系列文章将带您窥探Java庞大的体系.本系列教程希望您能站在上帝 ...

  7. LOL TGP更新影响VS debug 问题

    刚才看群里说到VS无法调试,出现"无法使用xxx附加到应用程序'webdev.webserver...'"的问题,群友提出自己的经历,可能是LOL TGP的问题. 提问者卸载了TG ...

  8. 缓存策略 半自动化就是mybaitis只支持数据库查出的数据映射到pojo类上,而实体到数据库的映射需要自己编写sql语句实现,相较于hibernate这种完全自动化的框架我更喜欢mybatis

    springboot入门(三)-- springboot集成mybatis及mybatis generator工具使用 - FoolFox - CSDN博客 https://blog.csdn.net ...

  9. Silverlight中ListBox的数据绑定

    在Silverlight中ListBox是一个非常强大的控件.总结下ListBox的绑定数据的方式. 首先,新建一个Book类, public class Book { public string B ...

  10. Logback配置讲解

    复制文件并粘贴到项目下: logback.xml: <?xml version="1.0" encoding="UTF-8"?> <confi ...