我是否有重新排序问题,是否因为参考逃脱?

Gur*_*ngh 10 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 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

我的想法:运行时可以在构造函数中重新排序语句,并DataWrapper在初始化data对象之前发布当前实例(放在映射中).第二个线程DataWrapper从map中读取实例并看到空data字段(或部分构造).

这可能吗?如果是,是否仅仅因为参考逃逸?

如果没有,你能否解释一下如何以更简单的方式推断事先发生的一致性?

如果我这样做了:

public class DataWrapper {
    ...
    public static DataWrapper getInstance(String name) {
        DataWrapper instance = map.get(name);
        if (instance == null) {
            instance = new DataWrapper(name);
            map.put(name, instance);
        }
        return instance.cloneInstance();
    }

    private DataWrapper(String name) {
        this.name = name;
        this.data = loadData(name);     // A heavy method
    }
    ...
}
Run Code Online (Sandbox Code Playgroud)

它仍然容易出现同样的问题吗?

请注意,如果多个线程尝试创建并同时将实例放入相同的值,我不介意是否创建了一个或两个额外的实例.

编辑:

如果名称和数据字段是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
    }
    ...
}
Run Code Online (Sandbox Code Playgroud)

它还不安全吗?据我所知,构造函数初始化安全保证仅适用于初始化期间未引用的引用.我正在寻找证实这一点的官方消息来源.

Sto*_*ica 5

实施有一些非常微妙的警告.

您似乎已经意识到了,但只是要清楚,在这段代码中,多个线程可能会获得一个null实例并进入该if块,从而不必要地创建新DataWrapper实例:

public static DataWrapper getInstance(String name) {
    DataWrapper instance = map.get(name);
    if (instance == null) {
        instance = new DataWrapper(name);
    }
    return instance.cloneInstance();
}
Run Code Online (Sandbox Code Playgroud)

看起来你没关系,但这需要假设loadData(name)(使用者DataWrapper(String))将始终返回相同的值.如果它可能会根据时间返回不同的值,则无法保证加载数据的最后一个线程会将其存储在中map,因此该值可能已过时.如果你说这不会发生或者它不重要,那很好,但这个假设至少应该记录下来.

为了演示另一个微妙的问题,让我内联instance.cloneInstance()方法:

public static DataWrapper getInstance(String name) {
    DataWrapper instance = map.get(name);
    if (instance == null) {
        instance = new DataWrapper(name);
    }
    return new DataWrapper(instance);
}
Run Code Online (Sandbox Code Playgroud)

这里的一个微妙问题是这个退货声明不是安全的出版物.DataWrapper可以部分地构造新实例,并且线程可以在不一致状态下观察它,例如,可能尚未设置对象的字段.

有一个简单的解决方法:如果你创建namedata字段final,该类将变为不可变.不可变类享有特殊的初始化保证,并return new DataWrapper(this);成为安全的出版物.

通过这个简单的更改,并假设您对第一点(loadData不是时间敏感的)没问题,我认为实现应该正常工作.


我会建议一个与正确性无关的额外改进,但其他好的做法.当前的实现有太多的责任:它是一个包装器Data,同时也是一个缓存器.增加的责任使得阅读有点混乱.另外,并发哈希映射并未真正用于其潜力.

如果分开职责,结果可以更简单,更好,更容易阅读:

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);
  }
}
Run Code Online (Sandbox Code Playgroud)


Raf*_*ter 5

如果您想要符合规范,则无法应用此构造函数:

private DataWrapper(String name) {
  this.name = name;
  this.data = loadData(name);
  map.put(name, this);
}
Run Code Online (Sandbox Code Playgroud)

正如您所指出的那样,允许JVM将其重新排序为:

private DataWrapper(String name) {
  map.put(name, this);
  this.name = name;
  this.data = loadData(name);
}
Run Code Online (Sandbox Code Playgroud)

当值分配给final领域,这意味着所谓的冻结行动,在年底的构造.内存模型保证在此冻结操作与应用此冻结操作的实例的任何解除引用之间的关系之前发生.但是,这种关系只存在于构造函数的末尾,因此,您打破了这种关系.通过将出版物拖出构造函数,您可以解决此问题.

如果您想要更正式地了解这种关系,我建议您查看此幻灯片组.我还从大约34分钟开始解释了这个演讲中的关系.