2

I'm trying to lazily initialize a map in a thread safe way. I've come up with this following the initialization-on-demand holder idiom:

private static class NameIndexMapHolder {
    private static final Map<String, Long> NAME_INDEX_MAP;
    static {
        Map<String, Long> map = new HashMap<>();
        map.put("John", 3424534643);
        map.put("Jane", 4328759749);
        NAME_INDEX_MAP = Collections.unmodifiableMap(map);
    }
}

public static Map<String, Long> getNameIndexMap() {
    return NameIndexMapHolder.NAME_INDEX_MAP;
}

Does this work? Is it thread safe? From what I've read this works for Singletons only. The only other alternative I've read is double checked locking which seems to have its own issues.

ujjwal-gupta
  • 123
  • 1
  • 12

3 Answers3

3

Static blocks are guaranteed to be initialized in a thread-safe manner. A singleton is just one use case. Your code is entirely thread safe. See this discussion for more details.

Your initialization, however, is only pseudo-lazy (my own made-up term). Classes are not initialized until referenced in your code (lazy class initialization by the JVM), but your map is technically initialized immediately (when the class is first accessed). See this discussion for more information on class loading.

Community
  • 1
  • 1
Nathan
  • 66
  • 4
1

Yes, this is thread-safe and lazy.

First, let's look at whether it's thread-safe:

  1. Can two threads assign a reference to NAME_INDEX_MAP? No. That happens in the static initializer, which is only executed when the class is initialized (JLS 8.7). Class initialization uses synchronization to ensure that only one thread will execute the initializer (JLS 12.4.2).
  2. Is the initial publication safe? Yes. The JLS is actually a bit fuzzy on this, but the VM definition (JVMS) is explicit:

    1: Synchronize on the initialization lock, LC, for C. ...

    ...

    4: If the Class object for C indicates that C has already been initialized, then no further action is required. Release LC and complete normally.

    ...

    A Java Virtual Machine implementation may optimize this procedure by eliding the lock acquisition in step 1 (and release in step 4/5) when it can determine that the initialization of the class has already completed, provided that, in terms of the Java memory model, all happens-before orderings (JLS §17.4.5) that would exist if the lock were acquired, still exist when the optimization is performed.

    (JVMS 5.5, emphasis added)

  3. Will the object be modified after publication? No. It is wrapped in an unmodifiableMap, with no other reference to the underlying, modifiable HashMap.

So, is it lazy? Yes; class initialization only happens immediately before any members of that class are accessed for the first time (JLS 12.4.1), and in this case, you have only the one field. So the initialization will only happen immediately before the first time you access NAME_INDEX_MAP, which is the laziness you want.

yshavit
  • 42,327
  • 7
  • 87
  • 124
0

Unless I'll be writing code for java < 5 I would go with double checked locking:

public final class Example {
    private static final Object LOCK = new Object();
    private static volatile Map<String, Long> NAME_INDEX_MAP;

    public static Map<String, Long> getNameIndexMap() {
        if (null == NAME_INDEX_MAP) {
            synchronized (LOCK) {
                if (null == NAME_INDEX_MAP) {
                    NAME_INDEX_MAP = new HashMap<>();
                    NAME_INDEX_MAP.put("abc", 123);

                    //maybe make it immutable or use a ConcurrentMap instead?
                    NAME_INDEX_MAP = Collections.unmodifiableMap(NAME_INDEX_MAP);
                }
            }
        }

        return NAME_INDEX_MAP;
    }
}

The key to making the double checked locking idiom working for java >= 5 is declaring the map reference volatile which will make the JVM put the necessary happens-before constraints.

Svetlin Zarev
  • 14,713
  • 4
  • 53
  • 82