A race condition occurs when two or more threads access and modify shared data concurrently, and the final outcome depends on the relative timing of the threads’ execution. For example:
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();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + counter.getCount());
}
}
In this example, the increment
method is not thread - safe because the count++
operation is not atomic. It consists of three steps: read the value of count
, increment it, and write the new value back. If two threads execute these steps concurrently, the final count may be less than 2000.
Atomic operations are operations that are executed as a single, indivisible unit. Java provides atomic classes in the java.util.concurrent.atomic
package, such as AtomicInteger
, AtomicLong
, etc.
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 AtomicityExample {
public static void main(String[] args) throws InterruptedException {
AtomicCounter counter = new AtomicCounter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + counter.getCount());
}
}
Visibility refers to the ability of one thread to see the changes made by another thread. In Java, the volatile
keyword can be used to ensure visibility.
class VolatileExample {
private volatile boolean flag = false;
public void setFlag() {
flag = true;
}
public boolean getFlag() {
return flag;
}
}
When a variable is declared as volatile
, any write to the variable is immediately visible to all other threads.
Synchronization is a mechanism in Java that allows only one thread to access a block of code or a method at a time. Java provides the synchronized
keyword for this purpose.
class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
public class SynchronizationExample {
public static void main(String[] args) throws InterruptedException {
SynchronizedCounter counter = new SynchronizedCounter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + counter.getCount());
}
}
In this example, the synchronized
keyword ensures that only one thread can execute the increment
and getCount
methods at a time.
Java also provides the Lock
interface in the java.util.concurrent.locks
package, which offers more flexibility than the synchronized
keyword.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class LockCounter {
private int count = 0;
private Lock 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 LockExample {
public static void main(String[] args) throws InterruptedException {
LockCounter counter = new LockCounter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + counter.getCount());
}
}
Immutable objects are objects whose state cannot be changed after they are created. Since there is no mutable state, they are inherently thread - safe.
final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
Thread - local storage allows each thread to have its own copy of a variable. Java provides the ThreadLocal
class for this purpose.
public class ThreadLocalExample {
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
threadLocal.set(threadLocal.get() + 1);
}
System.out.println("Thread 1: " + threadLocal.get());
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
threadLocal.set(threadLocal.get() + 1);
}
System.out.println("Thread 2: " + threadLocal.get());
});
t1.start();
t2.start();
}
}
java.util.concurrent
package provides many high - level concurrency utilities such as ExecutorService
, BlockingQueue
, etc. These utilities are designed to be thread - safe and can simplify your code.ThreadMXBean
to detect thread contention and use unit testing frameworks to test your code in a multi - threaded environment.Thread safety is a crucial aspect of Java programming when dealing with multi - threaded applications. By understanding the fundamental concepts of race conditions, atomicity, and visibility, and by using strategies such as synchronization, atomic classes, and lock objects, developers can write thread - safe code. Additionally, common patterns like immutable objects and thread - local storage can further simplify the development process. Following best practices can help in creating robust and reliable multi - threaded applications.