A program that maintain its correctness in a multi-threaded environment is said to be a Thread-Safe program.
Fundamentally, thread safety revolves around proper management of the shared state. In the absence of such proper management, the program could potentially spiral into an invalid state when executed in a multi-threaded environment.
An application’s state is formed by the collective states of the individual objects that constitute it. An object’s state encompasses the data stored in its static or instance fields, which can influence its externally visible behavior. Furthermore, an object’s state may encompass fields from other dependent objects, creating a complex web of interconnected states within the application.
Whether an object’s state need to be managed for concurrent access depends on its usage in your program. If an object is only ever accessed from a single thread, state management might not be needed!
If multiple threads access the same mutable state of an object without proper synchronization, your application is technically broken. There are three ways to fix it —
Don’t share the object across threads
Make the object’s state immutable
Implement proper synchronization for accessing the object’s shared state
Using good Object Oriented Principles can help us in designing thread-safe classes. Thoughtful implementation of encapsulation, immutability, and precise specification of invariants can significantly reduce the effort required to create thread-safe classes, ultimately streamlining the development process.
An important thing to note is that a program consisting of only thread-safe classes may not be thread-safe. Additionally, a program may be thread-safe even when some of its classes are not thread-safe. It all depends on how your objects interact with each other in your program!
A thread-safe class behaves correctly even when accessed from multiple threads, regardless of the scheduling or interleaving of the execution. The calling code needs no synchronization on its part when working with a thread-safe class.
Thread-safe classes encapsulate the necessary synchronisation, relieving the client from the burden of providing their own.
The need for synchronization arises when multiple threads access the same mutable state. However, stateless objects, by virtue of having no state to share, inherently possess thread safety. As a result, stateless objects are always thread-safe.
Consider the following Java class that is used to find a certain number of primes,
public class PrimeFinder {
// Variable to count the number of invocations
private int hitCount = 0;
public int getHitCount() {
return hitCount;
}
public List<Integer> findPrimes(int count) {
hitCount++;
// Find 'count' number of primes
final List<Integer> primes = new ArrayList<>();
for(int i = 2; i < Integer.MAX_VALUE; i++) {
if(isPrime(i)) {
primes.add(i);
if(primes.size() == count) {
break;
}
}
}
return primes;
}
private boolean isPrime(int num) {
for(int i = 2; i < num; i++) {
if(num % i == 0) {
return false;
}
}
return true;
}
}
Note that the instance variable hitCount
constitutes the state of any PrimeFinder
object.
Unfortunately, PrimeFinder
is not thread-safe, even though it would work fine in a single-threaded environment. This class is vulnerable to lost updates, as the operation to increase the hitCount
, although seemingly atomic, is actually broken down into three distinct steps. Here is an example of an unlucky sequence of events causing hitCount
to enter an invalid state.
Unlucky interleaving of execution causes hitCount to be 10 when it should be 11
Computation of incorrect results in case of some unlucky timings in a concurrent program is called a race condition. The particular type of race condition that plagues our PrimeFinder
is called check-then-act or read-modify-write. To eliminate this race condition, we must increment the hitCount in a single atomic operation.
There are several ways to fix the PrimeFinder
class. One such way is to use a thread-safe class inside PrimeFinder
. Here is the updated code,
public class PrimeFinder {
private final AtomicInteger hitCount = new AtomicInteger(0);
public int getHitCount() {
return hitCount.get();
}
public List<Integer> findPrimes(int count) {
hitCount.getAndIncrement();
// Find 'count' number of primes
final List<Integer> primes = new ArrayList<>();
for(int i = 2; i < Integer.MAX_VALUE; i++) {
if(isPrime(i)) {
primes.add(i);
if(primes.size() == count) {
break;
}
}
}
return primes;
}
private boolean isPrime(int num) {
for(int i = 2; i < num; i++) {
if(num % i == 0) {
return false;
}
}
return true;
}
}
Using the atomic variable, we can ensure atomic operations on numbers and object references. Since the state of PrimeFinder
consists only of hitCount
, which is thread-safe now, the class PrimeFinder
is thread-safe now!
When a single element of state is added to a stateless class, the resulting class will be thread-safe if the state is entirely managed by a thread-safe object.
Is it possible to add more state variables to PrimeFinder in a similar manner and still maintain its thread safety? Let us see…
Consider the following (updated) PrimeFinder
class,
public class PrimeFinder {
private final AtomicInteger numCache = new AtomicInteger(1);
private final AtomicReference<List<Integer>> primeFactorCache = new AtomicReference<>(new ArrayList<>());
// ...
public List<Integer> findPrimeFactors(int num) {
if(numCache.get() == num) {
return new ArrayList<>(primeFactorCache.get());
}
// Find prime factors of 'num'
final List<Integer> primeFactors = new ArrayList<>();
for(int i = 2; i < num; i++) {
if(isPrime(i) && num % i == 0) {
primeFactors.add(i);
}
}
numCache.set(num);
primeFactorCache.set(new ArrayList<>(primeFactors));
return primeFactors;
}
// ...
}
The findPrimeFactors
method is supposed to return prime factors of a number. It also stores a cache of the last number factorized and its factors. We have used atomic classes to store both the numCache
and the primeFactorCache
. Still, our class is not thread-safe!
Can you spot the race condition here? Hint: There is a check-then-act race condition here!
One invariant of our PrimeFinder
class is that the list primeFactorCache
will hold all prime factors of numCache
. Since two different variables participate in a single variant, these variables are not independent! In such cases, all the dependent variables must be updated in the same atomic operation!
To maintain state consistency, ensure that dependent state variables are updated in a single atomic operation.
Every Java object can implicitly act as a lock for the purpose of synchronization. These built-in locks are what we call intrinsic locks or monitor locks.
There is a built-in mechanism in Java to provide synchronization called a synchronized block. A synchronized block is a set of statements guarded by an intrinsic lock (object reference). Here is its syntax —
synchronized (lock) {
// Statements working with shared mutable state
}
The lock is automatically acquired by an executing thread when it enters a synchronized block and is released when the block is exited. Since these intrinsic locks can be held by at most one executing thread at once, they allow exclusive access to the guarded code.
A synchronized method is a synchronized block with the entire method as body and this
object as its lock.
Let us fix our PrimeFinder
class using intrinsic locks.
public class PrimeFinder {
private final AtomicInteger numCache = new AtomicInteger(1);
private final AtomicReference<List<Integer>> primeFactorCache = new AtomicReference<>(new ArrayList<>());
// ...
public List<Integer> findPrimeFactors(int num) {
synchronized (this) {
if (numCache.get() == num) {
return new ArrayList<>(primeFactorCache.get());
}
}
// Find prime factors of 'num'
final List<Integer> primeFactors = new ArrayList<>();
for(int i = 2; i < num; i++) {
if(isPrime(i) && num % i == 0) {
primeFactors.add(i);
}
}
synchronized (this) {
numCache.set(num);
primeFactorCache.set(new ArrayList<>(primeFactors));
}
return primeFactors;
}
// ...
}
With this, our PrimeFinder
class is thread-safe again!
With this, we reach the end of this blog. We discussed the fundamentals of thread safety in terms of program state and explored some basic synchronization techniques in Java.
If you enjoy reading this blog, consider throwing in a like and subscribe to not miss future articles on Java concurrency!
Also published here.