Explore IntelliJ IDEA Tools to Debug Multithreaded Java apps
Debugging concurrency issues or race conditions in your code is usually difficult. This post should give you a good start to debug multithreaded Java applications. We will learn through an example. Here, I have written a multithreaded program to calculate this math problem:
100! + 100000!
Here is what is happening in the above code: We initialize, name, and start two threads — ‘Thread 1’ (to calculate 100!) and ‘Thread 2’ (to calculate 100000!). Then in the main() method, we call thread1.join() so that the main thread won’t execute further until ‘Thread 1’ returns. Similarly, we call thread2.join() Use of Thread.join() method ensures that sum (on line 25) is not calculated until both the threads return, that is, addition is performed only after we get the factorials of 100 and 100000. Let us explore the IntelliJ IDEA (v. 2019.2.2 (CE)) tools that I often use while debugging a multithreaded app.
Frames and Thread panes
The Debug tool window has Frame pane which consists of a drop down. It focuses on the thread which is currently paused because of the breakpoint and shows the call stack of that thread. In the image below, the breakpoint is in the main() method and the Frame is showing us the call stack for the main thread. If you want to check call stack of other threads, you can select them from the drop down.
Thread pane shows all the threads that are currently active. Referring to the code above, I have added a breakpoint at thread1.join() (on line 18). When the app pauses at that breakpoint, we should see at least three threads — ‘main’, ‘Thread 1’ and ‘Thread 2’ in this pane (check screenshot below). You can double click on each thread to observe their call stacks.
Selective debugging
Assume that I am troubleshooting a bug in this program and I need to pause execution only for ‘Thread 2’ as soon as it starts running. This suggests that I need to add a breakpoint on the first line of FactorialCalculatingThread’s run() method (line 39). But we will run into a problem — all the threads that encounter the breakpoint will be suspended. This includes ‘Thread 1’ and ‘Thread 2’ for our app. I don’t want both the threads to pause. Can you think of any other approach? We can use conditional breakpoint feature. Let us see how. After adding a breakpoint, right-click on it, check ‘Suspend’ and select ‘Thread’. Then we add the condition as shown in the screenshot below. This condition ensures that the debugger would pause the current thread only if that thread’s name is ‘Thread 2’: Now, debug the program. When the app pauses, only ‘Thread 2’ is suspended. You can confirm that ‘Thread 1’ was executed and didn’t get suspended through the following steps: 1.In the console, you can verify through the logs that ‘Thread 1’ ran and exited.
2.In the Threads pane, you can check that there is no ‘Thread 1’
The way to configure conditional breakpoints may be different in different IDE versions. But the key idea is to be aware of these features and use them.
Happy debugging!
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.
Problem
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:
import java.util.ArrayList; import java.util.Collections; import java.util.List; public class ConcurrencyTest < static final List a = Collections.synchronizedList(new ArrayList()); public static void main(String[] args) < Thread t = new Thread(() ->addIfAbsent(17)); t.start(); addIfAbsent(17); t.join(); System.out.println(a); > private static void addIfAbsent(int x) < if (!a.contains(x)) < a.add(x); >> >
The 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 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.
If you are curious and want reproduce the unexpected behavior, you can create a test that would run over and over until the first failure.
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.
If stopping a particular thread breaks the operation of your app, this indicates that there is a design flaw. In a robust design, threads operate correctly irrespective of the events timing.
- 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 .
If you suspend individual threads often, you can click Make Default to make every new breakpoint only suspend the thread where it was hit.
- Start the debug session by clicking the Run button near the main method and selecting Debug .
When the program has run, both threads are individually suspended in the addIfAbsent method. 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 17 and are ready to add the number to the list.
- Switch to Thread-0 .
- 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 17 to 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.