如果写入的值始终相同,那么延迟初始化引用是否是线程安全的?
Is it thread-safe to lazily initialize references if the written value is always the same?
在我的应用程序中,我需要延迟设置一个变量,因为在 class 初始化期间我无法访问必要的方法,但我还需要该值可以跨多个线程访问。我知道我可以使用 double-checked locking to solve this, but it seems like overkill. The method that I need to call to obtain the value is idempotent and the return value will never change. I'd like to lazily initialize the reference as if I were in a single-threaded environment. It seems like this should work since reads and writes to references are atomic.[1][2]
这是我正在做的一些示例代码。
// views should only be accessed in getViews() since it is
// lazily initialized. Call getViews() to get the value of views.
private List<String> views;
/* ... */
private List<String> getViews(ServletContext servletContext) {
List<String> views = this.views;
if (views == null) {
// Servlet Context context and init parameters cannot change after
// ServletContext initialization:
// https://docs.oracle.com/javaee/6/api/javax/servlet/ServletContext.html#setInitParameter(java.lang.String,%20java.lang.String)
String viewsListString = servletContext.getInitParameter(
"my.views.list.VIEWS_LIST");
views = ListUtil.toUnmodifiableList(viewsListString);
this.views = views;
}
return views;
}
This question about 32-bit primitives is similar,但我想确认对 String
s 和 List
s 等对象的引用的行为是相同的。
看起来这应该可以正常工作,因为每个线程要么看到 null
并重新计算值(这不是问题,因为该值永远不会改变),要么看到已经计算的值。我在这里错过任何陷阱吗?这段代码是线程安全的吗?
This question about 32-bit primitives is similar, but I want to confirm that the behavior is the same for references to objects like Strings and Lists.
是的,因为写引用总是原子的per the JLS:
Writes to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values.
从 Java 5 开始有效。
但请注意:
Without synchronization (synch block or volatile) you might end up with every thread having its own instance of list (every thread could see that views == null
and initialize variable and use its own copy of the list)
...和:
Under this implementation, each thread could end up with a different instance of views
. Is that okay?
你的代码不一定是thread-safe。尽管“[w]引用和读取引用始终是原子的...,”[1] Java 内存模型不保证对象在被其他线程引用时将被完全初始化。 Java 内存模型仅保证对象的 final
字段在任何线程可以看到对它的引用之前被初始化:
A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object’s final fields.
JSR-133: Java Memory Model and Thread Specification
因此,如果 ListUtil.toUnmodifiableList(viewsListString);
returns 一个具有任何 non-final 字段的 List
对象的实现,其他线程可能会看到 List
non-final 字段初始化前的引用。
例如,假设 toUnmodifiableList()
方法的实现类似于:
public static List<String> toUnmodifiableList(final String viewsString) {
return new AbstractList<String>() {
String[] viewsArray = viewsString.split(",");
@Override
public String get(final int index) {
return viewsArray[index];
}
};
}
线程 A 调用 getViews(servletContext)
并发现 views
为 null
因此它尝试初始化 views
.
在调用 toUnmodifiableList()
期间,JVM 执行优化并对指令重新排序,以便执行以下操作:
views = /* Reference to AbstractList<String> prior to initialization */
this.views = views;
/* new AbstractList<String>() occurs and viewsString.split(",") completes */
当线程 A 正在执行时,线程 B 在线程 A 执行 this.views = views;
之后但在 viewsString.split(",")
完成之前调用 getViews(servletContext)
。
现在线程 B 引用了 this.views
,其中 this.views.viewsArray
是 null
,因此对 this.views.get(index)
的任何调用都将导致 NullPointerException
。
为了确保 thread-safety,getViews()
返回的任何对象都需要确保它只有 final
字段,以保证没有线程看到部分已初始化的对象(或者您可以确保在对象中正确处理未初始化的值,但这很可能是不可能的)。我相信您需要确保 getViews()
返回的对象中的所有 Object
引用也只有 final
字段。因此,如果您返回的 List
包含对 MyClass
的 final
引用,您需要确保 MyClass
的所有成员也是 final
.
有关更多信息,请查看:Partial constructed objects in the Java Memory Model。
在我的应用程序中,我需要延迟设置一个变量,因为在 class 初始化期间我无法访问必要的方法,但我还需要该值可以跨多个线程访问。我知道我可以使用 double-checked locking to solve this, but it seems like overkill. The method that I need to call to obtain the value is idempotent and the return value will never change. I'd like to lazily initialize the reference as if I were in a single-threaded environment. It seems like this should work since reads and writes to references are atomic.[1][2]
这是我正在做的一些示例代码。
// views should only be accessed in getViews() since it is
// lazily initialized. Call getViews() to get the value of views.
private List<String> views;
/* ... */
private List<String> getViews(ServletContext servletContext) {
List<String> views = this.views;
if (views == null) {
// Servlet Context context and init parameters cannot change after
// ServletContext initialization:
// https://docs.oracle.com/javaee/6/api/javax/servlet/ServletContext.html#setInitParameter(java.lang.String,%20java.lang.String)
String viewsListString = servletContext.getInitParameter(
"my.views.list.VIEWS_LIST");
views = ListUtil.toUnmodifiableList(viewsListString);
this.views = views;
}
return views;
}
This question about 32-bit primitives is similar,但我想确认对 String
s 和 List
s 等对象的引用的行为是相同的。
看起来这应该可以正常工作,因为每个线程要么看到 null
并重新计算值(这不是问题,因为该值永远不会改变),要么看到已经计算的值。我在这里错过任何陷阱吗?这段代码是线程安全的吗?
This question about 32-bit primitives is similar, but I want to confirm that the behavior is the same for references to objects like Strings and Lists.
是的,因为写引用总是原子的per the JLS:
Writes to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values.
但请注意
Without synchronization (synch block or volatile) you might end up with every thread having its own instance of list (every thread could see that
views == null
and initialize variable and use its own copy of the list)
...和
Under this implementation, each thread could end up with a different instance of
views
. Is that okay?
你的代码不一定是thread-safe。尽管“[w]引用和读取引用始终是原子的...,”[1] Java 内存模型不保证对象在被其他线程引用时将被完全初始化。 Java 内存模型仅保证对象的 final
字段在任何线程可以看到对它的引用之前被初始化:
A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object’s final fields.
JSR-133: Java Memory Model and Thread Specification
因此,如果 ListUtil.toUnmodifiableList(viewsListString);
returns 一个具有任何 non-final 字段的 List
对象的实现,其他线程可能会看到 List
non-final 字段初始化前的引用。
例如,假设 toUnmodifiableList()
方法的实现类似于:
public static List<String> toUnmodifiableList(final String viewsString) {
return new AbstractList<String>() {
String[] viewsArray = viewsString.split(",");
@Override
public String get(final int index) {
return viewsArray[index];
}
};
}
线程 A 调用 getViews(servletContext)
并发现 views
为 null
因此它尝试初始化 views
.
在调用 toUnmodifiableList()
期间,JVM 执行优化并对指令重新排序,以便执行以下操作:
views = /* Reference to AbstractList<String> prior to initialization */
this.views = views;
/* new AbstractList<String>() occurs and viewsString.split(",") completes */
当线程 A 正在执行时,线程 B 在线程 A 执行 this.views = views;
之后但在 viewsString.split(",")
完成之前调用 getViews(servletContext)
。
现在线程 B 引用了 this.views
,其中 this.views.viewsArray
是 null
,因此对 this.views.get(index)
的任何调用都将导致 NullPointerException
。
为了确保 thread-safety,getViews()
返回的任何对象都需要确保它只有 final
字段,以保证没有线程看到部分已初始化的对象(或者您可以确保在对象中正确处理未初始化的值,但这很可能是不可能的)。我相信您需要确保 getViews()
返回的对象中的所有 Object
引用也只有 final
字段。因此,如果您返回的 List
包含对 MyClass
的 final
引用,您需要确保 MyClass
的所有成员也是 final
.
有关更多信息,请查看:Partial constructed objects in the Java Memory Model。