(preliminary note: maybe this is better suited for codereview?)
EDIT Answer to self; I believe this answer covers all my needs/problems, but of course, comments are welcome. Original question left below for reference.
Hello,
Of interest here is the .getSources()
method. This method is meant to return a list of message sources for a given Locale
.
The two central data structures to this method are sources
and failedLookups
, see the code for comments.
This particular implementation of .getSources()
can only ever return either an empty list or a single-element list, depending on the tryAndLookup()
method which prototype is:
protected abstract MessageSource tryAndLookup(final Locale locale)
throws IOException;
Right now, the logic of the code is as follows:
tryAndLookup
method, record either a success or a failure.Now, why I go to such lengths: I have no control over tryAndLookup()
; it may take an inordinate amount of time to execute before returning a valid source or failing. As a result, I am reluctant to using a coarse lock or synchronized
block.
/**
* Set of locales known to have failed lookup.
*
* <p>When a locale is in this set, it will not attempt to be reloaded.</p>
*/
private final Set<Locale> lookupFailures
= new CopyOnWriteArraySet<Locale>();
/**
* Set of message sources successfully looked up
*
* <p>When a source is in there, it is there permanently for now.</p>
*/
private final ConcurrentMap<Locale, MessageSource> sources
= new ConcurrentHashMap<Locale, MessageSource>();
@Override
protected final List<MessageSource> getSources(final Locale locale)
{
MessageSource source = sources.get(locale);
/*
* If found, return it
*/
if (source != null)
return Arrays.asList(source);
/*
* If it is a registered failure, return the empty list
*/
if (lookupFailures.contains(locale))
return Collections.emptyList();
/*
* OK, try and look it up. On success, register it in the sources map.
* On failure, record the failure an return the empty list.
*/
try {
source = tryAndLookup(locale);
sources.putIfAbsent(locale, source);
// EDIT: fix for bug pinpointed by JBNizet
// was:
// return Arrays.asList(source);
// now is:
return Arrays.asList(sources.get(locale));
} catch (IOException ignored) {
lookupFailures.add(locale);
return Collections.emptyList();
}
}
My question here is threefold:
ConcurrentHashMap
as a ConcurrentMap
implementation, and CopyOnWriteArraySet
as a thread-safe Set
implementation; from the javadoc, these are the best I could find. But was I misled somewhere?.putIfAbsent()
; right now I have always used, and trusted, Guava's LoadingCache
for caching purposes, and this is my first foray out of this territory; is this code actually thread safe?tryAndLookup()
at the same time... What solutions exist to have this method executed only once per lookup?The main way we can avoid such concurrency issues and build reliable code is to work with immutable objects. This is because their state cannot be modified by the interference of multiple threads. However, we can't always work with immutable objects.
1) Immutable objects are by default thread-safe because their state can not be modified once created. Since String is immutable in Java, it's inherently thread-safe. 2) Read-only or final variables in Java are also thread-safe in Java. 3) Locking is one way of achieving thread-safety in Java.
As an alternative to the CopyOnWriteArraySet
, you could use a ConcurrentHashMap
with meaningless values (e.g. always use Boolean.TRUE
as the value - you only care about the keys), or else you could use a ConcurrentSkipListSet which is essentially a concurrent TreeSet
that uses a skiplist instead of a balanced binary tree.
Assuming that tryAndLookup
is fast and doesn't have any side-effects, it doesn't really matter if you occasionally execute it more than once, as as such your "eventually thread-safe" code is thread-safe in the sense that it won't produce any anomalous behavior. (If it's slow then it may be more efficient to use locks in order to ensure that you execute it as few times as possible, but in this case your code would still be free from anomalous behavior. If it has side-effects then you may have a data race if you execute it twice on the same locale.)
Edit Because tryAndLookup
may have side-effects, you can either synchronize on locale
, or else you can modify your method as follows
private final MessageSource nullSource = new MessageSource(); // Used in place of null in the ConcurrentHashMap, which does not accept null keys or values
protected final List<MessageSource> getSources(final Locale locale) {
...
try {
if(sources.putIfAbsent(locale, nullSource) == null) {
source = tryAndLookup(locale);
sources.replace(locale, nullSource, source);
return Arrays.asList(sources.get(locale));
} else {
// Another thread is calling tryAndLookup, so wait for it to finish
while(sources.get(locale) == nullSource && !lookupFailures.contains(locale))
Thread.sleep(500);
}
if(sources.get(locale) != nullSource) {
return Arrays.asList(sources.get(locale));
} else {
return Collections.emptyList();
}
}
} catch (IOException ignored) {
lookupFailures.add(locale);
return Collections.emptyList();
}
}
You can run the first two steps in a synchronized block using simpler data types.
If in those steps you find you need to perform the tryAndLookup()
, store a Future for the pending result in a separate list of pending lookups before leaving the synchronized block.
Then outside the synchronized block, do the actual lookup.
Threads that find they need the same result will find the Future and can get()
its result outside the synchronized block.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With