1

I want to create a memoized version of a given Supplier such that multiple threads can use it concurrently with the guarantee that the original supplier's get() is called at most once, and that all of the threads see that same result. Double-checked locking seems like a good fit.

class CachingSupplier<T> implements Supplier<T> {
    private T result = null;

    private boolean initialized = false;

    private volatile Supplier<? extends T> delegate;

    CachingSupplier(Supplier<? extends T> delegate) {
        this.delegate = Objects.requireNonNull(delegate);
    }

    @Override
    public T get() {
        if (!this.initialized && this.delegate != null) {
            synchronized (this) {
                Supplier<? extends T> supplier = this.delegate;
                if (supplier != null) {
                    this.result = supplier.get();
                    this.initialized = true;
                    this.delegate = null;
                }
            }
        }
        return this.result;
    }
}

My understanding is that in this case delegate needs to be volatile because otherwise the code in the synchronized block might be reordered: the write to delegate could happen before the write to result, possibly exposing result to other threads before it has been fully initialized. Is that correct?

So, normally this would entail a volatile read of delegate outside of the synchronized block on each invocation, only entering the synchronized block at most once per contending thread while result is uninitialized and then never again.

But once result has been initialized, is it possible to also avoid the cost, however negligible, of the unsynchronized volatile read of delegate on subsequent invocations by first checking the non-volatile flag initialized and short-circuiting? Or does this buy me absolutely nothing over normal double-checked locking? Or does it somehow hurt performance more than it helps? Or is it actually broken?

gdejohn
  • 7,451
  • 1
  • 33
  • 49
  • Yes. You can drop the `supplier` checks and only check `initialized`. Then set the `supplier` to null in the synchronized block, since it will be unused afterwards. This would be identical to Guava's `Suppliers.memoize` except to allow the delegate to be GC'd. – Ben Manes Dec 30 '17 at 07:13
  • @BenManes I saw the source for that, but their `initialized` flag is `volatile`, so they are still doing a volatile read on every invocation. I'm curious if that can be avoided as well once `result` is initialized. I expect there's something wrong with my approach, and I just can't see it because I don't know enough about Java's memory model. – gdejohn Dec 30 '17 at 07:17
  • A `Supplier` can return a `null` result, so there would be no way to determine if you had a racy read or not without checking a volatile flag, which would act as a memory barrier to ensure consistency. But a volatile read is mostly free on x86 so you wouldn't see a gain in a JMH benchmark even if you relaxed it unsafely. – Ben Manes Dec 30 '17 at 07:29
  • @BenManes Yes, I wanted to account for suppliers that return `null`, that's why I'm piggybacking on the original supplier. The supplier is what's volatile, not the result, and I'm checking if the supplier is null or not, ensuring it's non-null in the constructor and nulling it out at the end of the synchronized block (making it eligible for GC as a small bonus). The non-volatile `initialized` field is there to avoid subsequent volatile reads of the original supplier for what I admit is likely negligible gains. I'm just wondering if it actually works. – gdejohn Dec 30 '17 at 07:37

2 Answers2

3

Don’t implement double-checked locking, use an existing tool that does the work for you:

class CachingSupplier<T> implements Supplier<T> {
    private final Supplier<? extends T> delegate;
    private final ConcurrentHashMap<Supplier<? extends T>,T> map=new ConcurrentHashMap<>();

    CachingSupplier(Supplier<? extends T> delegate) {
        this.delegate = Objects.requireNonNull(delegate);;
    }

    @Override
    public T get() {
        return map.computeIfAbsent(delegate, Supplier::get);
    }
}

Note that more than often, simply doing an eager first-time evaluation and replacing the supplier by a constant returning one before publishing it to other threads is even simpler and sufficient. Or just using a volatile variable and accepting that there might be a few concurrent evaluations if multiple threads encounter a supplier that has not been evaluated yet.


The implementations below are just for informational (academic) purpose, the simpler implemen­tation above is strongly recommended.

You can use publication guarantees of immutable objects instead:

class CachingSupplier<T> implements Supplier<T> {
    private Supplier<? extends T> delegate;
    private boolean initialized;

    CachingSupplier(Supplier<? extends T> delegate) {
        Objects.requireNonNull(delegate);
        this.delegate = () -> {
            synchronized(this) {
                if(!initialized) {
                    T value = delegate.get();
                    this.delegate = () -> value;
                    initialized = true;
                    return value;
                }
                return this.delegate.get();
            }
        };
    }

    @Override
    public T get() {
        return this.delegate.get();
    }
}

Here, initialized is written and read under the synchronized(this) guard, but at the first evaluation, the delegate is replaced by a new Supplier that invariably returns the evaluate value without the need for any check.

Since the new supplier is immutable, it is safe, even if read by a thread which never executed the synchronized block.


As igaz correctly pointed out, the class above is not immune to data races, if the CachingSupplier instance itself is not safely published. An implementation that is completely immune to data races, even when being improperly published, but still working without memory barriers in the ordinary access case, is even more involved:

class CachingSupplier<T> implements Supplier<T> {
    private final List<Supplier<? extends T>> delegate;
    private boolean initialized;

    CachingSupplier(Supplier<? extends T> delegate) {
        Objects.requireNonNull(delegate);
        this.delegate = Arrays.asList(() -> {
            synchronized(this) {
                if(!initialized) {
                    T value = delegate.get();
                    setSupplier(() -> value);
                    initialized = true;
                    return value;
                }
                return getSupplier().get();
            }
        });
    }
    private void setSupplier(Supplier<? extends T> s) {
        delegate.set(0, s);
    }
    private Supplier<? extends T> getSupplier() {
        return delegate.get(0);
    }

    @Override
    public T get() {
        return getSupplier().get();
    }
}

