在我的应用程序中,我需要懒惰地设置一个变量,因为我在类初始化期间无法访问必要的方法,但我还需要跨多个线程访问该值。我知道我可以使用double-checked locking来解决这个问题,但这似乎有些过分。 我需要调用以获取值的方法是幂等的,返回值永远不会改变。我喜欢懒惰地初始化引用,就像我在单线程环境中一样。看起来这应该有效,因为对引用的读写都是原子的。[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
和List
s等对象的引用行为相同。
看起来这应该可以正常工作,因为每个线程都会看到null
并重新计算值(因为值永远不会改变而不是问题)或者看到已经计算过的值。我错过了任何陷阱吗?这段代码是否是线程安全的?
答案 0 :(得分:2)
关于32位基元的这个问题是类似的,但我想确认对字符串和列表等对象的引用的行为是相同的。
是的,因为写引用总是原子per the JLS:
对引用的写入和读取始终是原子的,无论它们是作为32位还是64位实现。
Peter Lawrey notes这从Java 5开始有效。
但请注意Ivan's observation:
如果没有同步(同步块或易失性),您可能最终会得到每个线程都有自己的列表实例(每个线程都可以看到
import numpy as np from itertools import chain a = np.array([[1,2,3,4],[5,6,7,8]]) for i in chain.from_iterable(a): print(i) 1 2 3 4 5 6 7 8
并初始化变量并使用自己的列表副本)
...和erickson's question:
在此实现中,每个线程最终可能会使用
views == null
的不同实例。那可以吗?
答案 1 :(得分:0)
您的代码不一定是线程安全的。尽管“ [w]引用的读取和读取始终是原子的……” [[1],但是Java内存模型不能保证对象在被其他线程引用时将被完全初始化。 Java内存模型仅保证对象的final
字段将在任何线程看到引用之前被初始化:
一个线程只能在完全初始化该对象之后才能看到对该对象的引用,这可以确保看到该对象的最终字段的正确初始化值。
JSR-133: Java Memory Model and Thread Specification
因此,如果ListUtil.toUnmodifiableList(viewsListString);
的实现返回具有任何非最终字段的List
对象,则其他线程可能会在非最终字段被引用之前看到List
引用。初始化。
例如,假设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执行getViews(servletContext)
之后但在this.views = views;
完成之前调用viewsString.split(",")
。
现在线程B引用了this.views
,其中this.views.viewsArray
是null
,因此对this.views.get(index)
的任何调用都将产生NullPointerException
。
为了确保线程安全,getViews()
返回的任何对象都需要确保其只有final
个字段,以确保没有线程看到部分初始化的对象(或者您可以可以确保在对象中正确处理了未初始化的值,但这不可能。我相信您需要确保Object
返回的对象中的所有getViews()
引用也都只有final
字段。因此,如果您返回的List
包含对final
的{{1}}引用,则需要确保MyClass
的所有成员也都是MyClass
有关更多信息,请查看:Partial constructed objects in the Java Memory Model。