Java 我是否有重新排序问题,是否由于引用转义?
我有一个类,在这个类中我缓存实例并在使用它们时克隆它们(数据是可变的) 我想知道我是否可以面对这个重新排序的问题 我已经看过和JLS,但我仍然没有信心Java 我是否有重新排序问题,是否由于引用转义?,java,multithreading,java-memory-model,happens-before,Java,Multithreading,Java Memory Model,Happens Before,我有一个类,在这个类中我缓存实例并在使用它们时克隆它们(数据是可变的) 我想知道我是否可以面对这个重新排序的问题 我已经看过和JLS,但我仍然没有信心 public class DataWrapper { private static final ConcurrentMap<String, DataWrapper> map = new ConcurrentHashMap<>(); private Data data; private String
public class DataWrapper {
private static final ConcurrentMap<String, DataWrapper> map = new ConcurrentHashMap<>();
private Data data;
private String name;
public static DataWrapper getInstance(String name) {
DataWrapper instance = map.get(name);
if (instance == null) {
instance = new DataWrapper(name);
}
return instance.cloneInstance();
}
private DataWrapper(String name) {
this.name = name;
this.data = loadData(name); // A heavy method
map.put(name, this); // I know
}
private DataWrapper cloneInstance() {
return new DataWrapper(this);
}
private DataWrapper(DataWrapper that) {
this.name = that.name;
this.data = that.data.cloneInstance();
}
}
它仍然倾向于同样的问题吗
请注意,如果多个线程试图同时为相同的值创建并放置实例,我不介意是否会创建一个或两个额外的实例
编辑:
如果名称和数据字段是final或volatile呢
public class DataWrapper {
private static final ConcurrentMap<String, DataWrapper> map = new ConcurrentHashMap<>();
private final Data data;
private final String name;
...
private DataWrapper(String name) {
this.name = name;
this.data = loadData(name); // A heavy method
map.put(name, this); // I know
}
...
}
公共类数据包装器{
私有静态最终ConcurrentMap=新ConcurrentHashMap();
私人最终数据;
私有最终字符串名;
...
专用数据包装器(字符串名称){
this.name=名称;
this.data=loadData(name);//沉重的方法
map.put(名字,这个);//我知道
}
...
}
它仍然不安全吗?据我所知,构造函数初始化安全保证仅适用于在初始化过程中没有逃逸引用的情况。我正在寻找证实这一点的官方消息来源。该实现有一些非常微妙的警告 你似乎知道,但我要说清楚, 在这段代码中,多个线程可能会得到一个
null
实例,并输入if
块,
不必要地创建新的DataWrapper
实例:
看来你没问题,
但这需要假设loadData(name)
(由DataWrapper(String)
使用)将始终返回相同的值。
如果它可能根据时间返回不同的值,
无法保证加载数据的最后一个线程会将其存储在映射中
,因此该值可能已过时。
如果你说这不会发生或者这不重要,
这很好,但是这个假设至少应该被记录下来
为了演示另一个微妙的问题,让我内联instance.cloneInstance()
方法:
这里的微妙问题是,这个返回语句不是安全的发布。
新的DataWrapper
实例可以部分构造,
线程可能会在不一致的状态下观察到它,
例如,对象的字段可能尚未设置
有一个简单的解决方案:
如果将名称
和数据
字段设置为最终,
该类变得不可变。
不可变类具有特殊的初始化保证,
然后返回新的数据包装器(this)代码>成为安全的出版物
有了这个简单的更改,并且假设您对第一点没有意见(loadData
对时间不敏感),我认为实现应该能够正常工作
我会推荐一个与正确性无关的额外改进,而是其他良好实践。
当前的实施有太多的责任:
它是数据
的包装器,同时也是缓存。
增加的责任使阅读起来有点混乱。
另一方面,并发哈希映射并没有真正发挥其潜力
如果将责任分开,结果会更简单、更好、更容易阅读:
class DataWrapperCache {
private static final ConcurrentMap<String, DataWrapper> map = new ConcurrentHashMap<>();
public static DataWrapper get(String name) {
return map.computeIfAbsent(name, DataWrapper::new).defensiveCopy();
}
}
class DataWrapper {
private final String name;
private final Data data;
DataWrapper(String name) {
this.name = name;
this.data = loadData(name); // A heavy method
}
private DataWrapper(DataWrapper that) {
this.name = that.name;
this.data = that.data.cloneInstance();
}
public DataWrapper defensiveCopy() {
return new DataWrapper(this);
}
}
类DataWrapperCache{
私有静态最终ConcurrentMap=新ConcurrentHashMap();
公共静态数据包装器get(字符串名称){
返回map.computeIfAbsent(名称,DataWrapper::new).defensiveCopy();
}
}
类数据包装器{
私有最终字符串名;
私人最终数据;
数据包装器(字符串名称){
this.name=名称;
this.data=loadData(name);//沉重的方法
}
专用数据包装器(数据包装器){
this.name=that.name;
this.data=that.data.cloneInstance();
}
公共数据包装器defensiveCopy(){
返回新的数据包装器(此);
}
}
如果要符合规范,则不能应用此构造函数:
private DataWrapper(String name) {
this.name = name;
this.data = loadData(name);
map.put(name, this);
}
正如您所指出的,JVM可以将其重新排序为:
private DataWrapper(String name) {
map.put(name, this);
this.name = name;
this.data = loadData(name);
}
将值赋给final
字段时,这意味着在构造函数的末尾执行所谓的冻结操作。内存模型保证此冻结操作与应用此冻结操作的实例的任何取消引用之间存在“先发生后发生”关系。但是,此关系只存在于构造函数的末尾,因此,您将打破此关系。通过将发布从构造函数中拖出,可以修复这种关系
如果你想更正式地了解这种关系,我建议你。我还解释了这种关系。从我对您链接到的问题的理解来看-是的,您可能有重新排序问题,因为您可以通过引用转义看到一个部分初始化的实例。在另一种情况下,当您移动
map.put
到构造函数之外时,则不会出现这种情况。但我不确定,所以我期待一个权威的答案。@GurV这是我对同一个问题的答案。考虑到final和volatile具有内存栅栏(barrier)语义谢谢你的回答。我不想创建DataWrapperCache
,但我可以看到您将“放入映射”部分移出了DataWrapper
构造函数。我想了解它在DataWrapper
构造函数中存在(或不存在)的效果。另外,关于非最终字段,您是对的。但是如果我让它们成为final
(或者volatile
)并保留映射,那么会怎么样呢?按照答案的第一部分的暗示,将调用放在构造函数中?那我安全吗?我正在寻找一些官方/可靠来源的参考资料。@GurV我在第一点的末尾写道,将字段设置为最终字段应该会使您的实现安全。您不需要创建DataWrapperCache
,但我仍然建议您这样做。我的论点基于我在实践书中对Java并发性的解释,特别是第3章。我没有到JLS文档的链接,
class DataWrapperCache {
private static final ConcurrentMap<String, DataWrapper> map = new ConcurrentHashMap<>();
public static DataWrapper get(String name) {
return map.computeIfAbsent(name, DataWrapper::new).defensiveCopy();
}
}
class DataWrapper {
private final String name;
private final Data data;
DataWrapper(String name) {
this.name = name;
this.data = loadData(name); // A heavy method
}
private DataWrapper(DataWrapper that) {
this.name = that.name;
this.data = that.data.cloneInstance();
}
public DataWrapper defensiveCopy() {
return new DataWrapper(this);
}
}
private DataWrapper(String name) {
this.name = name;
this.data = loadData(name);
map.put(name, this);
}
private DataWrapper(String name) {
map.put(name, this);
this.name = name;
this.data = loadData(name);
}