Crafting Thread-Safe Functions in Java - Part 2
In this blog post, we will revisit a simple class with three methods for printing values to the console and explore how to make it thread-safe.
package com.example;
public class OrderedPrinting {
public void printFirst() {
System.out.println("First");
}
public void printSecond() {
System.out.println("Second");
}
public void printThird() {
System.out.println("Third");
}
}
You can guess what will be printed on the console:
First
Second
Third
The result becomes unpredictable when allocating each function to a different thread:
package com.example;
public class Main {
public static void main(String[] args) throws InterruptedException {
OrderedPrinting orderedPrinting = new OrderedPrinting();
Thread t1 = new Thread(orderedPrinting::printFirst);
Thread t2 = new Thread(orderedPrinting::printSecond);
Thread t3 = new Thread(orderedPrinting::printThird);
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}
}
First First Second
Third Second Third
Second Third First
Let’s make these functions thread-safe in the following way:
package com.example;
import lombok.SneakyThrows;
public class OrderedPrinting {
private int counter = 1;
public synchronized void printFirst() {
// when the thread enters this function, it acquires the lock on the instance of this class
System.out.println("First");
counter++;
this.notifyAll();
}
@SneakyThrows
public synchronized void printSecond() {
//condition variable the thread checks for
while (counter != 2) {
this.wait();
//every java object exposes wait() and signal methods()
//when wait is called(), the thread releases the acquired lock and joins waiting queue
// now that the lock is released, other threads in the waiting queue has a chance to acquire the lock
}
System.out.println("Second");
counter++;
// the thread now finished with its computation and ready to release the lock
// calling notifyAll sends a signal to threads in the waiting queue that the mutex will be available soon
this.notifyAll();
}
@SneakyThrows
public synchronized void printThird() {
while (counter != 3) {
this.wait();
}
System.out.println("Third");
}
}
Synchronising the methods this way, we can be sure that no matter how many times we run the main method, the output will be the same: First, Second and Third.
An interesting thing to note is how we check for the condition variable in the methods. One might ask why we check the predicate in a while () loop instead of an if condition - and that is answered by the type of monitor being used by Java
Hoare vs Mesa monitors
Java implements Mesa monitors. In this type of monitor, when Thread A signals to Thread B that it releases its lock for Thread B to acquire it - Thread B is in competition with the rest of the Threads in the waiting queue, so it can easily happen that by the time Thread A releases the lock and Thread B wakes up to acquire it, a third thread rushes in and changes the predicate back to false. For this reason, a while() loop is the required way to check for the condition variable in Java.
In contrast to Mesa monitors, in a Hoare monitor Thread A will yield the lock to Thread B and wait out until Thread B acquires the lock - in this instance, checking the condition variable in an if statement is acceptable.
In conclusion, thread safety in Java is vital for consistent program execution in concurrent environments. By synchronizing methods and understanding monitor types, developers can prevent race conditions and optimize performance.
Subscribe to my newsletter
Read articles from Her Code Review directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by