Design a thread-safe class
The design process for a thread-safe class should include these three basic elements:
- Identify the variables that form the object’s state.
- Identify the invariants that constrain the state variables.
- 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 |
|
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 | public class PrivateLock { |
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 | public class VisualComponent { |
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 |
|
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 |
|
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 |
|
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
.