Using AtomicReference for lazy init

127 views Asked by At

can the lazy init be done safely this way? We had a dispute where a colleague was claming for some first threads it could fail and they would see the null value.

public class TestAtomicReference {
    static final AtomicReference<String> A = new AtomicReference<>();

    public static void main(String[] args) throws Exception {

        Callable<Integer> callable = () -> {
            lazyInit();
            return 0;
        };

        ExecutorService executorService = Executors.newFixedThreadPool(50);

        for (int i = 0; i < 1000; i++) {
            A.set(null);
            List<Future<Integer>> futures = executorService.invokeAll(Collections.nCopies(50, callable));
            futures.forEach(f -> {
                try {
                    f.get();
                } catch (Exception ignore) {
                }
            });
        }

        executorService.shutdown();
        executorService.awaitTermination(10, TimeUnit.SECONDS);
    }

    private static void lazyInit() {
**        if (A.get() == null) {
            A.compareAndSet(null, costlyGetValue());
        }
**
        if (A.get() == null) {
            System.out.println("INIT FAILURE!");
        }
    }

    private static String costlyGetValue() {
        return "A";
    }
}

The separation of get & compareAndSet is there to avoid a costly get every time. This test code never fails... Thanks

1

There are 1 answers

1
DuncG On

It does look like your code will work as only one thread can change the atomic reference from null to not null - see compareAndSet(V expectedValue, V newValue). From that point, all threads will see non-null A.get()

However it isn't a good way to lazy initialise as you could have many threads evaluate a.get() == null. Therefore all but one of your initial calls may evaluate costlyGetValue() and all but one result of these costly call are discarded. This could cause serious issues on systems, say, when the operation uses significant I/O or database access.

You can demonstrate by changing the test code like this and which may show that up to 50 threads call costlyGetValue():

private static String costlyGetValue() {
    System.out.println(Thread.currentThread()+" costlyGetValue()");
    // if it doesn't show, add sleep here too
    return "A";
}

Instead you can change the lazy initialisation to be handled by an holder class. The JVM will initialise the holder class once only, making just one call to costlyGetValue(). You can use Holder.getInstance() whenever you wish to read the value:

static class Holder {
    private static final Holder INSTANCE = new Holder();
    private final String costlyValue;
    private Holder() {
        this.costlyValue = costlyGetValue();
    }
    public static String getInstance() {
        // Useful to add here: 
        // Objects.requireNonNull(INSTANCE.costlyValue);
        return INSTANCE.costlyValue;
    }
}
private static void lazyInit() {
    if (Holder.getInstance() == null) {
        System.out.println("INIT FAILURE!");
    }
}