Java Concurrency in Practice Notes: Composing Objects

(This notes is from reading Brian Goetz‘s Java Concurrency in Practice)

Chapter 4 Composing Objects

We don’t want to have to analyze each memory access to ensure that our program is thread-safe; we want to be able to take thread-safe components and safely compose them into larger components and programs.

This chapter covers patterns for structuring classes that can make it easier to make programs thread-safe and maintain programs without accidentally undermaining their saftey guarantees.

Designing a thread-safe class

The design process for a thread-safe class should include three basic elements:

  • Identify the variables that form the object’s state
  • Identify the invariants that contain the state variables
  • Establish a policy for managing concurrent access to the object’s state

The synchronization policy defines how an object coordinates access to its state without violating its invariants or postconditions. It specifies what combinations of immutability, thread confinement, and locking is used to maintain thread safety, and which variables are guarded by which locks.

Gathering synchronization requirements

You cannot ensure thread safety without understanding an object’s invariants and postconditions. Constraints on the valid values or state transitions for state variables can create atomicity and encapsulation requirements.

State-dependent operations

Class invariants and method postconditions constrain the valid states and state transitions for an object. Operations with state-based preconditions are called state-dependent.

Concurrent programs add the possibility of waiting util the precondition becomes true, and then proceeding with the operation.

State ownership

When defining which variables form an object’s state, we want to consider only the data that object owns. Ownership is not embodied explicitly in the language, but instead an element of class design.

In many cases, ownership and encapsulation go together - the object encapsulates the state it owns and owns the state it encapsulates.

Collection classes often exhibit a form of “split ownership”, in which the collection owns the state of the collection infrastructure, but client code owns the objects stored in the collection.

Instance confinement

Encapsulation simplifies making classes thread-safe by promoting instance confinement often just called confinement. When an object is encapsulated with another object, all code paths that have access to the encapsulated object are known and can be therefore analyzed more easily than if that object were accessible to the entire program.

Encapsulating data within an object confines access to the data to the object’s methods, making it easier to ensure that the data is always accessed with the appropriate lock held. The code below illustrates how confinement and locking can work together to make a class thread-safe even when its component state variables are not.

1
2
3
4
5
6
7
8
9
10
11
12
@ThreadSafe
public class PersonSet {
@GuardedBy("this") private final Set<Person> mySet = new HashSet<Person>();
public synchronized void addPerson(Person p) {
mySet.add(p);
}
public synchronized boolean containsPerson(Person p) {
return mySet.contains(p);
}
}

Confinement makes it easier to build thread-safe classes because a class that confines its state can be analyzed for thread safety without having to examine the whole program.

The Java monitor pattern

Followint the principle of instance confinement to its logic conclusion leads you to the Java monitor pattern. An object following the Java monitor pattern encapsulates all its mutable state and guards it with the object’s own intrinsic lock.

Any lock object could be used to guard an object’s state so long as it is used consistently. The code below illustrates a class that uses a private lock to guard its state.

1
2
3
4
5
6
7
8
9
10
public class PrivateLock {
private final Object myLock = new Object();
@GuardedBy("myLock") Widget widget;
void someMethod() {
synchronized (myLock) {
// Access or modify the state of widget
}
}
}

Making the lock object private encapsulates the lock so that client code cannot acquire it, whereas a publicly accessible lock allows client code to participate in its synchronization policy - correctly or incorrectly.

Example: tracking fleet vehicles

