Introduction:
Concurrency bugs are extremely difficult to reliably find by testing, due to their dependence on the non-deterministic scheduling of concurrent threads.
As we all know, Java programs are multithreaded and backend application servers like Tomcat, Wildfly, and others spin a new thread (or pick one from a pool) for every user request. Medium to large systems process a huge number of parallel requests spinning multiple threads. If programs are incorrectly written for multithreaded JVM, you will have intermittently occurring defects that elude even the most rigorous testing regimes and sneak into production.
Problems:
It is highly difficult, in some cases even for experts, to write thread-safe programs. For example, most Java applications use java.text.SimpleDateFormat object to format date strings for display purposes. But how many of us know that SimpleDateFormat objects are not thread-safe? Many developers actually think that a cached static instance of SimpleDateFormat object is the best way of sharing resources. So, they declare a static field level instance for such an object, share it across all threads, and feel accomplished after their unit tests pass. But in reality, this code will fail in production when put under even a decent load.
The same problem exists with the Xerces DOM object, which is also not thread-safe. It was this Xerces DOM object that caused the multithreading issues in one of our client’s production environment, who had to live with the defect for over a year until Seagence detected it. Read their story here.
Common symptoms of concurrency issues in Java:
Data corruption
Data corruption issues are frequent and serious. This happens when multiple threads race to change the state of a shared object. When the object state is corrupted, the results include all kinds of weird Exceptions being thrown which seriously impact end-user experience. Some of the exceptions thrown when SimpleDateFormat object is shared include NumberFormatExceptions, ArrayIndexOutOfBoundsExceptions, and NullPointerExceptions, etc. Because user requests fail intermittently with different exceptions at different times debugging becomes difficult. The situation becomes even worse if these exceptions are swallowed, which leaves no trace at all in the log.
Deadlocks
Deadlocks are rare but very serious. This happens due to incorrectly written synchronization blocks or when the lock acquisition is not in proper order. The results include threads keeping busy without actually doing any work.

The above figure shows the wrong order of lock acquisition which leads to deadlock. While Thread 1 is acquiring locks in the order Object 1 and then Object 2 but Thread 2, on the other hand, is acquiring locks in the reverse order i.e. Object 2 and then Object 1. In a multithreaded JVM, this will lead to deadlocks even under a decent load.
The below figure shows the correct order of lock acquisition to avoid deadlocks. See that both threads want to acquire locks in the same order i.e. Object 1 and then Object 2. In this order, Thread 2 will go into wait state at the time of acquiring the lock on Object 1 itself. Because of the enforced order, it will not get the lock on Object 2 also. In that situation, Thread 1 is free to acquire the lock on Object 2, complete its work, release the Object 2 lock, and then continue its work on Object 1 and eventually release the lock on Object 1. With both the locks now available, Thread 2 is now free to complete its work.

