【问题标题】:Is there a problem if I read a HashMap from many threads?如果我从多个线程中读取 HashMap,会不会有问题?
【发布时间】:2021-03-19 22:35:43
【问题描述】:

我有许多带有他自己的哈希图的服务,我的哈希图是不可变的,它们在初始化程序块中被初始化。所以所有线程都将读取哈希图,但从不写它们。

我的服务是 Spring 的组件,所以它们是“Spring Singletons”。

为此使用 HashMap 有什么问题吗?我应该使用其他类吗?为什么?

这是我在说什么的一个例子:

@Service
public class TrackingServiceOne extends TrackingService {

    
Map<Integer, String> mapCreditos = new HashMap<Integer, String>();
Map<Integer, String> mapDeudas = new HashMap<Integer, String>();
Map<Integer, String> mapEjemplo = new HashMap<Integer, String>();

{

    mapCreditos.put(1, "hi");
    mapCreditos.put(2, "example");
    // And like this I will populate each map.

}



// My methods will read the maps, never write them.
    
}

【问题讨论】:

  • HashMap 不是线程安全的。你应该考虑ConcurrentHashMapSynchronizedMap
  • 你应该没问题,只要 HashMap 永远不会被写入,否则考虑使用 ConcurrentHashMap。
  • 如果它是只读的,HashMap 没问题,但我会在构造函数中实例化。如果地图会发生变异,请考虑 ConcurrentHashMap
  • @Idos 如果HashMap 已正确发布且发布后从未写入,则不需要。

标签: java spring multithreading hashmap


【解决方案1】:

HashMap 对于只读并发访问是安全的。确保在线程启动之前初始化 hashmap,并且只要没有其他线程写入它们,它们就可以在多个线程中使用而没有任何竞争条件。

【讨论】:

    【解决方案2】:

    原则上并发使用HashMap是没有问题的,只要你真的确保没有修改!

    随着代码随着时间的推移而演变,您应该考虑使用Collections.unmodifiableMap() 包装地图,以确保以后没有人可以修改。为此,由构造函数初始化可能是最简单的方法。

    另外,请确保您既不将地图本身也不会将任何部分(如 keySet()values())暴露在课堂之外。

    【讨论】:

    • 那个方法到底是做什么的?最后一句话,你的意思是把这个字段设为私有?
    • @NicolasVillacorta 方法 unmodifiableMap() 在传递的地图周围创建一个包装器,通过抛出异常拒绝任何修改。当然,该字段必须是私有的,但 keySet() 和 values() 会暴露地图的内部,因此不应从外部看到,原因与该字段应该是私有的相同。
    【解决方案3】:

    Map.copyOf

    正如其他答案所说,任何Map 对于只读访问都应该是线程安全的。为确保地图仅用于读取而非写入,请使用无法修改的地图。

    制作unmodifiable map 的现代方法是在Java 10 及更高版本中使用Map.copyOf

    使用临时对象填充您的HashMap。然后制作一个不可修改的副本,作为对象的成员字段。

    Map<Integer, String> mapCreditos = Map.copyOf( myTemporaryHashMap ) ;
    

    【讨论】:

      【解决方案4】:

      是的,从形式上讲,当您在线程中读取 HashMap 而不是用于写入时,可能会出现问题。见下文...

      有一种流行的二分法:数据竞争和竞争条件(例如,参见https://dzone.com/articles/race-condition-vs-data-race)。由于 HashMap#get() 不会修改 HashMap 的状态,正如其他答案所说,您在同时读取时没有竞争条件。但是,您也必须避免数据竞赛

      显然,您必须保证在您的读者开始阅读之前完成所有写入。这意味着您需要在写入和读取之间实现线性化。在实践中/就 java 代码而言,这将导致写入和读取之前的一种 HB (https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.5)。举几个例子:

      • 地图填写完毕后,在最后一个字段中保存对地图的引用 (https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.5)
      • Thread#start 产生 HB 边缘,因此,如果您自己管理线程,请将填充的映射传递给读取器线程,然后才调用它们的 start()。你应该从作者线程开始传递和开始
      • 读取线程正在等待CountDownLatch 并在映射填充后写入线程调用countDown()。读者开始使用地图。
      • 也可以使用 ForkJoinPool/ExecutorService,因为 if 会在后台生成所需的 HB。执行/调度一个任务填图,只有任务完成后,才执行/调度所有的reader。
      • 等等……

      所以,如果你真的保证(从 JMM 的角度来看)最后一次写入在任何读取开始之前完成 - 恭喜,你也没有数据竞争。

      第一个例子似乎就像你的情况......即使 Spring 在引擎盖下做了一些无形的魔法,从而导致你的地图的正确可见性(没有人可以证明,是吗?:)),我更喜欢遵循规范和明确的 JMM 安全发布:

      @Service
      public class TrackingServiceOne extends TrackingService {
              
          private final Map<Integer, String> mapCreditos;
          private final Map<Integer, String> mapDeudas;
          private final Map<Integer, String> mapEjemplo;
      
          {
              Map<Integer, String> mapCreditosTemp = new HashMap<Integer, String>();
      
              mapCreditosTemp.put(1, "hi");
              mapCreditosTemp.put(2, "example");
      
              mapCreditos = mapCreditosTemp; // publishing to the final field produces the freeze action, which guaranty visibility of the field for readers
          }
       ...   
      }
      

      以下不太明确的形式也可以,因为如果我们在类中定义了至少一个 final 字段,它的构造函数会以冻结操作结束:

      @Service
      public class TrackingServiceOne extends TrackingService {
              
          private final Map<Integer, String> mapCreditos = new HashMap<Integer, String>();
          ...
      
          {
      
              mapCreditos.put(1, "hi");
              mapCreditos.put(2, "example");
              ...
          }
       ...   
      }
      

      查看https://shipilev.net/blog/2014/all-fields-are-final/ 了解一些有趣的 HotSpot 相关详细信息

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-05-21
        • 1970-01-01
        • 2023-03-14
        • 1970-01-01
        • 1970-01-01
        • 2023-03-24
        • 2016-06-13
        • 1970-01-01
        相关资源
        最近更新 更多