【问题标题】:How to guarantee get() of ConcurrentHashMap to always return the latest actual value?如何保证 ConcurrentHashMap 的 get() 总是返回最新的实际值?
【发布时间】:2015-06-28 14:07:15
【问题描述】:

简介
假设我有一个 ConcurrentHashMap 单例:

public class RecordsMapSingleton {

    private static final ConcurrentHashMap<String,Record> payments = new ConcurrentHashMap<>();

    public static ConcurrentHashMap<String, Record> getInstance() {
        return payments;
    }

}

然后我有来自不同来源的三个后续请求(均由不同线程处理)。
第一个服务发出请求,获取单例,创建 Record 实例,生成唯一 ID 并将其放入 Map,然后将此 ID 发送到另一个服务。
然后第二个服务使用该 ID 发出另一个请求。它获取单例,找到Record 实例并对其进行修改。
最后(可能半小时后)第二个服务发出另一个请求,以进一步修改Record

问题
在一些非常罕见的情况下,我遇到了heisenbug。在日志中我可以看到,第一个请求成功地将Record 放入Map,第二个请求通过 ID 找到并修改它,然后第三个请求尝试通过 ID 查找记录,但什么也没找到(get() 返回@987654332 @)。
我发现关于ConcurrentHashMap 保证的唯一一点是:

在将对象放入任何并发之前线程中的操作 收集发生在访问或删除之后的操作 从另一个线程中的集合中提取该元素。

来自here。如果我做对了,它的字面意思是,get() 可以返回任何实际存在于 Map 中的值,只要它不会破坏 happens-before 不同线程中的操作之间的关系。
在我的情况下,它是这样应用的:如果第三个请求不关心第一个和第二个处理过程中发生的事情,那么它可以从 Map 读取 null

它不适合我,因为我真的需要从 Map 获取最新的实际 Record

我尝试了什么
于是我开始思考,如何在后续的Map修改之间形成happens-before关系;并提出了想法。 JLS says(在 17.4.4 中):

对 volatile 变量 v(第 8.3.1.4 节)的写入与所有变量同步 任何线程对 v 的后续读取(其中定义了“后续” 按同步顺序)。

所以,假设,我将像这样修改我的单例:

public class RecordsMapSingleton {

    private static final ConcurrentHashMap<String,Record> payments = new ConcurrentHashMap<>();
    private static volatile long revision = 0;

    public static ConcurrentHashMap<String, Record> getInstance() {
        return payments;
    }

    public static void incrementRevision() {
        revision++;
    }
    public static long getRevision() {
        return revision;
    }

}

然后,在每次修改 MapRecord 内部后,我将调用 incrementRevision(),在从 Map 读取任何内容之前,我将调用 getRevision()

问题
由于 heisenbugs 的性质,没有多少测试足以证明这个解决方案是正确的。而且我不是并发方面的专家,所以无法正式验证它。

有人可以批准,遵循这种方法可以保证我总是能从ConcurrentHashMap 获得最新的实际值吗?如果这种方法不正确或看起来效率低下,您能推荐我其他方法吗?

【问题讨论】:

  • 也许我在这里读错了你的问题,但你似乎认为读总是在写之后发生;为什么?
  • @fge 尽管它们是独立的线程。这是一个预先确定的顺序(put-get-get),我很感兴趣,但有时会出错。
  • 好吧,无论如何它在某一点或另一点出错;你不能保证线程的执行顺序,除非你互相保护它们,所以这看起来很像一个失败的原因;您所说的“最新值”是最后一个写入线程将写入的任何值,因此到目前为止它是一致的。据我所知,您需要一些其他同步机制。
  • @fge 好吧,这第二个服务只有在我向他发送 ID 后才会向我发送第二个请求。并且仅在我向他发送对第二个请求的响应后才发送第三个请求。我认为由于流程的逻辑,这个顺序是强制的。第二个服务在收到我的包含 ID 的请求之前无法发送第二个请求,并且在收到对第二个的响应之前它无法发送第三个请求。
  • @mkrakhin 如果有多个实例以某种方式负载平衡,我相信这是您的问题。获取单例的内存引用可能有助于您的调试和确认。

标签: java multithreading concurrency volatile concurrenthashmap


【解决方案1】:

你的方法是行不通的,因为你实际上又在重复同样的错误。由于ConcurrentHashMap.putConcurrentHashMap.get 将创建一个发生在 关系但没有时间顺序保证,这同样适用于您对volatile 变量的读取和写入。它们形成 happen before 关系,但没有时间顺序保证,如果一个线程碰巧在另一个线程执行 put 之前调用 get,这同样适用于将在volatile 那就写吧。除此之外,您还添加了另一个错误,因为将 ++ 运算符应用于 volatile 变量不是原子的。

volatile 变量的保证并不比对ConcurrentHashMap 的保证强。 It’s documentation 明确声明:

检索反映了最近完成更新操作在开始时保持的结果。

The JLS states 表示外部动作是关于program order 的线程间动作:

线程间动作是由一个线程执行的可以被另一个线程检测或直接影响的动作。程序可以执行多种线程间操作:

  • 外部操作。外部动作是可以在执行之外观察到的动作,其结果基于执行外部的环境。

简单地说,如果一个线程放入ConcurrentHashMap 并向外部实体发送消息,而第二个线程根据先前发送的消息从外部实体接收消息后从同一个ConcurrentHashMap 获取,则不可能是内存可见性问题。

可能是这些操作不是这样编程的,或者外部实体没有假定的依赖关系,但也可能是错误位于完全不同的区域,但我们不能告诉你没有发布相关代码,例如密钥不匹配或打印代码错误。但不管是什么,volatile 变量都不会修复它。

【讨论】:

  • 你的观点很有趣。我认为 volatile 修饰符提供了一些其他类型的保证(有点,如果 read 真的是 after write,它会看到正确的值)而不是 ConcurrentHashMap 的方法(你可以在 cmets 中看到我的想法来质疑)。对了,谢谢提醒,增量不是原子的,是我的疏忽。
  • 要补充的另一件事是 ConcurrentHashMap 的 JavaDoc 禁止插入 null 作为键或值 - 这证明您要查找的键在地图中不存在.
  • 好吧,如果读取是“真正在写入之后”,您将看到正确的值,但同样适用于ConcurrentMapgetput。请注意,如果您的代码是根据您描述的意图安排的,即与外部实体的通信意味着排序,那么您用于通信的 API 确实会为您提供扩展至 ConcurrentMap 操作的排序。但是既然遇到了问题,代码显然不是这样排列的(除非真正的错误出在打印或者不相关的区域……)
  • @Holger 正如我之前所说,沟通确实意味着订购。请求是无条件的后续。所以看来问题确实出在其他领域。您能否提供一些规范中的引用,证明 ConcurrentHashMap 确实提供了这种保证,所以我可以接受您的回答?
  • 好吧,来自 Oracle 的 Aleksey Shipilev 证实,ConcurrentHashMap 确实应该返回检索时的最新更新。那么我接受这个答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-16
  • 2012-06-10
  • 1970-01-01
  • 2018-10-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多