【问题标题】:Is lazy initialization with immutable data always thread-safe?不可变数据的延迟初始化总是线程安全的吗?
【发布时间】:2014-03-09 23:42:55
【问题描述】:

我有两个班级AB

class A {
    private final String someData;
    private B b;

    public String getSomeData() { return someData; }

    public B getB() {
        if (b == null) {
             b = new B(someData);
        }
        return b;
    }
}

其中B 是不可变的,并且仅从A 的实例计算其数据。 A 具有不可变的语义,但它的内部是可变的(如 java.lang.String 中的 hashCode)。

当我从两个不同的线程调用getB(),并且调用重叠时,我假设每个线程都有自己的B 实例。但是由于B的构造函数只获取不可变数据,所以B的两个实例应该相等。

正确吗?如果不是,我必须让getB() 同步以使其成为线程安全的吗?

假设B实现equals(),比较B的所有实例变量。hashCode()也一样

【问题讨论】:

  • B 是否覆盖 equals
  • 当两个线程同时调用getB() 时,可能会发生每个线程都有自己的对象B,或者它们也可能共享相同的B。这里的问题就像@MarkElliot 指出的那样:B 是否覆盖equals()hashCode()
  • @MarkElliot 是的,假设B 实现equals(),它比较B 的所有实例变量。 hashCode() 也一样。

标签: java multithreading thread-safety immutability lazy-initialization


【解决方案1】:

不是线程安全的,因为您没有与volatilesynchronized 创建任何“发生前”关系,因此两个线程可能会相互干扰其他。

问题是虽然b = new B(someData)的意思是“为B的实例分配足够的内存,然后在那里创建实例,然后将b指向它”,但允许系统将其实现为“分配足够B 实例的内存,然后将 b 指向它,然后创建实例”(因为在单线程应用程序中,这是等效的)。因此,在您的代码中,两个线程可以创建 separate 实例但返回 same 实例,一个线程有可能在实例之前返回另一个线程的实例 已完全初始化

【讨论】:

    【解决方案2】:

    对于“但是由于 B 的构造函数只获取不可变数据,因此 B 的两个实例应该相等。” 如您所知,它不是线程安全的,一个线程可能会获得未初始化的 B 实例(B 为空或不一致的状态,其中一些数据尚未设置)其他线程可能会获得带有一些数据集的 b 实例。

    要解决此问题,您需要同步 getB 方法或使用带双重检查锁的同步块或一些非阻塞技术,如 AtomicReference。供您参考,我在此处添加示例代码,了解如何使用 AtomicReference 实现正确的 threadSafe getB() 方法。

    class A {
        private final String someData = "somedata";
        private AtomicReference<B> bRef;
    
        public String getSomeData() { return someData; }
    
        public B getB() {
            if(bRef.get()== null){
                synchronized (this){
                    if(bRef.get() == null)
                        bRef.compareAndSet(null,new B(someData));
                }
            }
            return bRef.get();
        }
    }
    
    class B{
        public B(String someData) {
    
        }
    }
    

    【讨论】:

    • 我对否决这一点犹豫不决,因为除了它未能初始化 b(可能应该将其命名为 bRef 以避免混淆,并声明+初始化为 private final AtomicReference&lt;B&gt; bRef = new AtomicReference&lt;B&gt;())之外,您的代码没有 broken 。 . .但这真的很奇怪。首先,您不应该在B.class 上进行同步,因为这意味着A 的所有实例都需要相同的锁,这也可能由其他一些代码持有(因为A 不拥有B.class )。其次,compareAndSet 是多余的,因为保证引用是null [继续]
    • [续] 在那个位置;您可以只写bRef.set(new B(someData)),此时您不妨使用volatile 字段。原子的全部意义在于避免锁定。如果您发现自己将 atomics 与 synchronized 结合使用,您可以确定自己做错了事。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-11-17
    • 2014-07-11
    • 2015-07-27
    • 2012-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多