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

时间:2018-05-23 18:27:42

标签: java multithreading concurrency volatile

在我的应用程序中,我需要懒惰地设置一个变量,因为我在类初始化期间无法访问必要的方法,但我还需要跨多个线程访问该值。我知道我可以使用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,但我想确认对StringList s等对象的引用行为相同。

看起来这应该可以正常工作,因为每个线程都会看到null并重新计算值(因为值永远不会改变而不是问题)或者看到已经计算过的值。我错过了任何陷阱吗?这段代码是否是线程安全的?

2 个答案:

答案 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)并发现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执行getViews(servletContext)之后但在this.views = views;完成之前调用viewsString.split(",")

现在线程B引用了this.views,其中this.views.viewsArraynull,因此对this.views.get(index)的任何调用都将产生NullPointerException


为了确保线程安全,getViews()返回的任何对象都需要确保其只有final个字段,以确保没有线程看到部分初始化的对象(或者您可以可以确保在对象中正确处理了未初始化的值,但这不可能。我相信您需要确保Object返回的对象中的所有getViews()引用也都只有final字段。因此,如果您返回的List包含对final的{​​{1}}引用,则需要确保MyClass的所有成员也都是MyClass

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