So, how do we know if our production software has concurrency or multithreading defects? To find, take a look at the user reported issues or your bug database and look for phrases like
- The application occasionally does not respond
- The application outputs, rarely, an error screen
- The system sometimes behaves in an erratic manner
- I could not reproduce it, but it just now glitched out
If you find one or more cases with similar phrases to the above examples, then your application likely has concurrency or related multithreading bugs.
Also Read: Difference between Error Monitoring and Defect Monitoring
Options and approaches to avoid Concurrency Issues
1. Synchronized Blocks and Methods
Use synchronized blocks or methods to provide mutual exclusion and ensure only one thread can execute the synchronized code at a time. However, be mindful of potential deadlocks and performance implications.
2. Java Concurrency API (java.util.concurrent)
The java.util.concurrent package provides a wealth of classes and interfaces to handle concurrency effectively. Some key components include:
ThreadPoolExecutor: Efficiently manages a pool of worker threads for executing tasks.
Concurrent Collections: Thread-safe versions of standard Java collections.
CountDownLatch, CyclicBarrier, Phaser: Synchronization aids to control the flow of execution among threads.
3. Executors Framework
Utilize the Executors factory class to create thread pools, which abstract the complexity of managing threads. It provides different types of thread pools, such as fixed thread pool, cached thread pool, and scheduled thread pool.
ExecutorService executor = Executors.newFixedThreadPool(10);
4. Fork-Join Framework
Introduced in Java 7, this framework is useful for parallel processing and recursive decomposition of a task into subtasks. It’s highly efficient for tasks that can be divided into smaller subtasks.
ForkJoinPool forkJoinPool = new ForkJoinPool();
forkJoinPool.invoke(new RecursiveTask());
5. CompletableFuture
Introduced in Java 8, CompletableFuture represents a future result of an asynchronous computation. It’s useful for managing asynchronous operations and combining them in a non-blocking way.
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
.thenApplyAsync(result -> result + " World");
6. Reactive Programming (e.g., RxJava, Reactor)
Reactive frameworks like RxJava and Reactor are gaining popularity for handling concurrency by providing an event-driven and reactive approach. They handle asynchronous and event-based programming effectively.
7. Locks (e.g., ReentrantLock)
The ReentrantLock class provides a more flexible alternative to synchronized blocks, allowing finer-grained control over locking and unlocking.
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// Critical section
} finally {
lock.unlock();
}
8. Atomic Variables
The java.util.concurrent.atomic package provides atomic variables that allow for atomic operations, avoiding race conditions in multithreaded scenarios.
AtomicInteger atomicInt = new AtomicInteger(0);
atomicInt.incrementAndGet();
9. ThreadLocal
ThreadLocal provides thread-local variables, allowing each thread to have its own independent copy. It’s useful when you need to maintain per-thread state.
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
By combining these approaches and selecting the most appropriate ones for your specific use case, you can effectively handle concurrency issues in Java, leading to efficient, scalable, and reliable concurrent applications.
Seagence – Your Fix for Concurrency Issues in Java
There are actually not many tools available to detect and root cause multithreading issues. The reason is simple. The nature of the issues and their symptoms are intermittent and sporadic due to the underlying system’s non-deterministic scheduling of concurrent threads. Debugging multithreading issues in the development environment itself is a very difficult job and it is even more difficult and painful to detect and debug such issues in production.
Seagence is a Realtime Defect Detection and Resolution Tool that proactively detects the hard to find and fix defects missed by other approaches including concurrency and multithreading defects. The good news is, Seagence proactively detects these defects, even before end-users report them, and provides the root cause eliminating the need for manual debugging. So there is no need to reproduce the defect. Also, it seamlessly plugs into any production Java application using a tiny Java agent and starts monitoring. The Seagence agent is compatible with the APM and Observability agents used by the major APM and Observability vendors like Datadog and New Relic.
Seagence brings a new approach to production monitoring. Using its unique ExecutionPath Technology and machine learning, Seagence detects every defect as they occur and sends you an alert. How does Seagence do this? Seagence’s ExecutionPath Technology differentiates successfully executed requests from failed requests and machine learning helps separate them into different groups or clusters.
What separates Seagence from other tools is that it not only detects defects due to HTTP 500 internal server errors but also detects defects due to any type of exception whether it is a swallowed exception or a caught exception. Seagence also detects defects that disguise in a proper HTTP 200 success response code. With Seagence provided defect and root cause in hand, you fix your broken code without needing any debugging.
Also Read: Exceptions in Java: Finding and Fixing
Conclusion:
No software is bug-free. Any amount of testing you do defects still sneak into production and create trouble to end users. Your best bet to improve your end user experience is by using a proper production monitoring tool that proactively detects all defects on its own with root cause. Seagence is the only such tool built to find and help you fix difficult issues. It detects known and unknown defects due to concurrency and multi-threading issues, 500 errors, and others in production. Seagence also provides all the context you need to help you debug production application when debugging becomes necessary. Sounds interesting? You can start a free trial here.