I think that emphasizes even more the beauty of the first solution…

Holger
  • 285,553
  • 42
  • 434
  • 765
  • this should be stamped with `Holger's self mutating lambda` or something like that, just like "double check locking"... – Eugene Jan 03 '18 at 21:37
  • @Eugene: replacing a delegate works without lambda expressions too, one example of this pattern is the Reflection implementation code, since Java 1.3 iirc. I suppose, there is already a name for that, but I don’t have it at hand. – Holger Jan 04 '18 at 07:55
  • This code is not thread-safe, for instance T get can throw an NPE – igaz Jan 10 '18 at 00:10
  • @igaz: well, if the original supplier throws an NPE, this `CachingSupplier` will throw an NPE. Besides that, I don’t see, how an NPE may happen, for which this code would be responsible. – Holger Jan 10 '18 at 07:06
  • I'm not referring to the supplier throwing an NPE - a read by another thread can see CachingSupplier#delegate as null (since #delegate is not final); only if CachingSupplier is safely published itself, would another thread be guaranteed to see a non-null delegate – igaz Jan 10 '18 at 13:08
  • @igaz: I never said that this class protected itself against improper publication, likewise, it does not protect itself against malicious reflective manipulation. If used correctly, it is thread-safe. Publication of the `CachingSupplier` instance is outside the responsibility of this code. – Holger Jan 10 '18 at 13:55
  • When we describe a class as thread-safe, what do we mean? That it is internally thread-safe, or to put it more precisely, if *I have a non-null reference to an instance of this class*, it behaves "correctly". It is not our responsibility to ensure that any reference to CachingSupplier is non-null of course (that is not in our power), but we can ensure that if you have a CachingSupplier instance, it will behave as expected,and throwing an NPE when invoking get() doesn't meet that criteria obviously. And that is in our power. The code as written is not multi-thread-safe,that's not an opinion – igaz Jan 10 '18 at 14:36
  • @igaz: So you’re saying, [`LinkedBlockingQueue`](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/util/concurrent/LinkedBlockingQueue.java#80) is not thread safe, as it initializes its `last` and `head` fields in the [constructor](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/util/concurrent/LinkedBlockingQueue.java#260) without acquiring the lock and they are neither, `volatile` nor `final`, hence, the object requires proper publication? – Holger Jan 10 '18 at 15:24
  • LBQ is thread-safe without regards to safe-publication of course; as I believe all juc classes are - if you've found one that isn't, you've found a bug!! Look at the code more closely (hint: it's not irrelevant that #capacity is final) – igaz Jan 10 '18 at 15:38
  • @igaz: having `final` fields does not guaranty that other fields are safely published. That may work for a particular version of a particular JVM implementation, but still is not different to this code. I didn’t find a bug, I just question your idea of “thread safe” meaning “even thread safe if combined with broken techniques that are not thread safe”. – Holger Jan 10 '18 at 15:42
  • a wrt to LBQ, it is thread-safe without respect to safe-publication (look at the (Collection) constructor (where an explicit lock is acquired for exactly this purpose); you're correct about one final not protecting all other instance variables (it's early here), but my initial point stands; by definition, a multi-thread-safe class does not depend on safe publication for its thread-safety guarantees – igaz Jan 10 '18 at 16:06
  • @igaz: The Java Language Specification dodges from giving a definition of the term “thread safe”, so I updated the answer by simply stating explicitly what is guaranteed and what not, without using this term. I added an alternative that fulfills your requirements. – Holger Jan 10 '18 at 16:07
  • Yeah, I'm not referring to the JLS for a definition of "thread-safe"; class but I meant what I wrote, if you find a j.u.c class that *requires* safe-publication for thread-safety guarantees - you've found a bug. Let Doug Lea know! – igaz Jan 10 '18 at 16:13
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/162913/discussion-between-igaz-and-holger). – igaz Jan 10 '18 at 16:19
2

It is broken, i.e. it is not multi-thread-safe. According to the JMM, simply "seeing" a shared memory value (in your example, a reader thread might see #initialized as true), is not a happens-before relation, and therefore a reader thread could:

load initialized //evaluates true
load result //evaluates null

Above is an allowed execution.

There is no way to avoid the "cost" of a synchronize action (e.g. a volatile read of a volatile write) and at the same time avoid a data race (and consequently broken code). Full stop.

The conceptual difficulty is in breaking the common sense inference that for a thread to see initialized as true -> there had to be a prior write of true to initialized; hard as it may be to accept, that inference is incorrect

And as Ben Manes points out, volatile reads are just plain-old loads on x-86

igaz
  • 392
  • 3
  • 9
  • Considering the changes to the JMM in Java 1.5, don't the writes to `initialized` and `result` have a happens-before relation with the write to `delegate` because `delegate` is `volatile`? – gdejohn Dec 30 '17 at 12:38
  • In your code, a reader thread need not ever read #delegate; look at the legal execution in my response. – igaz Dec 30 '17 at 12:42
  • Is it possible for a thread to see `initialized` as `true` and `result` as `null` because the writes to those variables in the synchronized block can still be reordered with respect to each other, even though they both must come before the write to `delegate`? – gdejohn Dec 30 '17 at 12:55
  • 1
    I don't find it useful to think about "reordering" per se; the "anomalous" behavior might result from speculative execution or branch prediction or whatever else the hardware architects have come up with the speed up our programs. The JMM frees you from having to know about what's happening at the hardware level. The specific answer to your question is it's possible for a *reader* thread to see initialized (true); result(null) *because there is no happens-before relation between a reader thread (Thread B) reading initialized and the writer thread (Thread A) writing initialized (true)* – igaz Dec 30 '17 at 13:03