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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
MutablePoint is not thread-safe, the tracker class is.
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.
Point is now thread-safe because it is immutable. And we no longer need to copy the locations when returning them.
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:
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.
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.
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
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.
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.
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.
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.
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.
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
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.
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.
There is a less fragile alternative for adding a atomic operation to an existing class: composition.
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.
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
- 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)