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
overrideequals
?当两个线程同时调用getB()
时,可能每个线程都有自己的对象B
,也可能它们都共享相同的B
。这里的问题就像@MarkElliot指出的那样:B
overrideequals()
和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
相结合,您可以非常肯定您的做法是错误的。