In this chapter, you'll learn about popular methodologies for how to keep threads from interfering with each other's work, namely synchronized scopes, synchronized variables, and guarded blocks. While not as modern as the methodology you'll see in the next chapter, they're still important to recognize and understand, as you'll encounter them in the workplace.
Working on One Thread at a Time With Synchronized Scopes
As you may recall, atomic types provide methods that guarantee that the precise operations they carry out are thread-safe. These methods are typically used to inspect a value and replace it. However, they do not allow you to create and define your own atomic sequence of operations.
Java also has another mechanism that you can use to guarantee that a specific set of statements in a particular instance of a class can only ever get executed by one thread at a time. To achieve this, you can use the synchronized keyword as a gate, ensuring that statements that can not be safely executed concurrently are always executed in serial.
There are two ways you can use this to guard a block of code:
Use of Synchronized | Description | Example |
Method modifier | Placing synchronized in front of your method declaration to only allow one execution per instance of that class. |
|
As a function with an argument | Placing synchronized in front of a block of code to allow one execution across all threads using the same argument reference. |
|
Let's check out how each of these options work.
Option 1: Synchronized as a Method Modifier
In the first case, using synchronized it as a modifier on a method is a little like having a meeting where only one person may talk at a time. I used to do this in meetings where we had a habit of talking over one another. We used a popular solution, passing a shell around. Only the person holding the shell can speak. 🐚
Adding synchronized to your method is the same. It results in only one thread at a time being able to execute that method on a specific instance of its class. You call the instance a monitor for your lock. All code entering an area synchronized on the same monitor suspends. For instance, any other threads using the same instance of a class containing your synchronized method. In other words, any methods marked synchronized also wait on each other, so that only one can be executed at any point in time.
Having many synchronized methods is like a team with many meetings and only one shell. Regardless of the number of meetings in progress, only one person may speak at a time.
Option 2: Synchronized as a function
In the second case, you can decide to use something other than a talking shell. When you add synchronized on a method, the critical section is restricted to all threads executing that method. If instead, you use synchronized(someObject)
as a function, you can get it to restrict access to a block of code following this declaration. Using synchronized as a function can look like the following code snippet:
public void myUnsynchronizedMethod {
// some unsychronized code
callToCode();
synchronized(this) {
// critical section
}
// some more unsynchronized code
callToCode();
}
Lines 1 to 3: Method declaration and unsynchronized code.
Lines 4 to 6: This block of code is synchronized across all threads using the same value of this. That is, they are calling the method
myUnsychronizedMethod
against the same instance of this class.Line 8: You execute some more unsynchronized code.
The value you pass the function is the equivalent of the talking shell; it's the monitor used for synchronization. Unlike method synchronization
, where the monitor is implicitly the current instance of a class, you can define any instance here. Use this monitor to restrict access to your block of code.
In most concurrent code, you often pass in a monitor of this, which is the reference to the current object instance. Doing this allows you the same level of control as using synchronized
as a method modifier but across a very specific set of statements.
For instance, check out the following execution:
In this method, you can see that two threads execute to a point where you see the synchronized keyword.
But can't I just refactor my code and use synchronized
with smaller methods?
Please do! However, this might prevent you from being able to improve the concurrency of your code. Synchronized
as a method modifier stops all threads using any other synchronized method on the same instance.
You might decide to change your implicit lock from this to a few different objects that you can use as a talking shell. Using the synchronized()
function with a specific object makes it easier to effect such a change later. You might want to do this if your performance requirements direct you to optimize your code to avoid blocking every synchronization on that instance of a particular class.
Using Synchronized Variables
As you saw with atomic variables, you often want to protect a shared mutable variable from being corrupted by multiple threads reading and updating inconsistently. You have two tools at your hands to aid you in doing this:
The
volatile
keyword which ensures that any read of a variable will always reflect the latest update by any writing thread.The use of synchronized blocks to ensure that critical sections relating to the reading and writing a variable are thread-safe.
As you've already seen, you can change the implicit lock used for synchronization by calling the function, synchronized(monitorObject). This change ensures that multiple threads synchronizing on the same value argument have to take turns at executing the following code.
In this case, select some instantiated object as your monitor or talking shell, making sure that everything is sharing the same blocks while it is synchronized.
Can I use this to synchronize any variable?
Java does not allow you to synchronize changes to a variable; however, you can synchronize changes using a specific object reference or class instance. That object or reference is used for coordination.
What this means is that if you want to synchronize on a particular field of a class, you can encapsulate it in a class. Let's do this for our subtotal example!
Try It Out for Yourself!
Fire up JShell and let's implement a thread-safe subtotal. Paste each chunk into the interpreter.
Step 1: First, create a class to store your subtotal. Wrap the subtotal in a new class so that you can synchronize on the same Java instance without reassigning it.
public class SubTotal {
public volatile Integer value = 0;
}
This is a plain old Java class (POJO) with one exception. The value field is marked with the volatile
field modifier. This causes Java to ensure that if two threads attempt to modify and read from it at the same time, they reflect the new modified value. This reduces the risk of reading stale data.
Step 2: Next, create a class that creates an instance of SubTotal on which you can synchronize.
public class Adder extends Thread {
static volatile SubTotal subtotal = new SubTotal();
}
This creates a class with a static SubTotal instance that you also make sure is consistent, using volatile.
Step 3: Create a run method which adds a value of 20 for each running thread.
public class Adder extends Thread {
public static volatile SubTotal subtotal = new SubTotal();
public void run() {
synchronized(subtotal) {
// Don't do this in production code
// This should be encapsulated.
subtotal.value = subtotal.value+20;
}
}
}
JShell will inform you that you have "replaced class Adder." Now, let's break down this code:
Line 1 to 2: As before, you have a local instance of SubTotal().
Line 3: Override the run method.
Line 4: Synchronize on the static instance of SubTotal defined at Line 2.
Line 7: Increment your subtotal by 20. We're updating a public variable directly, for the sake of illustration.
Step 4: Let's create some threads now!
jshell> Adder thread1 = new Adder();
thread1 ==> Thread[Thread-0,5,main]
jshell> Adder thread2 = new Adder();
thread2 ==> Thread[Thread-1,5,main]
jshell> Adder thread3 = new Adder();
thread3 ==> Thread[Thread-2,5,main]
As you can see there are three instances of Adder from calling its constructor.
Step 5: Let's run those threads!
jshell> thread1.start(); thread2.start(); thread3.start();
jshell> Adder.subtotal.value
$9 ==> 60
OK, let's break this down:
Line 1: Run all three threads by calling their
start()
methods.Line 3: Inspect the updated value of the subtotal. As you can see, this is set to 60, representing the three additions of 20.
How do I know that this is safer?
Since we're avoiding hard-to-reproduce issues, you'll have to trust the specification of Java to an extent. I would recommend that you try and recreate the issue by writing the same code without synchronization. Since hard-to-predict ordering issues cause the issues of concurrency in the JVM, you can try using more threads and running the code multiple times. Time-box yourself to about an hour, though - that should give you enough information! 😉
Avoiding Jamming Your Program With Deadlocks!
When synchronizing your code, be careful that threads do not become stuck waiting on one another. Consider this class:
public class Deadlocker {
enum Reachability {
REACHED,
UNREACHED
}
private static Reachability unReachablePoint = Reachability.UNREACHED;
public void runOnThread1() {
// Blocks Line 23
synchronized(Boolean.class) {
// Blocked by Line 21
synchronized(Deadlocker.class) {
unReachablePoint = Reachability.REACHED;
}
}
}
public void runOnThread2() {
// Blocks Line 13
synchronized(Deadlocker.class) {
// Blocked by Line 11
synchronized(Boolean.class) {
unReachablePoint = Reachability.REACHED;
}
}
}
}
The static variable unReachablePoint
is set to UNREACHED
, and there are two methods that could potentially set it to REACHED
. It's possible that neither will modify it if both methods runOnThread1()
and runOnThread2()
are executed at the same time.
Look through the code above again and see if you can spot why. I'll wait. 😇
Did you have a look? Let's consider this together and understand the risk:
Line 11 Thread 1- Imagine
runOnThread1()
is called by Thread 1 and this statement is executed first. Since no other thread has synchronized onBoolean.class
, the thread gets past this point.Line 21 Thread 2 - Imagine that
runOnThread2()
is executed next by Thread 2 and reaches Line 21. Since no threads have synchronized onDeadlocker.class
, Thread 2 now has exclusive access to this lock.Line 13 Thread 1 - When Thread 1 reaches this line, it needs to wait for
Deadlocker.class
to be released by Thread 2. This only happens if Thread 2 finishes running Line 24. At this point, Thread 1 also releases its lock onBoolean.class
.Line 23 Thread 2 - When Thread 2 reaches this statement, it won't be able to continue to Line 24 as it needs to wait for Thread 1 to release its lock on Boolean.class. As you've seen, this won't happen until Thread 2 releases its lock on
Deadlocker.class
.
As you can tell, both threads wait on one another indefinitely and jam the program. We'll never continue to the critical sections of the code. Since both threads are waiting on one another, this is called deadlock. They are dead waiting on one another's locks, as is our program.
How do I avoid deadlock?
To avoid deadlock, you should always make sure that multiple threads acquire the same synchronization locks in the same order. If both threads synchronized on Boolean.class and then Deadlocker.class (or the other way around), we would have avoided the risk of deadlock.
Will this code always deadlock?
The bad thing about deadlock is that it might creep up on you out of the blue if you've not designed to defend against it. If either Thread 1 or Thread 2 were able to acquire both their synchronization before the other thread, we'd avoid deadlock. As usual, it's down to the scheduler and timing.
As you'll see, there are many other ways to coordinate execution between your threads. In all cases, watch out for deadlocks.
Guarding Access to the Point of no Return With Guarded Blocks
One of the most primitive forms of synchronizing between threads is known as the guarded block. This form is a pattern or common style of implementation used to coordinate between multiple threads with synchronized methods. It helps the threads coordinate when two methods must take turns running. For instance, one might be waiting for a resource output by the other, such as values needing to be summed before averaging them.
The pattern depends on two types of methods: wait and notify, which all Java classes inherit from the base object class. Their basic behavior is summarized as:
Method | Behavior | Example |
Block the current thread in a WAIT state until Object.notify() is called on the same instance. |
| |
Unblock at most one thread which is in a WAIT state resulting from calling Object.wait(). |
| |
Unblock all the threads which are in a WAIT state, resulting from calling Object.wait(). |
|
Here's an example program that prints a word when one is available. If it prints before the word is set, you'll see a NullPointerException. The pattern for implementing a guarded block is generally structured as follows:
public class WordPrinter {
// Create monitor
volatile Object lock = new Object();
// Create signal for resource sharing
volatile Boolean readyToPrint = false;
// Resources used for computation
String wordToPrint;
// Waits for a wordToPrint to be set
public void printWord() throw InterruptedException {
synchronized(lock) {
while(!readyToPrint) {
lock.wait();
}
System.out.println("The word is " + wordToPrint);
readyToPrint=false;
}
}
// Sets a wordToPrint
public void setWordToPrint(String wordToPrint) {
synchronized(lock) {
this.wordToPrint = wordToPrint;
// we are ready
readyToPrint=true;
lock.notify();
}
}
}
Let's break this down!
Line 4: Create a
volatile
object instance used as a monitor for synchronization between multiple threads. Eg.,Object lock
.Line 6: A
volatile
variable used for communication is shared between multiple threads. Eg.,Boolean readyToPrint=false;
Line 13: In the printWord method, synchronize on the lock and allow a single thread through.
Threads synchronize on the lock object, using it as a monitor. Eg.,
synchronize(lock)
.Line 14: Go into a while loop waiting for
readyToPrint
to be true. This is a check to ensure that you're ready to print a result.Line 15: If you are not ready to print, wait for this condition to be true by calling
Object.wait()
. If we had not called this method, we'd still wait for the condition, but burn CPU cycles by keeping the thread active and continuously checking the condition. Instead, the thread goes into a WAIT state until the condition can be satisfied.
Line 17: You can print out the result when this line is reached. You only reach this line when
Object.wait()
has received a notification.Line 23-29: The
setWordToPrint
method receives a word. It synchronizes on the same object before setting the word, signaling thatreadyToPrint=true
and unsuspending another thread by callingObject.notify()
.
Try It Out for Yourself!
Paste the above code into JShell so you can try it out. Then do the following to use it concurrently:
Step 1: Create an instance of WordPrinter which you'll share:
jshell> WordPrinter wordPrinter = new WordPrinter();
wordPrinter ==> WordPrinter@63c12fb0
Step 2: Create a thread which calls a lambda ()->wordPrinter.printWord()
and start it.
jshell> Thread printerThread = new Thread(()->printer.printWord())
printerThread ==> Thread[Thread-7,5,main]
jshell> printerThread.start()
Notice how it's not doing anything. That's because it's waiting!
Step 3: Create a thread which calls a lambda ()->wordPrinter.setWordToPrint("Pluto").
jshell> Thread wordSetterThread = new Thread(()->printer.setWordToPrint("Pluto!"))
wordSetterThread ==> Thread[Thread-8,5,main]
jshell> wordSetterThread.start()
The word is Pluto!
Can you see how starting the thread at line which called setWordToPrint("Pluto!")
kicked off the notify()
required to print the word from the first thread?
Let's Recap!
A critical section consists of a set of Java statements that much be executed by only one thread at a time. This is usually a set of sequential statements that risks corrupting your data or program logic if run concurrently.
To mark a whole method as a critical section, we place the keyword
synchronized
before the method.Synchronizing a method results in only one thread able to run that method at a time, per instance of its class.
Calling
synchronized(<Some Reference>;)
as a function with an argument before a block of code containing your critical section allows you to use any object reference as a monitor for synchronizing threads. Of all threads sharing that object, only one executes any block of code synchronized with the same object.Java's object class provides a
wait()
andnotify()
methods. Threads can use these on any object to, respectively, wait for a signal, and also notify other threads that they may proceed. These are low-level tools that give you a lot of control and increase your risk of getting things wrong. Avoid them if you can.
In the next chapter, you'll learn about more ReentrantLocks and the Lock API, which are a more modern style of keeping threads from interfering with each other.