0%

Book Reading: Java Concurrency in Practice, Chapter 4

Design a thread-safe class

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

  1. Identify the variables that form the object’s state.
  2. Identify the invariants that constrain the state variables.
  3. Establish a policy for managing concurrent access to the object’s state.

An object’s state starts with its fields, no matter they are primitive types or references to other objects. Objects and variables have a state space: the range of possible states they can take on. That is why we want to use final whenever possible since it can make the space state smaller and easier to reason about.

Operations may have postconditions that identify certain state transitions as invalid. When the next state is derived from the current state, the operation is necessarily a compound action. As the result, an object’s state could be a subset of the fields in the object graph rooted at that object.

Constraints placed on states or state transition by invariants and postconditions create additional synchronization or encapsulation requirements. A class can also have invariants that constrain multiple state variables, which create atomicity requirements: related variables must be fetched or updated in a single atomic operation.

Class invariants and method postconditons constrain the valid states and state transitions for an object, it is impossible to ensure thread safety without understanding an object’s invariants and postconditions.

In addition to that, some objects also have method with state-based preconditions, operations with state-based preconditions are called state-denpendent. In a concurrent program, it adds possibility of waiting until the precondition becomes true, and then proceeding with the operation.

Ownership is another thing needs to be considered when designing thread-safe class, since ownership implies control. Usually the object encapsulates the state it owns and owns the state it encapsulates, but once a mutable object gets published, you no longer have exclusive control, which will result in a “shared ownership”, a typical example is collection classes.

Instance confinement

If an object is not thread-safe, several techniques can still let it be used safely in a multithreaded program.

Encapsulating data within an object is one of the methods, which confines access to the data to the object’s method, making it easier to ensure that the data is always accessed with the appropriate lock held.

Example (Note class Person is not thread safe):

1
2
3
4
5
6
7
8
9
10
11
12
13
@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);
}
}

It is still possible to violate confinement by pushlishing a supposedly confined object. Confined Objects can also escape by publishing other objects such as iterators or inner class instances that may indirectly publish the confined objects.

Another technique is to use java monitor pattern. An object following the java monitor pattern encapsulates all its mutable state and guards it with the object’s own instrinsic lock.

Example (Note any lock can be used as long as it is consistent):

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 widget
}
}
}

There are advantages to using a private lock object instead of an object’s intrinsic lock. Making the lock object private encapsulates the lock so that clent code cannot acquire it.

Delegating thread safety

Sometimes if you are using a thread-safe object, it is possible to make you class thread-safe without extra synchronization actions. We can also delegate thread safety to more than one underlying state variables as long as those underlying state variables are independent.

For example, the code below delegates thread safety to underlying CopyOnWriteArrayList, which is thread safe:

1
2
3
4
5
6
7
8
public class VisualComponent {
private final List<KeyListener> keyListeners = new CopyOnWriteArrayList<KeyListner>();
private final List<MouseListner> mouseListners = new CopyOnWriteArrayList<MouseListener>();

// Add Listeners...

// Remove Listeners...
}

Note there is not relationship between the set of mouse listeners and key listeners, they are independent and that is why VisualComponent can delegate its thread safety obligations to two underlying thread-safe lists.

Most composite classes may have invariants that relate their component state variables. If a class has compound actions, delegation alone is again not a suitable approach for thread safety, and class must provide its own locking to ensure that compound actions are atomic.

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 operation, then it can safely be published.

Building with existing thread-safe classes

The java class library contains many useful “building block” classes. Reusing existing classes is often preferable to creating new ones.

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 access to the source code or may not be free to modify it.

Another approach is to extend the class, assuming it was designed for extension. But 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. If the underlying class were to change its synchronization policy by choosing a different lock to guard its state variables, the subclass would subtly and silently break:

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;
}
}

A third strategy is to extend the functionality of the class without extending the class itself by placing extension code in a “helper” class. To make this approach work, we have to use the same lock that the List uses by using client-side locking.

1
2
3
4
5
6
7
8
9
10
11
12
13
@ThreadSafe
public class ListHelper<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;
}
}
}

Everytime we use this approach, we must make sure we know what lock the library class uses, otherwise we may put locking code into classes that are totally unrelated.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@ThreadSafe
public class ImprovedList<T> implements List<T> {
private final List<T> list;

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(); }

// ...similar delegate methods
}

In effect, we used the java monitor pattern to encapsulate an existing List, and this is guaranteed to provide thread safety so long as our class holds the only reference to the underlying List.