【问题标题】:Do I need to use thread safe map in this case?在这种情况下我需要使用线程安全映射吗?
【发布时间】:2014-04-12 08:46:12
【问题描述】:

我想用java周期性地从mysql加载一些系统信息,大概10秒一次,用一个线程完成,同时有多个线程需要读取这些信息,有两种情况:

1、声明一个public static映射:

public  volatile static Map<String, String> info = new HashMap<String, String>();

// Context.java
// start a thread to load from mysql
private void load() {
    new Thread(new Runnable() {
        while(true) {
              // return a map
              info = loadFromDB();        // every time a new map 
              Thread.sleep(5 * 1000);     // sleep 5 seconds
           } 
     }).start();
}

// mutiple threads call this function
public String getConfig(String key) {
    return Context.map.get(key);
}

正如代码所示,每次线程从db加载信息时,都是一个新的映射,我不需要更改map的内容,这个线程安全吗?

2,也许我想要一个有序的map,而读取线程需要获取map中的max key,这种情况,我应该使用TreeMap还是ConcurrentSkipListMap

更新

Map 信息更改为volatile 以避免使用cpu 进行缓存。

【问题讨论】:

  • 我认为这种情况有点不同。
  • 摘自HashMap - If multiple threads access a hash map concurrently, and at least one of the threads modifies the map structurally, it must be synchronized externally 的文档。因此,我认为使用ConcurrentHashMap 是您最好的选择。
  • 但是没有线程尝试修改地图,只是改变了参考,所以我还是一头雾水。
  • 绝对!我完全同意您的看法,因为您不会更新从外部任何地方的数据库加载的地图,并且仅将其用于阅读目的。

标签: java mysql multithreading concurrency concurrentmodification


【解决方案1】:

java 中的单个字段分配是原子的,所以如果你的线程在 loadFromDB() 方法调用中构建了一个新的 hashmap(所以它不会触及现有的 info map),然后只需将 info 引用的值更改为 this它创建的新映射操作正常 - 任何其他并发线程都可以看到旧的 info 值或新的值 - 两者之间没有任何内容。

不过有几点:

  1. 在这种情况下,您应该将 info 设置为 volatile 字段:public static volatile Map... 以避免值将缓存在处理器缓存中并且在您更改引用时不会更新的情况。
  2. 您编写的任何从信息映射中读取值的代码都需要获取对映射的引用,并且只需要一次,并使用该引用进行操作。例如:

没关系

Map infoMap = info;
value1 = infoMap.get("key1");
//do something
value2 = infoMap.get("key2");

但这不行

value1 = info.get("value1");
//do soemthing
value2 = info.get("value2");

因为您可能正在从较新的信息地图中读取 value2。

【讨论】:

  • 既然info map总是引用加载的informat map,为什么下面的代码不能工作?
  • @znlyj - 因为如果在获取 value1 和 value2 之间有很长的延迟,那么它可能会替换地图,然后代码获得 value1 的旧值和 value2 的新值。这通常是一个非常糟糕的主意。
  • 既然 infoMap 和 info 都是对 loadFromDB 返回的加载地图的引用,为什么 infoMap 可以避免这种情况呢?我认为,key2 的值​​也可能被新地图取代。
  • @znlyj - 但在第一种情况下,我将始终从旧地图中获取内容(因为我将它的引用保留为 infoMap) - 代码获取对地图的引用,同时保留它它正在做它的事情,并且没有看到任何更新的地图(直到操作完成并且下一个操作再次获取当前地图)
  • 明白! infoMap 变量保留对旧地图的引用,这样我就可以得到这个旧地图所持有的值,但 info 将始终引用最新的地图。
【解决方案2】:

对于具有共享内存的多线程应用程序,需要记住一条重要规则。关于 Java 内存模型,JLS7 在 17.4.5 中声明:

[..] 如果所有顺序一致执行都没有数据竞争,[..]那么程序的所有执行都将显示为顺序一致

乍一看,这句话听起来很奇怪。但是,它准确地告诉您您必须知道的内容。在您的情况下,这意味着:

无易失性

如果你从变量info的定义中去掉限定符volatile,那么这个变量就会有数据竞争,因为两个线程可以同时访问这个变量,至少一个操作是写操作。这意味着,不能保证顺序一致性,这意味着什么也很难解释。很可能,您会看到各种奇怪的异常,因为阅读器线程会看到不一致的 HashMap

具有易失性

如果将限定符 volatile 添加到变量 info 的定义中,则不会有 数据竞争,无论是在变量 info 还是在任何其他内存位置上,因为没有两个线程(至少有一个写操作)可以同时访问的内存位置。请注意,对 volatile 变量的操作不是数据竞争

这意味着在所有执行中,所有操作都将以与程序顺序一致的总顺序执行。这意味着,您的应用程序按预期工作。

【讨论】:

  • 所以,情况 1 会按预期工作,情况 2 怎么样,我想从排序后的地图中获取价值。
  • 不需要并发数据结构。使用TreeMap
【解决方案3】:

理论上可以,实际上不行。

理论上您需要使用并发映射,因为get 的实现可能不是线程安全的。地图实现可以使用不安全的变量进行内部优化或在 get 调用上重新散列或执行其他可能导致某些线程问题出现的事情。

在实践中,据我所知,get 调用是一个简单的数组查找,应该是线程安全的。但是,这仅适用于特定实现(Oracle、Windows)上的当前 Java 版本,并且在其他地方可能会有所不同。

由于在非并发映射上使用并发的成本非常低,我建议使用并发映射,即使只是怀疑它是否是线程安全的。

【讨论】:

  • 我不修改地图,而是创建一个新的参考,这也不是线程安全的吗?
  • 这不是关于你用地图做什么,而是关于get()是如何被Java实现的。没有定义 get 是否是线程安全的,所以你不能仅仅假设它是并且永远都是。
  • 逻辑 101:如果 A 隐含 B,则 B 不隐含 A。没有具体说明导致结构修改的原因。但是正如我所说:他只是一个理论上的东西,实际上你可以get() 尽可能多地,它不会失败。就像您碰巧为核反应堆设计控制装置一样,如果您不指望它,那就太好了。
  • @TwoThe:你的意思是多线程的读操作不是线程安全的?
  • 请注意我区分理论和实践。我对理论的评论只是作为正确阅读 JavaDocs 的声明:如果那些不能保证线程安全,你不应该指望它。关于一个完全不同的主题:多次读取始终是线程安全的,您需要写入才能使其混乱。 ;)
猜你喜欢
  • 2012-07-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-09-11
相关资源
最近更新 更多