10

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

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

我已经看过这个答案和 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第二个线程DataWrapper从映射读取实例并看到空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
    }
    ...
}

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

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

编辑:

如果 name 和 data 字段是 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
    }
    ...
}

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

4

2 回答 2

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);
}

这里的微妙问题是这个返回语句不是安全的发布。新DataWrapper实例可能是部分构造的,线程可能会观察到它处于不一致的状态,例如对象的字段可能尚未设置。

对此有一个简单的解决方法:如果您设置nameanddata字段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);
  }
}
于 2017-10-07T21:50:14.707 回答
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一个所谓的冻结动作。内存模型保证此冻结操作与应用此冻结操作的实例的任何解除引用之间的关系之前发生。然而,这种关系确实只存在于构造函数的末尾,因此,你打破了这种关系。通过将发布拖出您的构造函数,您可以修复此关系。

如果您想更正式地了解这种关系,我建议您查看此幻灯片集。我还从大约 34 分钟开始解释了本演示文稿中的关系。

于 2017-10-09T09:07:52.023 回答