...
In presence of an invariant involving two objects, it is tempting to believe that if operations on the two objects are individually atomic, no additional locking is required; however this is not the case. Similarly, programmers sometimes incorrectly assume that using a thread-safe Collection
does not require explicit synchronization to preserve an invariant involving that involves the collection's elements. A thread-safe class can only guarantee atomicity of its individual methods. A grouping of calls to such methods requires additional synchronization.
For instance, consider a scenario where the standard thread-safe API does not provide a method to both find a particular person's record in a Hashtable
and also update the corresponding payroll information. In such cases, a custom atomic method must be designed and used. This guideline discusses the rationale behind using such a method and provides the necessary relevant implementation advice.
This guideline applies to all Collection
classes including the thread-safe Hashtable
class. Enumerations of objects of a Collection
and iterators also require explicit synchronization on the Collection
object (client-side locking) or any single internal private lock object.
...
This noncompliant code example uses two thread-safe AtomicReference
objects that hold wrap one BigInteger
object reference, each.
Code Block | ||
---|---|---|
| ||
final class AtomicAdderAdder { private final AtomicReference<BigInteger> first; private final AtomicReference<BigInteger> second; public AtomicAdderAdder(BigInteger f, BigInteger s) { first = new AtomicReference<BigInteger>(f); second = new AtomicReference<BigInteger>(s); } public void update(BigInteger f, BigInteger s) { // Unsafe first.set(f); second.set(s); } public BigInteger add() { // Unsafe return first.get().add(second.get()); } } |
An AtomicReference
is an object reference that can be updated atomically. Operations that use these two atomic references independently, are guaranteed to be atomic, however, if an operation involves using both together, thread-safety issues arise. In this noncompliant code example, one thread may call update()
while a second thread may call add()
. This might cause the add()
operation method to add the new value of first
to the old value of second
, yielding an erroneous result.
...
Code Block | ||
---|---|---|
| ||
final class AtomicAdderAdder { // ... public synchronized void update(BigInteger f, BigInteger s){ first.set(f); second.set(s); } public synchronized BigInteger add() { return first.get().add(second.get()); } } |
Prefer using block synchronization instead of method synchronization when the method contains non-atomic operations that either do not require any synchronization or can use a more fine-grained locking scheme involving multiple internal private lock objects. Non-atomic operations can be decoupled from those that require synchronization and executed outside the synchronized block. The guideline CON04-J. Synchronize using an internal private final lock object has more details on using private internal lock objects and block synchronization.
...
This noncompliant code example is comprised of a java.util.ArrayList<E>
collection which is not thread-safe by default. However, most classes that are not thread-safe have a synchronized thread-safe version, for example, Collections.synchronizedList
is a good substitute for ArrayList
and Collections.synchronizedMap
is a good alternative to HashMap
. The atomicity pitfall described in the subsequent paragraph, remains to be addressed even when the particular Collection
offers thread-safety guarantees.
Code Block | ||
---|---|---|
| ||
final | ||
Code Block | ||
| ||
final class IPHolder { private final List<InetAddress> ips = Collections.synchronizedList(new ArrayList<InetAddress>()); public void addIPAddress(InetAddress address) { // Validate address ips.add(address); } public void addAndPrintIP(InetAddress address) throws UnknownHostException { addIPAddress(InetAddress.getLocalHost()address); InetAddress[] ia = (InetAddress[]) ips.toArray(new InetAddress[0]); System.out.println("Number of IPs: " + ia.length); } } |
Even though the Collection
wrapper offers thread-safety guarantees, atomicity related issues manifest themselves when calling methods of the class. When the addAndPrintIP()
method is invoked on the same object from multiple threads, the output , consisting which consists of varying array lengths, may indicate a race condition between the threads. In other words, the The statements in method addAndPrintIP()
that are responsible for adding an IP address and printing out the length of the list, are not sequentially consistent.
...
Code Block | ||
---|---|---|
| ||
final class IPHolder { private final List<InetAddress> ips = Collections.synchronizedList(new ArrayList<InetAddress>()); public void addIPAddress(InetAddress address) { synchronized (ips) { // Validate ips.add(address); } } public void addAndPrintIP(InetAddress address) throws UnknownHostException { synchronized (ips) { addIPAddress(InetAddress.getLocalHost()address); InetAddress[] ia = (InetAddress[]) ips.toArray(new InetAddress[0]); System.out.println("Number of IPs: " + ia.length); } } } |
...
Wiki Markup |
---|
This noncompliant code example defines a class {{KeyedCounter}} which is not thread-safe. Even though the {{HashMap}} is wrapped in a synchronized {{Map}}, the overall increment operation is not atomic. \[[Lee 09|AA. Java References#Lee 09]\] |
Code Block | ||
---|---|---|
| ||
final class KeyedCounter { private final Map<String, Integer> map = Collections.synchronizedMap(new HashMap<String, Integer>()); public void increment(String key) { Integer old = map.get(key); int value = (old == null) ? 1 : old.intValue() + 1; map.put(key, value); } public Integer getCount(String key) { return map.get(key); } } ) { return map.get(key); } } |
The integer overflow check before incrementing has been omitted for brevity; the caller must ensure that the {increment()}} method is called no more than Integer.MAX_VALUE
times for any key to prevent overflow. Refer to INT00-J. Perform explicit range checking to ensure integer operations do not overflow for more information.
Compliant Solution (synchronized blocks)
To ensure atomicity, this compliant solution uses a an internal private internal lock object to synchronize the method bodies statements of the increment()
and getCount()
methods.
Code Block | ||
---|---|---|
| ||
public final class KeyedCounter { private final Map<String, Integer> map = new HashMap<String, Integer>(); private final Object lock = new Object(); public void increment(String key) { synchronized (lock) { Integer old = map.get(key); int value = (old == null) ? 1 : old.intValue() + 1; map.put(key, value); } } public Integer getCount(String key) { synchronized (lock) { return map.get(key); } } } |
...
Wiki Markup |
---|
The previous compliant solution does not scale very well because a class with several {{synchronized}} methods can be a potential bottleneck as far as acquiring locks is concerned, and may further yield a deadlock or livelock. The class {{ConcurrentHashMap}} provides several utility methods to perform atomic operations and is often a good choice, as demonstrated in this compliant solution \[[Lee 09|AA. Java References#Lee 09]\]. |
Code Block | ||
---|---|---|
| ||
public final class KeyedCounter { private final ConcurrentMap<String, AtomicInteger> map = new ConcurrentHashMap<String, AtomicInteger>(); public void increment(String key) { AtomicInteger value = new AtomicInteger(0); AtomicInteger old = map.putIfAbsent(key, value); if (old != null) { value = old; } value.incrementAndGet(); // Increment the value atomically } public Integer getCount(String key) { AtomicInteger value = map.get(key); return (value == null) ? null : value.get(); } } |
Wiki Markup |
---|
According to Goetz et al. \[[Goetz 06|AA. Java References#Goetz 06]\] section 5.2.1. ConcurrentHashMap: |
...