如果写入的值始终相同,那么延迟初始化引用是否是线程安全的?

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,但我想确认对 Strings 和 Lists 等对象的引用的行为是相同的。

看起来这应该可以正常工作,因为每个线程要么看到 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) 并发现 viewsnull 因此它尝试初始化 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.viewsArraynull,因此对 this.views.get(index) 的任何调用都将导致 NullPointerException


为了确保 thread-safety,getViews() 返回的任何对象都需要确保它只有 final 字段,以保证没有线程看到部分已初始化的对象(或者您可以确保在对象中正确处理未初始化的值,但这很可能是不可能的)。我相信您需要确保 getViews() 返回的对象中的所有 Object 引用也只有 final 字段。因此,如果您返回的 List 包含对 MyClassfinal 引用,您需要确保 MyClass 的所有成员也是 final .

有关更多信息,请查看:Partial constructed objects in the Java Memory Model