Search code examples
javamultithreadingconcurrencyvolatile

Is it thread-safe to lazily initialize references if the written value is always the same?


In my application, I need to set a variable lazily since I don't have access to the necessary methods during class initialization, but I also need that value to be accessible across multiple threads. I know that I could use 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]

Here's some example code for what I'm doing.

// 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, but I want to confirm that the behavior is the same for references to objects like Strings and Lists.

Seemingly this should work fine since each thread will either see null and recompute value (not a problem since the value never changes) or see the already computed value. Am I missing any pitfalls here? Is this code thread-safe?


Solution

  • Your code is not necessarily thread-safe. Although "[w]rites to and reads of references are always atomic...,"[1] the Java Memory Model provides no guarantee that the object will be completely initialized when referenced by other threads. The Java Memory Model only guarantees that an object's final fields will be initialized before any threads can see a reference to it:

    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

    So if the implementation of ListUtil.toUnmodifiableList(viewsListString); returns a List object that has any non-final fields, it's possible that other threads will see the List reference before the non-final fields are initialized.


    So for example, let's say the implementation of toUnmodifiableList() method was something like:

    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];
            }
        };
    }
    

    Thread A calls getViews(servletContext) and finds views to be null so it attempts to initialize views.

    During the call to toUnmodifiableList(), the JVM performs an optimization and reorders the instructions so that the following execution occurs:

    views = /* Reference to AbstractList<String> prior to initialization */
    this.views = views;
    /* new AbstractList<String>() occurs and viewsString.split(",") completes */
    

    While Thread A is executing, Thread B calls getViews(servletContext) after Thread A executes this.views = views; but before viewsString.split(",") completes.

    Now Thread B has a reference to this.views where this.views.viewsArray is null, so any calls to this.views.get(index) will result in a NullPointerException.


    In order to ensure thread-safety, any object returned by getViews() would need to ensure that it has only final fields in order to guarantee that no threads ever see a partially initialized object (or you could ensure that uninitialized values are handled correctly in the object, but that is likely not possible). I believe you would need to ensure that all Object references within the object returned by getViews() also have only final fields as well. So if you returned a List that contained a final reference to MyClass, you would need to make sure that all of MyClass's members are final too.

    For more information, check out: Partial constructed objects in the Java Memory Model.