...
This noncompliant code example is comprised of an ArrayList
collection which is non-thread-safe by default. There is, however, a way around this drawback. Most thread-unsafe classes 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 coming lines, remains to be addressed even when the particular Collection
offers thread-safety benefits.
Wiki Markup |
---|
The operations within a thread's {{run()}} method are non-atomic. That is, it is possible for the first thread to operate on data that it does not expect. The superfluous data may be fed in by other threads whilst the first thread is still executing. Conversely, as the {{toArray()}} method produces a copy of the parameter, it is possible that the first thread will operate on stale data \[[JavaThreads 04|AA. Java References#JavaThreads 04]\]. The code's output, consisting of varying array lengths, indicates a race condition. Such omissions can be pernicious in methods that use complex mathematical formulas. |
Code Block |
---|
|
class RaceCollection implements Runnable {
private List<InetAddress> ips = Collections.synchronizedList(new ArrayList<InetAddress>());
public void addIPAddress(InetAddress ia) {
// Validate
ips.add(ia);
}
public void run() {
try {
addIPAddress(InetAddress.getLocalHost());
InetAddress[] ia = (InetAddress[]) ips.toArray(new InetAddress[0]);
System.out.println("Number of IPs: " + ia.length);
} catch (UnknownHostException e) { /* Forward to handler */ }
} |
Code Block |
---|
|
class RaceCollection implements Runnable {
private List<InetAddress> ips = Collections.synchronizedList(new ArrayList<InetAddress>());
public static void addIPAddressmain(InetAddressString[] iaargs) {
synchronized(ips) {
ips.add(iaRaceCollection rc = new RaceCollection();
}
for(int i }
= 0; publici void removeIPAddress(InetAddress ia< 2; i++) {
synchronized(ips) {
ips.remove(ianew Thread(rc).start();
}
}
public void nonAtomic() throws InterruptedException {
InetAddress[] ia;
synchronized(ips) {
ia = (InetAddress[]) ips.toArray(new InetAddress[0]);
}
// This statement should be in the synchronized block above
System.out.println("Number of IPs: " + ia.length);
}
public void run() {
try {
addIPAddress(InetAddress.getLocalHost());
nonAtomic();
} catch (UnknownHostException e) { /* Forward to handler */ }
catch (InterruptedException e) { /* Forward to handler */ }
}
public static void main(String[] args) {
RaceCollection rc1 = new RaceCollection();
for(int i = 0; i < 2; i++) {
new Thread(rc1).start();
}
}
}
|
Compliant Solution
Wiki Markup |
---|
To eliminate the race condition, ensure atomicity. This can be achieved by including all statements that use the array list within the synchronized block. This technique is also called client-side locking. \[[Goetz 06|AA. Java References#Goetz 06]\] |
The operations within a thread's run()
method are non-atomic. That is, it is possible for the first thread to operate on data that it does not expect. This noncompliant code's output, consisting of varying array lengths, indicates a race condition between threads. In other words, the statements are not sequentially consistent.
Compliant Solution
Wiki Markup |
---|
To eliminate the race condition, ensure atomicity by using the underlying list's lock. This can be achieved by including all statements that use the array list within a synchronized block that locks on the list. This technique is also called client-side locking \[[Goetz 06|AA. Java References#Goetz 06]\]. |
Code Block |
---|
|
public void addIPAddress(InetAddress ia) {
synchronized(ips) { // Also synchronize this method
// Validate
ips.add(ia);
}
}
public void run() {
try {
synchronized(ips) {
addIPAddress(InetAddress.getLocalHost());
|
Code Block |
---|
|
synchronized(ips) {
ia = (InetAddress[]) ips.toArray(new InetAddress[0]);
System.out.println("Number of IPs: " + ia.length);
}
"Number of IPs: " + ia.length);
}
} catch (UnknownHostException e) { /* Forward to handler */ }
}
|
This form of synchronization is preferable over method synchronization for performance reasons. The advice suggested by this guideline applies to all uses of Note that this advice applies to all Collection
classes including the thread-safe Hashtable
class. Enumerations of the objects of a Collection
and iterators also require explicit synchronization on the Collection
object or any single lock object.
Wiki Markup |
---|
Although expensive, {{CopyOnWriteArrayList}} and {{CopyOnWriteArraySet}} classes are sometimes used to create copies of the core {{Collection}} so that iterators do not fail with a runtime exception when some data in the {{Collection}} is modified. TheseHowever, however,any sufferupdates fromto the {{toArray}} dilemma (operating on stale data) described earlier in this ruleCollection}} are not immediately visible to other threads. Consequently, their use is limited to boosting performance in code where the writes are fewer (or non-existent) as compared to the reads \[[JavaThreads 04|AA. Java References#JavaThreads 04]\]. In all other cases they must be avoided. |
Compliant Solution
...
Composition offers more some benefits as compared to the previous solution, although at the cost of a slight performance penalty (refer to [OBJ07-J. Understand how a superclass can affect a subclass] for details on how to implement composition). This allows the {{CompositeCollection}} class to use its own intrinsic lock in a way that is completely independent of the lock of the underlying list class. This provides consistent locking even when the underlying list is not thread-safe or when it changes its locking policy. \[[Goetz 06|AA. Java References#Goetz 06]\]
...
CompositeCollection
class to use its own intrinsic lock in a way that is completely independent of the lock of the underlying list class.
Code Block |
---|
|
class CompositeCollection implements Runnable {
private List<InetAddress> ips;
public CompositeCollection(List<InetAddress> list) {
this.ips = list;
}
public synchronized void addIPAddress(InetAddress ia) {
// Validate
ips.add(ia);
}
public void run() {
try {
InetAddress[] ia;
ia = (InetAddress[]) ips.toArray(new InetAddress[0]);
System.out.println("Number of IPs: " + ia.length);
} catch (UnknownHostException e) { /* Forward to handler */ }
}
}
|
Wiki Markup |
---|
This provides consistent locking even when the underlying list is not thread-safe or when it changes its locking policy. \[[Goetz 06|AA. Java References#Goetz 06]\] |
...
Wiki Markup |
---|
Yet another method is to extend the base class and synchronize on the method that is desired to be atomic, however, it is not recommended because it goes against the spirit of limiting class extension ([OBJ05-J. Limit the extensibility of non-final classes and methods to only trusted subclasses]). Moreover, Goetz et al. \[[Goetz 06|AA. Java References#Goetz 06]\] cite other reasons: |
...