Race Conditions & Why Synchronization Is Needed
A race condition occurs when two or more threads access and modify shared data at the same time, and the final outcome depends on the timing of their execution.
Because thread scheduling is unpredictable, this leads to inconsistent, incorrect, or unexpected results.
synchronization is needed:
- To ensure only one thread accesses a critical section at a time.
- To provide visibility guarantees, so changes made by one thread become visible to others.
- To avoid data corruption and maintain correctness.
- to bring order to the chaos caused by concurrent access.
Ways to Synchronize Threads
Java offers multiple synchronization mechanisms, from simple keywords to advanced APIs.
- synchronized keyword :Can be applied to methods or blocks (where threads give more granularity). Ensures mutual exclusion (one thread at a time). Uses intrinsic/monitor locks.
Lock framework (ReentrantLock, ReadWriteLock, etc.) : More flexible than synchronized. Features
like tryLock(), fairness policies, interruptible waiting.
volatile keyword : Ensures visibility of shared variables, does NOT ensure atomicity, so not a full synchronization tool.
Atomic classes (AtomicInteger, AtomicReference...) : Provide lock-free, thread-safe operations.
Inter-Thread Communication: wait(), notify(), notifyAll()
Java’s traditional inter-thread communication mechanism is built around Object class monitor methods.
wait() : Makes the current thread release the lock and go into a waiting state. Used when a thread needs to wait for a condition.
notify() : Wakes up one waiting thread (chosen by JVM). The awakened thread must re-acquire the lock before continuing.
notifyAll() : Wakes all threads waiting on the same lock. They compete to acquire the lock.
NOTE : All must be called inside synchronized blocks.
Thread Scheduling Basics
Java thread scheduling is preemptive and priority-based, but not guaranteed due to OS differences.
- Thread Priority
- Values from 1 (MIN) to 10 (MAX), default 5.
- Higher priority may get more CPU time, but JVM/OS may ignore priorities.
- Non-deterministic scheduling
- You cannot predict which thread runs first or how long it runs.
- Makes race conditions possible.
- yield()
- Suggests that the current thread is willing to let others run.
- Purely advisory; JVM may ignore it.
- sleep()
- Pauses the thread for a specific time.
- Does NOT release any locks.
- join()
- One thread waits for another to finish.
Basic Producer-Consumer code to understand synchronisation
package org.example.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static org.example.thread.ImplementPC.MAX;
/**
* Classic producer consumer problem
* */
class ImplementPC {
public static int num;
public static int MAX = 10;
public static boolean isSet = false;
public synchronized void setNum(int num) {
while(isSet) {
try {
wait();
} catch (InterruptedException e) {
}
}
System.out.println("producing = " + num);
this.num = num;
isSet = true;
notify();
}
public synchronized void getNum() {
while(!isSet) {
try {
wait();
} catch (InterruptedException e){}
}
System.out.println("Consuming = " + num);
isSet = false;
notify();
}
}
public class ProducerConsumer {
public static void main(String[] args) {
ImplementPC pc = new ImplementPC();
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() ->{
for(int i = 1; i < MAX; i++) {
pc.setNum(i);
}
});
executor.submit(() ->{
for(int i = 1; i < MAX; i++) {
pc.getNum();
}
});
executor.shutdown();
}
}