我目前正在阅读 Core Java,第一卷书,第 12 章 - 并发。
书中有一节涉及final关键字,其中指出:
还有另一种情况,访问共享字段是安全的——当 它被宣布为最终版本。
考虑final var accounts = new HashMap<String, Double>();
在构造函数完成后,其他线程可以看到帐户变量 完成的。 如果不使用final,就不能保证其他线程会 查看帐户的更新值 - 它们可能都看到 null,而不是 构造了 HashMap。
(Horstmann,2018)
我尝试创建一个示例,演示非最终字段可以在初始化之前被其他线程读取。
class SharedResource {
private int value;
public SharedResource(int value) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.value = value;
}
public int getValue() {
return value;
}
}
class NonFinalExample {
private static SharedResource sharedResource;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> sharedResource = new SharedResource(42));
thread1.start();
Thread thread2 = new Thread(() -> {
if (sharedResource != null) {
int result = sharedResource.getValue();
System.out.println("Thread 2: Value is " + result);
} else {
System.out.println("Thread 2: Shared resource is not initialized yet");
}
});
thread2.start();
}
}
它的问题是我无法理解,在上面的示例中,
thread2
如何能够将sharedResource
视为不为空,同时又具有未初始化的sharedResource.value
。SharedResource
对象不是在初始化完成、创建对象之后才被sharedResource
赋值给thread1
变量吗?
非常感谢任何与该主题相关的帮助
所引用的文字充其量只是误导性的。
行
final var accounts = new HashMap<String, Double>();
不能是字段声明,因为 var
关键字仅允许用于局部变量。关于字段初始化的讨论对于局部变量来说是毫无意义的。
此外,当构造的对象不正确地发布到其他线程时,“构造函数完成后”这句话是没有意义的。并发访问意味着没有顺序。但请注意,这专门针对不正确发布对象的场景。对于正确发布的对象,您不需要
final
字段。
同样重要的是,可能会看到不正确发布的对象的不一致值的问题不仅仅是线程可能会看到引用字段的
null
。它还可能会看到非 null
引用,但 HashMap
实例的状态不一致。并且由于HashMap
是一个可变对象,请注意,保证一致的状态是完成构造函数时HashMap
的状态,只要没有发生后续修改即可;如果构造函数之后有任何修改,那么一切都将失败。创建字段final
不能代替可变数据的正确同步。
当您遇到像示例代码中那样的数据竞争时
if (sharedResource != null) {
int result = sharedResource.getValue();
System.out.println("Thread 2: Value is " + result);
} else {
System.out.println("Thread 2: Shared resource is not initialized yet");
}
您可能不仅会获得在
0
中读取 getValue()
的潜在结果,甚至有可能第一次读取 sharedResource
中的 sharedResource != null
看到非 null
引用,但另一个读取 sharedResource.getValue()
中的引用看到 null
并产生 NullPointerException
。
如前所述,并发访问(没有正确的同步)意味着缺乏顺序。因此,您无法通过假设读取按特定顺序发生来推断字段
sharedResource
的两次读取的预期结果。
但是,正如评论中所述,可能的结果并不意味着您可以在示例程序中强制执行此结果。单次运行的程序特别不可能产生罕见的情况。