Tutorial: Detect concurrency issues
This tutorial introduces you to debugging multithreaded programs using IntelliJ IDEA.
When writing multithreaded apps, we must be extra careful as we may introduce bugs that will then be very hard to catch and fix. Concurrency-related bugs are trickier than those in a single-threaded application because of their random nature. An app may run flawlessly a thousand times and then fail unexpectedly for no obvious reason.
In this tutorial, we'll analyze a code example that demonstrates the core principles of debugging and analyzing a multithreaded app.
A common example of a concurrency-related bug is a race condition. It happens when some shared data is modified by several threads at the same time. The code may work fine as long as the modifications made by the two threads don't overlap.
Such overlapping may be very rare and lead us into thinking there is no flaw in the code. However, when the thread operations do overlap, the data gets corrupted.
If we don't take this into account, there is no guarantee that the threads will not operate on the data simultaneously, especially if we deal with something more complex than just reading and writing. Luckily, Java has built-in synchronization mechanisms that ensure only one thread works with the data at a time.
Let's consider the following code:
addIfAbsent method checks if a list contains a specific element, and if not, adds it. We call this method twice from different threads. Both times we pass the same integer value (
17), and because of the guard condition
(!a.contains(x)), only the first thread to call that method should be able to add the value. The use of
SynchronizedList is supposed to protect us against race conditions. Finally, the
System.out.println(a) statement prints out the contents of the list.
If we were to use this code for a long time, we would see that at times it still produces unexpected results.
To find the cause, let's examine how the code operates and see if we really managed to prevent race conditions.
Reproduce the bug
Using the IntelliJ IDEA debugger, you can test the multithreaded design of your application and reproduce concurrency-related bugs by controlling individual threads rather than the entire application.
Set a breakpoint at the statement that adds elements to the list.
Configure the breakpoint to only suspend the thread in which it was hit. This will ensure that both threads were suspended at the same line. To do this, right-click the breakpoint, then click Thread.
Start the debug session by clicking the Run button near the
mainmethod and selecting Debug.
When the program has run, both threads are individually suspended in the
addIfAbsentmethod. Now you can switch between the threads (in the Frames or Threads tab) and control the execution of each thread.
At this point, both threads have checked that the list does not contain
17and are ready to add the number to the list.
Resume the thread by pressing F9 or clicking in the left part of the Debug tool window.
After you resume
Thread-0, it proceeds with adding
17to the list and is then terminated. After that, the debugger automatically switches back to the main thread.
Resume the main thread to let it execute the remaining statements and then terminate.
Review the program output in the Console tab.
The output (
[17, 17]) demonstrates that it was possible for the two threads to add the same value bypassing the guard condition and synchronization. We used the debugger to simulate the way it happened, which showed us that a race condition exists, and we need to correct our approach.
Correct the program
As we have just seen,
SynchronizedList did not provide as much protection as we expected. It made sure that only one of the threads modifies the list at a time. However, we should have still taken into account that checking
if (!a.contains(x)) and modifying
a.add(x) were not an atomic operation. For this reason, both threads were able to evaluate the condition before any of them added anything to the list.
Let's correct the code by wrapping the condition in a synchronization block.
We can now repeat the procedure with the corrected code and make sure that the issue no longer reproduces.