Concurrency in Java provides significant benefits in terms of performance and responsiveness but can lead to complex issues like race conditions and deadlocks. This article explains how to identify and resolve these issues step by step with practical examples.
A race condition occurs when multiple threads access shared resources concurrently, and the program's behavior depends on the timing of their execution. This can lead to unpredictable and incorrect results.
public class RaceConditionExample { private int counter = 0; public void increment() { counter++; } public static void main(String[] args) { RaceConditionExample example = new RaceConditionExample(); Runnable task = () -> { for (int i = 0; i < 1000; i++) { example.increment(); } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Final Counter Value: " + example.counter); } }
Problem: The counter value is incorrect due to concurrent modifications by multiple threads.
To resolve race conditions, use synchronization mechanisms like synchronized
blocks or locks.
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class RaceConditionResolved { private int counter = 0; private final Lock lock = new ReentrantLock(); public void increment() { lock.lock(); try { counter++; } finally { lock.unlock(); } } public static void main(String[] args) { RaceConditionResolved example = new RaceConditionResolved(); Runnable task = () -> { for (int i = 0; i < 1000; i++) { example.increment(); } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Final Counter Value: " + example.counter); } }
A deadlock occurs when two or more threads are waiting for each other to release resources, resulting in a situation where none of the threads can proceed.
public class DeadlockExample { private final Object lock1 = new Object(); private final Object lock2 = new Object(); public void method1() { synchronized (lock1) { System.out.println(Thread.currentThread().getName() + " locked lock1"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { System.out.println(Thread.currentThread().getName() + " locked lock2"); } } } public void method2() { synchronized (lock2) { System.out.println(Thread.currentThread().getName() + " locked lock2"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1) { System.out.println(Thread.currentThread().getName() + " locked lock1"); } } } public static void main(String[] args) { DeadlockExample example = new DeadlockExample(); Thread t1 = new Thread(example::method1); Thread t2 = new Thread(example::method2); t1.start(); t2.start(); } }
Problem: Thread 1 locks lock1
and waits for lock2
, while Thread 2 locks lock2
and waits for lock1
, causing a deadlock.
To avoid deadlocks, follow these techniques:
java.util.concurrent
.import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class DeadlockResolved { private final Lock lock1 = new ReentrantLock(); private final Lock lock2 = new ReentrantLock(); public void method1() { if (lock1.tryLock()) { try { if (lock2.tryLock()) { try { System.out.println(Thread.currentThread().getName() + " acquired both locks"); } finally { lock2.unlock(); } } } finally { lock1.unlock(); } } else { System.out.println(Thread.currentThread().getName() + " could not acquire locks"); } } public static void main(String[] args) { DeadlockResolved example = new DeadlockResolved(); Thread t1 = new Thread(example::method1); Thread t2 = new Thread(example::method1); t1.start(); t2.start(); } }
Race conditions and deadlocks are common concurrency issues that can compromise the correctness and performance of multithreaded applications. Identifying these problems through careful debugging and using synchronization techniques, higher-level concurrency utilities, or lock ordering can help you build robust applications.