通过引用访问延迟初始化的非易失性字符串线程安全吗?
Is access by reference to a lazily initialized non-volatile String thread-safe?
我有一个 String
字段,它被初始化为 null
但随后可能被多个线程访问。该值将在第一次访问时延迟初始化为幂等计算值。
这个字段是否需要 volatile
才能线程安全?
这是一个例子。
public class Foo {
private final String source;
private String BAR = null;
public Foo(String source) {
this.source = source;
}
private final String getBar() {
String bar = this.BAR;
if (bar == null) {
bar = calculateHashDigest(source); // e.g. an sha256 hash
this.BAR = bar;
}
return bar;
}
public static void main(String[] args) {
Foo foo = new Foo("Hello World!");
new Thread(() -> System.out.println(foo.getBar())).start();
new Thread(() -> System.out.println(foo.getBar())).start();
}
}
我在示例中使用了 System.out.println()
,但我并不担心调用互锁时会发生什么。 (虽然我很确定这也是线程安全的。)
BAR
需要 volatile
吗?
我认为答案是否,不需要volatile
,是它是线程安全的,主要是因为这个 excerpt from JLS 17.5:
final
fields also allow programmers to implement thread-safe immutable objects without synchronization. A thread-safe immutable object is seen as immutable by all threads, even if a data race is used to pass references to the immutable object between threads.
并且由于 String
的 char value[]
字段确实是 final
.
(int hash
不是 final
但它的延迟初始化看起来也不错。)
Edit:编辑以阐明 BAR
的值是 fixed 值。它的计算是幂等的,没有副作用。我不介意计算是否跨线程重复,或者如果 BAR
由于内存缓存/可见性而成为有效的线程本地。我担心的是,如果它是非空的,那么它的值是完整的而不是部分的。
您的代码(技术上)不是线程安全的。
String
确实是一个正确实现的不可变类型,而且您所说的有关其 final
字段的内容是正确的。但这不是线程安全问题所在。
第一个问题是 BAR
的惰性初始化中存在竞争条件。如果两个线程同时调用 getBar()
,它们都会将 BAR
视为 null
,然后都尝试对其进行初始化。
第二个问题是内存问题。由于一个线程对 BAR
的写入与另一个线程对 BAR
的后续读取之间没有 happens-before 关系,因此无法保证第二个线程会看到BAR
的初始值。因此,它可能会重复初始化。
请注意,在示例中所写,这两个问题不是实际的线程安全问题。您正在执行的初始化是 idempotent。这对您可能多次初始化 BAR
的代码的行为没有影响,因为您总是将它初始化为对同一个 String
对象的引用。 (单次冗余初始化的开销太小不用担心。)
但是,如果 BAR
是对可变对象的引用,或者如果初始化开销很大,那么这就是一个真正的线程安全问题。
正如@Ravindra 所说,简单的解决方案是将 getBar
声明为 synchronized
。这解决了这两个问题。
您声明 BAR
的想法解决了内存危险,但没有解决竞争条件。
您在问题中添加了以下内容:
Edit to clarify the value intended for BAR
is a fixed value. Its calculation is idempotent and has no side-effects. I don't mind if the calculation is repeated across threads, or if BAR
becomes effectively a thread-local due to memory-caching / visibility. My concern is, if it's non-null then it's value is complete and not somehow partial.
这并没有改变我上面所说的。如果值是 String
,那么它是一个正确实现的不可变对象,您将 总是 看到完整的值 irrespective还要别的吗。这就是 JLS 报价所说的!
(实际上,我掩盖了 String
使用非 final
字段来保存延迟计算的哈希码的细节。但是,String::hashCode
实现会处理这个问题。它没有线程安全问题。如果你愿意,自己检查一下。)
您的代码不是线程安全的。看来您可能正在考虑双重检查锁定模式。正确的模式是这样的:
public class Foo {
private static volatile String BAR = null;
private static String getBar() {
String bar = BAR;
if (bar == null) {
synchronized( Foo.class )
if( bar == null ) {
bar = "Hello World!";
BAR = bar;
}
}
return bar;
}
// ...
这里有两件事。
如果BAR
已经初始化,则不会进入synchronized
块。 volatile
在这里是必需的,因为需要一些同步并且 BAR
的读取将与对易失性 BAR
.
的写入同步
如果 BAR
为 null 那么我们进入 synchronized
块,我们必须再次检查 BAR
是否仍然为 null 因此我们可以进行检查和分配原子。如果我们不进行原子检查,那么 BAR
有可能会被多次初始化。
您引用了 Java 规范。关于 final
关键字。虽然 String
是不可变的并且在内部使用 final
关键字,但这不会影响您的字段 BAR
。字符串很好,但是你的字段仍然是共享内存位置,如果你希望它是线程安全的,则需要同步。
另一位发帖人也提到了实习字符串。他们说在这个特定实例中只有一个 "Hello World!"
对象是正确的,因为 JVM 规范保证字符串是驻留的。这是一种奇怪的线程安全形式,不适用于其他对象,因此只有在您确定它可以正常工作时才使用它。您自己创建的大多数对象将无法使用您现在拥有的代码。
最后我想我要指出的是,因为 "Hello World!"
已经是一个字符串对象,所以尝试 "lazy load" 它没有多大意义。字符串是在您的 class 被加载时由 JVM 创建的,因此它们在您的方法是 运行 时已经存在,甚至在 BAR
第一次被读取时就已经存在。在这种情况下,只有一个字符串,尝试 "lazy load" 字符串没有优势。
public class Foo {
// probably better, simpler
private static final String BAR = "Hello World!";
我有一个 String
字段,它被初始化为 null
但随后可能被多个线程访问。该值将在第一次访问时延迟初始化为幂等计算值。
这个字段是否需要 volatile
才能线程安全?
这是一个例子。
public class Foo {
private final String source;
private String BAR = null;
public Foo(String source) {
this.source = source;
}
private final String getBar() {
String bar = this.BAR;
if (bar == null) {
bar = calculateHashDigest(source); // e.g. an sha256 hash
this.BAR = bar;
}
return bar;
}
public static void main(String[] args) {
Foo foo = new Foo("Hello World!");
new Thread(() -> System.out.println(foo.getBar())).start();
new Thread(() -> System.out.println(foo.getBar())).start();
}
}
我在示例中使用了 System.out.println()
,但我并不担心调用互锁时会发生什么。 (虽然我很确定这也是线程安全的。)
BAR
需要 volatile
吗?
我认为答案是否,不需要volatile
,是它是线程安全的,主要是因为这个 excerpt from JLS 17.5:
final
fields also allow programmers to implement thread-safe immutable objects without synchronization. A thread-safe immutable object is seen as immutable by all threads, even if a data race is used to pass references to the immutable object between threads.
并且由于 String
的 char value[]
字段确实是 final
.
(int hash
不是 final
但它的延迟初始化看起来也不错。)
Edit:编辑以阐明 BAR
的值是 fixed 值。它的计算是幂等的,没有副作用。我不介意计算是否跨线程重复,或者如果 BAR
由于内存缓存/可见性而成为有效的线程本地。我担心的是,如果它是非空的,那么它的值是完整的而不是部分的。
您的代码(技术上)不是线程安全的。
String
确实是一个正确实现的不可变类型,而且您所说的有关其 final
字段的内容是正确的。但这不是线程安全问题所在。
第一个问题是 BAR
的惰性初始化中存在竞争条件。如果两个线程同时调用 getBar()
,它们都会将 BAR
视为 null
,然后都尝试对其进行初始化。
第二个问题是内存问题。由于一个线程对 BAR
的写入与另一个线程对 BAR
的后续读取之间没有 happens-before 关系,因此无法保证第二个线程会看到BAR
的初始值。因此,它可能会重复初始化。
请注意,在示例中所写,这两个问题不是实际的线程安全问题。您正在执行的初始化是 idempotent。这对您可能多次初始化 BAR
的代码的行为没有影响,因为您总是将它初始化为对同一个 String
对象的引用。 (单次冗余初始化的开销太小不用担心。)
但是,如果 BAR
是对可变对象的引用,或者如果初始化开销很大,那么这就是一个真正的线程安全问题。
正如@Ravindra 所说,简单的解决方案是将 getBar
声明为 synchronized
。这解决了这两个问题。
您声明 BAR
的想法解决了内存危险,但没有解决竞争条件。
您在问题中添加了以下内容:
Edit to clarify the value intended for
BAR
is a fixed value. Its calculation is idempotent and has no side-effects. I don't mind if the calculation is repeated across threads, or ifBAR
becomes effectively a thread-local due to memory-caching / visibility. My concern is, if it's non-null then it's value is complete and not somehow partial.
这并没有改变我上面所说的。如果值是 String
,那么它是一个正确实现的不可变对象,您将 总是 看到完整的值 irrespective还要别的吗。这就是 JLS 报价所说的!
(实际上,我掩盖了 String
使用非 final
字段来保存延迟计算的哈希码的细节。但是,String::hashCode
实现会处理这个问题。它没有线程安全问题。如果你愿意,自己检查一下。)
您的代码不是线程安全的。看来您可能正在考虑双重检查锁定模式。正确的模式是这样的:
public class Foo {
private static volatile String BAR = null;
private static String getBar() {
String bar = BAR;
if (bar == null) {
synchronized( Foo.class )
if( bar == null ) {
bar = "Hello World!";
BAR = bar;
}
}
return bar;
}
// ...
这里有两件事。
如果
BAR
已经初始化,则不会进入synchronized
块。volatile
在这里是必需的,因为需要一些同步并且BAR
的读取将与对易失性BAR
. 的写入同步
如果
BAR
为 null 那么我们进入synchronized
块,我们必须再次检查BAR
是否仍然为 null 因此我们可以进行检查和分配原子。如果我们不进行原子检查,那么BAR
有可能会被多次初始化。
您引用了 Java 规范。关于 final
关键字。虽然 String
是不可变的并且在内部使用 final
关键字,但这不会影响您的字段 BAR
。字符串很好,但是你的字段仍然是共享内存位置,如果你希望它是线程安全的,则需要同步。
另一位发帖人也提到了实习字符串。他们说在这个特定实例中只有一个 "Hello World!"
对象是正确的,因为 JVM 规范保证字符串是驻留的。这是一种奇怪的线程安全形式,不适用于其他对象,因此只有在您确定它可以正常工作时才使用它。您自己创建的大多数对象将无法使用您现在拥有的代码。
最后我想我要指出的是,因为 "Hello World!"
已经是一个字符串对象,所以尝试 "lazy load" 它没有多大意义。字符串是在您的 class 被加载时由 JVM 创建的,因此它们在您的方法是 运行 时已经存在,甚至在 BAR
第一次被读取时就已经存在。在这种情况下,只有一个字符串,尝试 "lazy load" 字符串没有优势。
public class Foo {
// probably better, simpler
private static final String BAR = "Hello World!";