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

时间:2017-10-05 18:44:24

标签: java multithreading java-memory-model happens-before

我有这个类,当我使用它们时,我会缓存实例并克隆它们(数据是可变的)。

我想知道我是否可以面对这个重新排序的问题。

我看过this answer和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();
    }
}

我的想法:运行时可以在构造函数中重新排序语句,并在初始化DataWrapper对象之前发布当前的data实例(放在地图中)。第二个线程从map中读取DataWrapper实例,并看到null 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
    }
    ...
}

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

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

编辑:

如果名称和数据字段是最终的或不稳定的,该怎么办?

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
    }
    ...
}

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

2 个答案:

答案 0 :(得分: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();
}

看来你还好, 但这需要假设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);
}

这里的一个微妙问题是这个return语句不是安全的出版物。 新的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);
  }
}

答案 1 :(得分:5)

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

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字段分配值时,这意味着构造函数的结束处的所谓冻结操作。内存模型保证在此冻结操作与应用此冻结操作的实例的任何解除引用之间的关系之前发生。但是,这种关系只存在于构造函数的末尾,因此,您打破了这种关系。通过将出版物拖出构造函数,您可以修复此问题。

如果您想要更正式地了解这种关系,我建议looking through this slide set。我还解释了in this presentation starting at about minute 34的关系。