如何保证ConcurrentHashMap的get()总是return最新的实际值?

How to guarantee get() of ConcurrentHashMap to always return the latest actual value?

简介
假设我有一个 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 找到 Record,但什么也没找到(get() returned null).
我发现的关于 ConcurrentHashMap 保证的一件事是:

Actions in a thread prior to placing an object into any concurrent collection happen-before actions subsequent to the access or removal of that element from the collection in another thread.

来自 here。如果我做对了,它的字面意思是,get() 可以 return 任何实际上在某个时候进入 Map 的值,只要它不破坏 happens-before 不同线程中动作之间的关系.
在我的例子中,它是这样应用的:如果第三个请求不关心在处理第一个和第二个过程中发生了什么,那么它可以从 Map.
中读取 null
它不适合我,因为我真的需要从 Map 获取最新的实际 Record

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

A write to a volatile variable v (§8.3.1.4) synchronizes-with all subsequent reads of v by any thread (where "subsequent" is defined according to the synchronization order).

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

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 获得最新的实际值吗?如果这种方法不正确或看起来效率低下,你能给我推荐其他方法吗?

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

volatile 变量所做的保证并不比为 ConcurrentHashMap 所做的保证更强。 It’s documentation 明确指出:

Retrievals reflect the results of the most recently completed update operations holding upon their onset.

The JLS states that external actions are inter-thread actions regarding the program order:

An inter-thread action is an action performed by one thread that can be detected or directly influenced by another thread. There are several kinds of inter-thread action that a program may perform:

  • External Actions. An external action is an action that may be observable outside of an execution, and has a result based on an environment external to the execution.

简单地说,如果一个线程放入 ConcurrentHashMap 并向外部实体发送消息,而第二个线程在收到来自外部实体的消息后从同一个 ConcurrentHashMap 获取消息,具体取决于之前发送的消息,不会有内存可见性问题。

可能是因为这些操作不是以这种方式编程的,或者外部实体没有假定的依赖关系,但也可能是错误出在完全不同的区域,但我们可以不要说你没有 post 相关代码,例如密钥不匹配或打印代码错误。但无论如何,它不会被volatile变量固定。