Chapter 04 - Design A Thread Safe Class
Monitor pattern
https://java-design-patterns.com/patterns/monitor/
Basically use synchronization
to achieve thread safe
public class ThreadSafeClass {
private Object lock1 = new Object();
private Object lock2 = new Object();
public void methodA() {
synchronized(lock1) {
//critical section A
}
}
public void methodB() {
synchronized(lock1) {
//critical section B
}
}
public void methodC() {
synchronized(lock2) {
//critical section C
}
}
public void methodD() {
synchronized(lock2) {
//critical section D
}
}
}
Cirtical section here means only 1 thread can access at a time
Example: Tracking vehicle
@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);
}
}
@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;
}
}
In here, we're all synchronised on this
. Even though locations is not threadsafe.
Delegation
We can use already threadsafe classes to handle
@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);
}
}
@Immutable
public class Point {
public final int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
Note: Collections.unmodifiableMap()
will return the read-only view of the map. However, if the original map update, this view is also updated with new values. However unmodifiableMap cannot guarantee safety if the value or key is modifiable and the user modify it directly. It simply guard puts
operations
Independent State Variable
We can use delegation for 2 or more variables when the variables are independent of each other. Consider the following scenarios
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);
}
}
The keyListeners
and mouseListeners
are separated from each other. Therefore we dont need
When delegation fail
Delegation fail when there is a compound action. For example:
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());
}
}
For example in this case if someone called setLower(5)
and setUpper(4)
the same time, we might get (5,4)
.
Safe publishing
Consider the previous example of non-threadsafe MutablePoint
@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;
}
}
We can make this threadsafe by
@ThreadSafe
public class SafeMutablePoint {
private int x, y;
private SafeMutablePoint(int[] points) {
this.x = points[0];
this.y = points[1];
}
public SafeMutablePoint(SafeMutablePoint point) { // Copy constructor
this(point.get());
}
public SafeMutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public synchronized int[] get() {
return new int[] {x, y};
}
public synchronized void set(int x, int y) {
this.x = x;
this.y = y;
}
}
Note that in here, for get()
we need to use synchronised
as well, volatile
on x
,y
is not enough. The reason is when reading x
, y
could be change by another set(x, y)
therefore it could cause uncertainty.
To prevent this, we need to make sure that both x
and y
are being read by the same time only.
For the copy constructor here, instead of doing this(this.x, this.y)
, we use get()
so that we return a snapshot of consistent both x
and y
. Doing this.x, this.y
exposes a risk of one value might changes half way, for example y
might get changed after populating this.x
.
Adding functionality to thread-safe class
Easiest way to create a threadsafe class is inherit from already existing thread-safe class. When adding functionality to an existing thread safe class we have a few options:
- Adding another function.
- This option requires you have access to the code. However it's the most preferrable option since the source code is centralised
- Extending from the class
- This option is more limited since it requires that the class is exposed enough functionality for us to extend.
- This also split the source code to multiple places therefore making it hard to store
Example extending the vector class of putIfAbsent
@ThreadSafe
public class ExtendedVector<E> extends Vector<E> {
public synchronized boolean putIfAbsent(E item) {
if (this.contains(item)) {
return false;
}
this.add(item);
return true;
}
}
Client side locking
The above example demonstrate how to extend a class to add more support. However, it's not like we can extend in all scenario.
For example, we want to extend boolean putIfAbsent
method for object that returns for Collections.synchronizedList(..)
. This is not possible since we don't know what the class type inside the ..
since it could be any class.
An idea could be to have a helper method and synchronised on the same synchronised list.
@ThreadSafe
public class ClientSideLocking<E> {
final List<E> list = Collections.synchronizedList(new ArrayList<E>());
// ... other methiods
public boolean putIfAbsent(E item) {
synchronized (list) {
if (list.contains(item)) {
return false;
}
list.add(item);
return true;
}
}
}
It's important to note that in here we must use synchronized (list)
to synchronise on the right object. This technique is call client-side locking.
However this technique is very fagile since it's putting the lock on another object that's not even related to the current object.
Composition
public abstract class CompositionList<T> implements List<T> {
private final List<T> list;
public CompositionList(List<T> list) {
this.list = list;
}
public synchronized boolean putIfAbsent(T item) {
if (this.contains(item)) {
return false;
}
list.add(item);
return true;
}
// Other method
}
This time, we implements the List<T>
and add our own implementation of each method with our synchronisation.
With this implementation, we assume that from now on, the user will use CompositionList
instead of their own list. This add more overhead however is a more preferred way since it's more maintainable.
Documenting Synchronisation policies
It's always recommended to document the synchronisation policies. This can be used with the following annotation:
@ThreadSafe
@NotThreadSafe
@GuardedBy("..")
Interpreting vague documentation
If the code base does not specify if a class is thread safe or not. We can put ourself in the author place and see if this code would be implemented with thread safety.
For example the class HttpSession
would make sense to be used concurrently with other thread. Therefore it's highly possible that the class will be thread safe.