如何保证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;
}
}
然后,在内部每次修改 Map
或 Record
之后,我将调用 incrementRevision()
,在从 Map 读取之前,我将调用 getRevision()
。
问题
由于 heisenbugs 的性质,再多的测试也不足以证明这个解决方案是正确的。而且我不是并发专家,所以无法正式验证。
有人可以同意,按照这种方法保证我总是会从 ConcurrentHashMap
获得最新的实际值吗?如果这种方法不正确或看起来效率低下,你能给我推荐其他方法吗?
你的方法是行不通的,因为你实际上又在重复同样的错误。由于 ConcurrentHashMap.put
和 ConcurrentHashMap.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
变量固定。
简介
假设我有一个 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;
}
}
然后,在内部每次修改 Map
或 Record
之后,我将调用 incrementRevision()
,在从 Map 读取之前,我将调用 getRevision()
。
问题
由于 heisenbugs 的性质,再多的测试也不足以证明这个解决方案是正确的。而且我不是并发专家,所以无法正式验证。
有人可以同意,按照这种方法保证我总是会从 ConcurrentHashMap
获得最新的实际值吗?如果这种方法不正确或看起来效率低下,你能给我推荐其他方法吗?
你的方法是行不通的,因为你实际上又在重复同样的错误。由于 ConcurrentHashMap.put
和 ConcurrentHashMap.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
变量固定。