A race condition is a situation where the behavior of a program depends on the relative timing of events in different threads. When multiple threads access and modify shared resources simultaneously, the final result may vary depending on the order in which the threads execute. This can lead to inconsistent data, incorrect calculations, and other hard - to - reproduce bugs.
class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class RaceConditionExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// Create two threads
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final count: " + counter.getCount());
}
}
In this example, the increment
method is not thread - safe. The count++
operation is actually composed of three steps: read the current value of count
, increment it, and then write the new value back. If two threads access this method simultaneously, they may read the same value of count
, increment it, and then write back the same incremented value, resulting in a lost update.
Synchronized blocks and methods are a simple way to ensure that only one thread can access a shared resource at a time.
class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
public class SynchronizedExample {
public static void main(String[] args) throws InterruptedException {
SynchronizedCounter counter = new SynchronizedCounter();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final count: " + counter.getCount());
}
}
ReentrantLock
is a more flexible alternative to the synchronized
keyword. It allows for more advanced locking mechanisms such as lock interruption and timed waits.
import java.util.concurrent.locks.ReentrantLock;
class LockCounter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
public class ReentrantLockExample {
public static void main(String[] args) throws InterruptedException {
LockCounter counter = new LockCounter();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final count: " + counter.getCount());
}
}
Java provides atomic variables in the java.util.concurrent.atomic
package. These variables use low - level hardware support to perform atomic operations.
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
public class AtomicVariableExample {
public static void main(String[] args) throws InterruptedException {
AtomicCounter counter = new AtomicCounter();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final count: " + counter.getCount());
}
}
ConcurrentHashMap
, CopyOnWriteArrayList
, etc. Use them instead of their non - thread - safe counterparts.Race conditions are a common and challenging problem in concurrent Java programming. However, by understanding the causes of race conditions and using appropriate prevention techniques such as synchronized blocks, ReentrantLock
, and atomic variables, developers can write more reliable and bug - free multi - threaded applications. Following best practices like minimizing shared state and using thread - safe data structures further enhances the robustness of the code.