A “vehicle tracker” is a less trivial example, it’s for dispatching fleet vehicles. We’ll build it first using the monitor pattern, and then see how to relax some of the encapsulation requirements while retaining thread safety.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@ThreadSafe
public class MonitorVehicleTracker {
@GuardedBy("this") private final Map<String, MutablePoint> locations;
public MonitorVehicleTracker(Map<String, MutablePoint> locations) {
this.locations = deepCopy(locations);
}
public synchronized Map<String, MutablePoint> getLocations() {
return deepCopy(locations);
}
public synchronized MutablePoint getLocation(String id) {
MutablePoint loc = locations.get(id);
return loc == null ? null : new MutablePoint(loc);
}
public synchronized void setLocation(String id, int x, int y) {
MutablePoint loc = locations.get(id);
if (loc == null)
throw new IllegalArgumentException("No such ID: " + id);
loc.x = x;
loc.y = y;
}
private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> m) {
Map<String, MutablePoint> result = new HashMap<String, MutablePoint>();
for (String id : m.keySet())
result.put(id, new MutablePoint(m.get(id)));
return Collections.unmodifiableMap(result);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@NotThreadSafe
public class MutablePoint {
public int x, y;
public MutablePoint() {
x = 0;
y = 0;
}
public MutablePoint(MutablePoint p) {
this.x = p.x;
this.y = p.y;
}
}

Even though MutablePoint is not thread-safe, the tracker class is.

Delegating thread safety

The Java monitor patter nis useful when building classes from scratch or composing classes out of objects that are not thread-safe. But what if the components of our class are already thread-safe?

The answer is … “it depends”. In some cases a composite made of thread-safe components is thread-safe, and in others it is merely a good start.

Example: vehicle tracker using delegation

1
2
3
4
5
6
7
8
9
@Immutable
public class Point {
public final int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}

Point is now thread-safe because it is immutable. And we no longer need to copy the locations when returning them.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@ThreadSafe
public class DelegatingVehicleTracker {
private final ConcurrentMap<String, Point> locations;
private final Map<String, Point> unmodifiableMap;
public DelegatingVehicleTracker(Map<String, Point> points) {
locations = new ConcurrentHashMap<String, Point>(points);
unmodifiableMap = Collections.unmodifiableMap(locations);
}
public Map<String, Point> getLocations() {
return unmodifiableMap;
}
public Point getLocation(String id) {
return locations.get(id);
}
public void setLocation(String id, int x, int y) {
if (locations.replace(id, new Point(x, y)) == null)
throw new IllegalArgumentException("invalid vehicle name: " + id);
}
}

DelegatingVehicleTracker doese not use any explicit synchronization; all access to state is managed by ConcurrentHashMap, and all the keys and values of the Map are immutable.

If an unchanging view is required, we could code like this:

1
2
3
4
public Map<String, Point> getLocationsAsStatic() {
return Collections.unmodifiableMap(
new HashMap<String, Point>(locations));
}

Independent state varaibles

The delegation examples so far delegate to a single, thread-safe state variable. We could also delegate thread safety to more than one underlying state variable as long as those underlying state variables are independent, meaning the composite class does not impose any invariants involving the multiple state variables.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class VisualComponent {
private final List<KeyListener> keyListeners
= new CopyOnWriteArrayList<KeyListener>();
private final List<MouseListener> mouseListeners
= new CopyOnWriteArrayList<MouseListener>();
public void addKeyListener(KeyListener listener) {
keyListeners.add(listener);
}
public void addMouseListener(MouseListener listener) {
mouseListeners.add(listener);
}
public void removeKeyListener(KeyListener listener) {
keyListeners.remove(listener);
}
public void removeMouseListener(MouseListener listener) {
mouseListeners.remove(listener);
}
}

VisualComponent is a graphical component that allows client to register listeners for mouse and keystroke events. Since there are no relationship betwee nthe set of mouse listeners and key listeners, the weo are independent, and therefore VisualComponent can delegate its thread safety obligations to two underlying thread-safe lists.

When delegation fails

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class NumberRange {
// INVARIANT: lower <= upper
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i) {
// Warning -- unsafe check-then-act
if (i > upper.get())
throw new IllegalArgumentException("can't set lower to " + i + " > upper");
lower.set(i);
}
public void setUpper(int i) {
// Warning -- unsafe check-then-act
if (i < lower.get())
throw new IllegalArgumentException("can't set upper to " + i + " < lower");
upper.set(i);
}
public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}

NumberRange uses two AtomicIntegers to manage its state, but imposes an additional constraint - that the first number be less than or equal to the second.

NumberRange is not thread-safe. The setLower and setUpper methods attempt to respect this invariant but do so poorly. Both of them are check-then-act sequences, but they do not use sufficient locking to make them atomic.

If a class is composed of multiple independent thread-safe state variables and has no operations that have any invalid state transitions, then it can delegate thread safety to the underlying state variables.

If a class has compound actions, as NumberRange does, delegation alone is again not a suitable approach for thread safety. In these cases, the class must provide its own locking to encure that compound actions are atomic.

Publishing underlying state variables

When you delegate thread safety to an object’s underlying state varaiables, under what conditions can you publish those variables so that other classes can modify them as well?

The answer depends on what invariants your class impose on those variables.

