简介
假设我有一个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查找记录,但未找到任何内容({{ 1}}返回get()
)。
我发现关于null
保证的一件事是:
来自here的在将对象放入任何并发之前的线程中的操作 收集发生在访问或删除之后的操作之前 该元素来自另一个帖子中的集合。
。如果我做对了,它的字面意思是,ConcurrentHashMap
可以返回任何实际上某个时间进入Map的值,只要它不会破坏不同线程中的动作之间的get()
关系。
在我的情况下,它适用于:如果第三个请求不关心在处理第一个和第二个期间发生的事情,那么它可以从happens-before
读取null
。
它不适合我,因为我真的需要从Map
得到最新的实际Map
。
我尝试了什么
所以我开始思考,如何在后续的Record
修改之间建立happens-before
关系;并有想法。 JLS says(在17.4.4中):
对volatile变量v(§8.3.1.4)的写入与all同步 随后通过任何线程读取v(其中定义了“后续”) 根据同步顺序)。
所以,我们假设,我会像这样修改我的单身:
Map
然后,在对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()
。
的 问题
由于heisenbugs的性质,没有多少测试足以说明这个解决方案是正确的。而且我不是并发专家,因此无法正式验证。
有人可以批准,遵循这种方法可以保证我总是从getRevision()
获得最新的实际价值吗?如果这种方法不正确或效率低下,你能推荐一些其他方法吗?
答案 0 :(得分:6)
你的方法不会起作用,因为你实际上又在重复同样的错误。由于ConcurrentHashMap.put
和ConcurrentHashMap.get
会在关系之前创建,但没有时间排序保证,因此对volatile
变量的读取和写入完全相同。它们构成发生在关系之前但没有时间排序保证,如果一个线程在另一个线程执行get
之前调用put
,则同样适用于volatile
读取将在volatile
写入之前发生。除此之外,您添加了另一个错误,因为将++
运算符应用于volatile
变量并非原子。
volatile
变量的保证并不比ConcurrentHashMap
更强。 It’s documentation明确指出:
检索反映了最近已完成更新操作的结果。
The JLS states外部操作是关于program order:
的线程间操作线程间操作是由一个线程执行的操作,可以被另一个线程检测到或直接受其影响。程序可以执行几种类型的线程间操作:
...
- 外部行动。外部操作是在执行之外可以观察到的操作,并且具有基于执行外部环境的结果。
简单地说,如果一个线程放入ConcurrentHashMap
并向外部实体发送消息,并且第二个线程在收到来自外部实体的消息后从同一个ConcurrentHashMap
获取,具体取决于之前的发送消息,没有内存可见性问题。
可能是这些操作没有以这种方式编程或者外部实体没有假定的依赖关系,但可能是错误位于完全不同的区域但我们不能告诉您没有发布相关代码,例如密钥不匹配或打印代码错误。但不管它是什么,它都不会被volatile
变量修复。