0

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?

stiemannkj1
  • 4,418
  • 3
  • 25
  • 45
  • 1
    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) – Ivan May 23 '18 at 18:33
  • Access to references and primitives is atomic. The issues with atomic access to 64-bit values was resolved from Java 5.0 – Peter Lawrey May 23 '18 at 18:33
  • 3
    Under this implementation, each thread could end up with a different instance of `views`. Is that okay? – erickson May 23 '18 at 18:33
  • 1
    @erickson, ah I didn't really think about that, technically all those instances would contain equivalent data, but I guess that could cause potential memory issues. For the sake of the question, let's assume that each thread getting it's own instance is okay, but I'm glad to take that pitfall into account. Thanks! – stiemannkj1 May 23 '18 at 18:40
  • 1
    @stiemannkj1 String's hash is a lazily initialized, 32-bit value without volatile semantics since racing computations have the same result. However it is usually not worth such tricks unless isolated to extremely safe, performance critical code. – Ben Manes May 23 '18 at 21:53

2 Answers2

1

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.

stiemannkj1
  • 4,418
  • 3
  • 25
  • 45
0

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.

Yes, because writing references is always atomic 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.

Peter Lawrey notes that this is valid from Java 5 onward.

But note Ivan's observation:

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)

...and erickson's question:

Under this implementation, each thread could end up with a different instance of views. Is that okay?

stiemannkj1
  • 4,418
  • 3
  • 25
  • 45
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 1
    The last statement is true from Java 5.0 – Peter Lawrey May 23 '18 at 18:35
  • @PeterLawrey - Thanks! I didn't know it had ever not been true. I did use Java pre 5 (Java 1.0, in fact), but the first time I had to know this must have been after Java 5 wa sout. :-) – T.J. Crowder May 23 '18 at 18:37
  • 1
    Pre Java 5.0 it was implementation dependant for 64-bit primitives, however the x64 version happened to be atomic which means you were even less likely to come across the issue. – Peter Lawrey May 23 '18 at 18:47