Developer.com content and product recommendations are editorially independent. When you click on links to our partners, we may earn revenue. Learn more.
Multithreading is a powerful concept in Java that allows a program to run multiple threads simultaneously. However, this feature places the responsibility on the developer to manage synchronization so that threads do not interfere with each other and produce unexpected results. Thread synchronization errors can be subtle and difficult to detect, making them a common cause of bugs in multithreaded Java applications. This tutorial explains different types of thread synchronization errors and provides suggestions for resolving them.
Go to:
race condition
all Gyeongju A condition occurs when the behavior of a program depends on the relative timing of events, such as the order in which threads are scheduled to run. This can lead to unpredictable results and data corruption. Consider the following example:
public class RaceConditionExample { Â Â Â Â private static int counter = 0; Â Â Â Â public static void main(String[] args) { Â Â Â Â Â Â Â Â Runnable incrementTask = () -> { Â Â Â Â Â Â Â Â Â Â Â Â for (int i = 0; i < 10000; i++) { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â counter++; Â Â Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â }; Â Â Â Â Â Â Â Â Thread thread1 = new Thread(incrementTask); Â Â Â Â Â Â Â Â Thread thread2 = new Thread(incrementTask); Â Â Â Â Â Â Â Â thread1.start(); Â Â Â Â Â Â Â Â thread2.start(); Â Â Â Â Â Â Â Â try { Â Â Â Â Â Â Â Â Â Â Â Â thread1.join(); Â Â Â Â Â Â Â Â Â Â Â Â thread2.join(); Â Â Â Â Â Â Â Â } catch (InterruptedException e) { Â Â Â Â Â Â Â Â Â Â Â Â e.printStackTrace(); Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â System.out.println("Counter: " + counter); Â Â Â Â } }
In this example, two threads increment a shared counter variable. The lack of synchronization causes race conditions and makes the final value of the counter unpredictable. To solve this problem you can use: synced keyword:
public class FixedRaceConditionExample { Â Â Â Â private static int counter = 0; Â Â Â Â public static synchronized void increment() { Â Â Â Â Â Â Â Â for (int i = 0; i < 10000; i++) { Â Â Â Â Â Â Â Â Â Â Â Â counter++; Â Â Â Â Â Â Â Â } Â Â Â Â } Â Â Â Â public static void main(String[] args) { Â Â Â Â Â Â Â Â Thread thread1 = new Thread(FixedRaceConditionExample::increment); Â Â Â Â Â Â Â Â Thread thread2 = new Thread(FixedRaceConditionExample::increment); Â Â Â Â Â Â Â Â thread1.start(); Â Â Â Â Â Â Â Â thread2.start(); Â Â Â Â Â Â Â Â try { Â Â Â Â Â Â Â Â Â Â Â Â thread1.join(); Â Â Â Â Â Â Â Â Â Â Â Â thread2.join(); Â Â Â Â Â Â Â Â } catch (InterruptedException e) { Â Â Â Â Â Â Â Â Â Â Â Â e.printStackTrace(); Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â System.out.println("Counter: " + counter); Â Â Â Â } }
using synced Keywords of increase The method prevents race conditions by allowing only one thread to run at a time.
Detecting race conditions requires careful analysis of your code and understanding the interactions between threads. Always use the following synchronization mechanism: synced A method or block to secure shared resources and prevent race conditions.
deadlock
deadlock This happens when two or more threads are blocked forever, with each thread waiting for the other thread to release the lock. This situation may cause the application to freeze. Let’s look at a classic example of a deadlock.
public class DeadlockExample { Â Â Â Â private static final Object lock1 = new Object(); Â Â Â Â private static final Object lock2 = new Object(); Â Â Â Â public static void main(String[] args) { Â Â Â Â Â Â Â Â Thread thread1 = new Thread(() -> { Â Â Â Â Â Â Â Â Â Â Â Â synchronized (lock1) { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â System.out.println("Thread 1: Holding lock 1"); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â try { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Thread.sleep(100); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â } catch (InterruptedException e) { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â e.printStackTrace(); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â System.out.println("Thread 1: Waiting for lock 2"); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â synchronized (lock2) { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â System.out.println("Thread 1: Holding lock 1 and lock 2"); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â }); Â Â Â Â Â Â Â Â Thread thread2 = new Thread(() -> { Â Â Â Â Â Â Â Â Â Â Â Â synchronized (lock2) { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â System.out.println("Thread 2: Holding lock 2"); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â try { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Thread.sleep(100); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â } catch (InterruptedException e) { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â e.printStackTrace(); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â System.out.println("Thread 2: Waiting for lock 1"); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â synchronized (lock1) { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â System.out.println("Thread 2: Holding lock 2 and lock 1"); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â }); Â Â Â Â Â Â Â Â thread1.start(); Â Â Â Â Â Â Â Â thread2.start(); Â Â Â Â } }
In this example thread 1 possession lock 1 and wait lock 2while thread 2 possession lock 2 and wait lock 1. This causes a deadlock as neither thread can proceed.
To avoid deadlocks, ensure that threads always acquire locks in the same order. If you need multiple locks, use a consistent order to acquire them. A modified version of the previous example to avoid deadlock is:
public class FixedDeadlockExample { Â Â Â Â private static final Object lock1 = new Object(); Â Â Â Â private static final Object lock2 = new Object(); Â Â Â Â public static void main(String[] args) { Â Â Â Â Â Â Â Â Thread thread1 = new Thread(() -> { Â Â Â Â Â Â Â Â Â Â Â Â synchronized (lock1) { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â System.out.println("Thread 1: Holding lock 1"); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â try { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Thread.sleep(100); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â } catch (InterruptedException e) { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â e.printStackTrace(); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â System.out.println("Thread 1: Waiting for lock 2"); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â synchronized (lock2) { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â System.out.println("Thread 1: Holding lock 2"); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â }); Â Â Â Â Â Â Â Â Thread thread2 = new Thread(() -> { Â Â Â Â Â Â Â Â Â Â Â Â synchronized (lock1) { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â System.out.println("Thread 2: Holding lock 1"); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â try { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Thread.sleep(100); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â } catch (InterruptedException e) { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â e.printStackTrace(); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â System.out.println("Thread 2: Waiting for lock 2"); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â synchronized (lock2) { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â System.out.println("Thread 2: Holding lock 2"); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â }); Â Â Â Â Â Â Â Â thread1.start(); Â Â Â Â Â Â Â Â thread2.start(); Â Â Â Â } }
In this modified version, both threads acquire the lock in the same order. lock 1Then lock 2. This eliminates the possibility of deadlock occurring.
Locking strategies must be designed carefully to prevent deadlock. To avoid circular dependencies between threads, always acquire locks in a consistent order. Use tools like thread dumps and profilers to identify and resolve deadlock issues in your Java programs. You can also read our tutorial on how to prevent thread deadlocks in Java for more strategies.
hunger
hunger Occurs when a thread is unable to access a shared resource regularly and is unable to make progress. This can happen when low-priority threads are continuously preempted by higher-priority threads. Consider the following code example:
public class StarvationExample { Â Â Â Â private static final Object lock = new Object(); Â Â Â Â public static void main(String[] args) { Â Â Â Â Â Â Â Â Thread highPriorityThread = new Thread(() -> { Â Â Â Â Â Â Â Â Â Â Â Â while (true) { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â synchronized (lock) { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â System.out.println("High Priority Thread is working"); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â }); Â Â Â Â Â Â Â Â Thread lowPriorityThread = new Thread(() -> { Â Â Â Â Â Â Â Â Â Â Â Â while (true) { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â synchronized (lock) { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â System.out.println("Low Priority Thread is working"); Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â }); Â Â Â Â Â Â Â Â highPriorityThread.setPriority(Thread.MAX_PRIORITY); Â Â Â Â Â Â Â Â lowPriorityThread.setPriority(Thread.MIN_PRIORITY); Â Â Â Â Â Â Â Â highPriorityThread.start(); Â Â Â Â Â Â Â Â lowPriorityThread.start(); Â Â Â Â } }
In this example, both a high-priority thread and a low-priority thread are competing for the lock. High-priority threads dominate, while lower-priority threads experience starvation.
To alleviate starvation, you can use fair locking or adjust thread priorities. next reentrant lock with equity Flag enabled:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class FixedStarvationExample {     // The true boolean value enables fairness     private static final Lock lock = new ReentrantLock(true);     public static void main(String[] args) {         Thread highPriorityThread = new Thread(() -> {             while (true) {                 lock.lock();                 try {                     System.out.println("High Priority Thread is working");                 } finally {                     lock.unlock();                 }             }         });         Thread lowPriorityThread = new Thread(() -> {             while (true) {                 lock.lock();                 try {                     System.out.println("Low Priority Thread is working");                 } finally {                     lock.unlock();                 }             }         });         highPriorityThread.setPriority(Thread.MAX_PRIORITY);         lowPriorityThread.setPriority(Thread.MIN_PRIORITY);         highPriorityThread.start();         lowPriorityThread.start();     } }
that much reentrant lock with equity Reduces the chance of starvation by ensuring that the thread waiting the longest acquires the lock.
Alleviating starvation requires careful consideration of thread priorities, using fair locking, and ensuring that all threads have fair access to shared resources. Regularly review and adjust thread priorities based on the needs of your application.
Check out our tutorial on best threading practices for Java applications.
data inconsistency
data inconsistency Occurs when multiple threads access shared data without proper synchronization, resulting in unexpected and incorrect results. Consider the following example:
public class DataInconsistencyExample { Â Â Â Â private static int sharedValue = 0; Â Â Â Â public static void main(String[] args) { Â Â Â Â Â Â Â Â Runnable incrementTask = () -> { Â Â Â Â Â Â Â Â Â Â Â Â for (int i = 0; i < 1000; i++) { Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â sharedValue++; Â Â Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â }; Â Â Â Â Â Â Â Â Thread thread1 = new Thread(incrementTask); Â Â Â Â Â Â Â Â Thread thread2 = new Thread(incrementTask); Â Â Â Â Â Â Â Â thread1.start(); Â Â Â Â Â Â Â Â thread2.start(); Â Â Â Â Â Â Â Â try { Â Â Â Â Â Â Â Â Â Â Â Â thread1.join(); Â Â Â Â Â Â Â Â Â Â Â Â thread2.join(); Â Â Â Â Â Â Â Â } catch (InterruptedException e) { Â Â Â Â Â Â Â Â Â Â Â Â e.printStackTrace(); Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â System.out.println("Shared Value: " + sharedValue); Â Â Â Â } }
In this example, two threads increment a shared value without synchronization. As a result, the final value of shared value is unpredictable and inconsistent.
To resolve data inconsistency issues, you can use: synced Keywords or other synchronization mechanisms:
public class FixedDataInconsistencyExample { Â Â Â Â private static int sharedValue = 0; Â Â Â Â public static synchronized void increment() { Â Â Â Â Â Â Â Â for (int i = 0; i < 1000; i++) { Â Â Â Â Â Â Â Â Â Â Â Â sharedValue++; Â Â Â Â Â Â Â Â } Â Â Â Â } Â Â Â Â public static void main(String[] args) { Â Â Â Â Â Â Â Â Thread thread1 = new Thread(FixedDataInconsistencyExample::increment); Â Â Â Â Â Â Â Â Thread thread2 = new Thread(FixedDataInconsistencyExample::increment); Â Â Â Â Â Â Â Â thread1.start(); Â Â Â Â Â Â Â Â thread2.start(); Â Â Â Â Â Â Â Â try { Â Â Â Â Â Â Â Â Â Â Â Â thread1.join(); Â Â Â Â Â Â Â Â Â Â Â Â thread2.join(); Â Â Â Â Â Â Â Â } catch (InterruptedException e) { Â Â Â Â Â Â Â Â Â Â Â Â e.printStackTrace(); Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â System.out.println("Shared Value: " + sharedValue); Â Â Â Â } }
using synced Keywords of increase The method prevents data inconsistencies by allowing only one thread to run at a time.
To avoid data inconsistencies, always synchronize access to shared data. use synced Keywords or other synchronization mechanisms to protect critical parts of your code. Regularly review your code for potential data inconsistency issues, especially in multi-threaded environments.
Final thoughts on detecting and fixing thread synchronization errors in Java
In this Java tutorial, we have looked at real-world examples of each type of thread synchronization error and provided solutions to resolve them. Thread synchronization errors, such as race conditions, deadlocks, starvation, and data inconsistencies, can lead to subtle and hard-to-find bugs. However, incorporating the strategies presented here into your Java code can improve the stability and performance of multithreaded applications.
read: Best Online Courses for Java