代码之家  ›  专栏  ›  技术社区  ›  Gurwinder Singh

我是否有重新排序问题,是否由于引用转义?

  •  10
  • Gurwinder Singh  · 技术社区  · 7 年前

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

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

    我已经看过了 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 对象第二个线程读取 数据包装器 实例,并看到null 数据 字段(或部分构造)。

    这可能吗?如果是,是否只是由于引用转义?

    如果没有,请你解释一下如何解释 发生在之前 更简单地说,一致性?

    如果我这样做了呢:

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

    它仍然倾向于同样的问题吗?

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

    编辑:

    如果名称和数据字段是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
        }
        ...
    }
    

    2 回复  |  直到 4 年前
        1
  •  5
  •   Rafael Winterhalter    7 年前

    如果要符合规范,则不能应用此构造函数:

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

    将值赋给时 final 字段,这意味着 冻结操作 构造函数的。内存模型保证了此冻结操作和应用此冻结操作的实例的任何解引用之间的先发生后发生关系。然而,这种关系只存在于构造函数的末尾,因此,您打破了这种关系。通过将发布从构造函数中拖出,可以修复这种关系。

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

        2
  •  5
  •   janos slartidan    7 年前

    该实现有一些非常微妙的警告。

    你似乎知道,但我要说清楚, 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);
    }
    

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

    有一个简单的修复方法: 如果你成功了 name data 领域 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);
      }
    }