Java 使用不可变数据的延迟初始化总是线程安全的吗?

Java 使用不可变数据的延迟初始化总是线程安全的吗?,java,multithreading,thread-safety,immutability,lazy-initialization,Java,Multithreading,Thread Safety,Immutability,Lazy Initialization,我有两门课A和B: 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
B

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()也是如此。这不是线程安全的,因为您没有创建任何与
volatile
synchronized
的“之前发生”关系,所以这两个线程可能相互干扰

问题在于,尽管
b=new b(someData)
意味着“为
b
的实例分配足够的内存,然后在那里创建实例,然后指向它”,但允许系统按照“为
B
的实例分配足够的内存,然后指向
B
,然后创建实例”(因为在单线程应用程序中,这是等效的)因此,在您的代码中,如果两个线程可以创建单独的实例,但返回相同的实例,则一个线程有可能在实例完全初始化之前返回另一个线程的实例。

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

要解决这个问题,您需要同步的getB方法,或者使用带有双重检查锁的synchronized block,或者一些非阻塞技术,如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) {

    }
}
A类{
私有最终字符串someData=“someData”;
私有原子参考bRef;
公共字符串getSomeData(){return someData;}
公共B getB(){
if(bRef.get()==null){
已同步(此){
if(bRef.get()==null)
bRef.compareAndSet(null,新B(someData));
}
}
返回bRef.get();
}
}
B类{
公共B(字符串数据){
}
}

B
override
equals
?当两个线程同时调用
getB()
时,可能每个线程都有自己的对象
B
,也可能它们都共享相同的
B
。这里的问题就像@MarkElliot指出的那样:
B
override
equals()
hashCode()
?@MarkElliot是的,假设
B
实现了
equals()
,它比较了
B
的所有实例变量。对于
hashCode()
,同样如此。我很犹豫是否要否决它,因为除了初始化
B
失败之外(可能应该将其命名为
bRef
,以避免混淆,并声明+初始化为
private final AtomicReference bRef=new AtomicReference()
)首先,你不应该在
B.class
上同步,因为这意味着
A
的所有实例都需要相同的锁,这也可能由其他代码持有(因为
A
不拥有
B.class
)。其次,
compareAndSet
是多余的,因为该位置的引用保证为
null
[continued][continued];您只需编写
bRef.set(新B(someData))
,此时您最好只使用一个
volatile
字段。atomics的全部目的是避免锁定;如果您发现自己将atomics与
synchronized
相结合,您可以非常肯定您的做法是错误的。