If a state variable is thread-safe, does not participate in any invariants that constrain its value, and has no prohibited state transitions for any of its operations, then it can safely be published.

Example: vehicle tracker that publishes its state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@ThreadSafe
public class SafePoint {
@GuardedBy("this") private int x, y;
private SafePoint(int[] a) {
this(a[0], a[1]);
}
public SafePoint(SafePoint p) {
this(p.get());
}
public SafePoint(int x, int y) {
this.set(x, y);
}
public synchronized int[] get() {
return new int[]{x, y};
}
public synchronized void set(int x, int y) {
this.x = x;
this.y = y;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@ThreadSafe
public class PublishingVehicleTracker {
private final Map<String, SafePoint> locations;
private final Map<String, SafePoint> unmodifiableMap;
public PublishingVehicleTracker(Map<String, SafePoint> locations) {
this.locations = new ConcurrentHashMap<String, SafePoint>(locations);
this.unmodifiableMap = Collections.unmodifiableMap(this.locations);
}
public Map<String, SafePoint> getLocations() {
return unmodifiableMap;
}
public SafePoint getLocation(String id) {
return locations.get(id);
}
public void setLocation(String id, int x, int y) {
if (!locations.containsKey(id))
throw new IllegalArgumentException("invalid vehicle name: " + id);
locations.get(id).set(x, y);
}
}

Adding functionality to existing thread-safe classes

Need a thread-safe List with an atomic put-if-absent operation.

The safest way to add a new atomic operation is to modify the original class to support the desired operation, but this is not always possible because you may not have acess to the source code or may not be free to modify it.

Another apporach is to extend the class, assuming it was designed for extension.

1
2
3
4
5
6
7
8
9
@ThreadSafe
public class BetterVector <E> extends Vector<E> {
public synchronized boolean putIfAbsent(E x) {
boolean absent = !contains(x);
if (absent)
add(x);
return absent;
}
}

Extension is more fragile than adding code directly to a class, because the implementation of the synchronization policy is now distributed over multiple, separately maintained source files.

Client=side locking

For an ArrayList wrapped with a Collections.synchronizedList wrapper, neither of these approaches - adding a method to the original class or extending the class - works because the client code does not even know the class of the List object returned from the syncrhonized wrapper factories. A third strategy is to extend the functionality of the class without extending the class it self by placing extension code in a “helper” class.

1
2
3
4
5
6
7
8
9
10
11
@NotThreadSafe
class BadListHelper <E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}

Unfortunately it won’t work because it synchronizes on the wrong lock. Whatever lock the List uses to guard its state, it sure isn’t the lock on the ListHelper.

To make this approach work, we have to use the same lock that the List uses by using client-side locking or external locking. Client-side locking entails guarding client code that uses some object X with the lock X uses to guard its own state. In order to use client-side locking, you must know what lock X uses.

1
2
3
4
5
6
7
8
9
10
11
12
13
@ThreadSafe
class GoodListHelper <E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public boolean putIfAbsent(E x) {
synchronized (list) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
}

Client-side locking has a lot in common with class extension - they both couple the behavior of the derived class to the implementation of the bass class. Just as extension violates encapsulaion of implementation, client-side locking violates encpsulation of synchronization policy.

Composition

There is a less fragile alternative for adding a atomic operation to an existing class: composition.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@ThreadSafe
public class ImprovedList<T> implements List<T> {
private final List<T> list;
/**
* PRE: list argument is thread-safe.
*/
public ImprovedList(List<T> list) { this.list = list; }
public synchronized boolean putIfAbsent(T x) {
boolean contains = list.contains(x);
if (contains)
list.add(x);
return !contains;
}
public synchronized void clear() { list.clear(); }
// Plain vanilla delegation for List methods.
// Mutative methods must be synchronized to ensure atomicity of putIfAbsent.
// ...
}

ImprovedList adds an additional level of locking using its own intrinsic lock. It does not care whether the underlying List is thread-safe, because it provides its own consistent locking that provides thread safety even if the List is not thread-safe or changes its locking implementation.

Documenting synchronization policies

Document a class’s thread safety guarantees for its clients; document its synchronization policy for its maintainers.

Crafting a synchronization policy requires a number of decisions:

  • Which variables to make volatile?
  • Which variables to guard with locks?
  • Which locks guard which variables?
  • Which variables to make immutable or confine to a thread?
  • Which operation must be atomic?

(To Be Continued)