Handling concurrency in Java
Concurrency is about multiple threads executing at the same time. Without proper handling, threads can interfere with each other, causing race conditions and data corruption.
What's a race condition? When two threads try to read and modify the same data simultaneously, and the final result depends on which thread finishes first.
What's a critical section? The part of your code that accesses shared data and must not be executed by multiple threads at the same time. Think of it as a "danger zone" that needs protection.
// (in-memory wallet balance)
public class WalletService {
private static int balance = 100; // Rs 100
// Deduct Rs 30 for a purchase
public static void deductAmount() {
if (balance >= 30) { // <-- Thread A & Thread B both pass this
balance = balance - 30; // <-- Critical section (not thread-safe!)
// Both A and B will execute this
}
}
}
Java provides several mechanisms to coordinate thread access to shared resources and protect these critical sections. This guide covers the 6 most commonly used approaches.
1. Synchronized Keyword
What it does: The simplest way to make code thread-safe. Only one thread can execute synchronized code at a time.
How it works:

Lock behavior:
Acquired: When a thread enters the synchronized block/method
Released: Automatically when the thread exits (even if exception occurs)
Queuing: Other threads wait in line until the lock is free
public class Counter {
private int count = 0;
// Method synchronization
public synchronized void increment() {
count++; // Lock acquired on entry, released on exit
}
// Block synchronization
public void decrement() {
synchronized(this) { // Lock on 'this' object
count--;
} // Lock released here
}
}


// DEADLOCK EXAMPLE - DON'T DO THIS!
synchronized(lockA) {
synchronized(lockB) { // Thread 1 gets lockA, waits for lockB
// critical section
}
}
// Meanwhile in another thread...
synchronized(lockB) {
synchronized(lockA) { // Thread 2 gets lockB, waits for lockA
// DEADLOCK! Both threads waiting forever
}
}
2. Reentrant Lock
What it does: Explicit lock with more control than synchronized. You manually lock and unlock.
How it works:

Lock behavior:
Acquired: When lock() is called and lock becomes available
Released: When unlock() is called (must be explicit!)
Queuing: Threads wait in FIFO order (if fair mode is enabled)
import java.util.concurrent.locks.ReentrantLock;
public class BankAccount {
private final ReentrantLock lock = new ReentrantLock();
private int balance = 0;
public void deposit(int amount) {
lock.lock(); // Acquire lock
try {
balance += amount;
} finally {
lock.unlock(); // ALWAYS release in finally block
}
}
// Try to lock with timeout
public boolean transfer(int amount) {
if (lock.tryLock(1, TimeUnit.SECONDS)) { // Wait max 1 second
try {
balance -= amount;
return true;
} finally {
lock.unlock();
}
}
return false; // Couldn't get lock
}
}


3. Read Write Lock
What it does: Allows multiple threads to read at once, but only one thread to write. Perfect for read-heavy scenarios.
How it works:

Lock behavior:
Read Lock: Multiple threads can acquire if no writer is active
Write Lock: Only acquired when no readers or writers are active
Queuing: Writers wait for all readers to finish, new readers wait if a writer is waiting
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Cache {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private Map<String, String> data = new HashMap<>();
// Multiple threads can read simultaneously
public String read(String key) {
rwLock.readLock().lock();
try {
return data.get(key);
} finally {
rwLock.readLock().unlock();
}
}
// Only one thread can write, blocks all readers
public void write(String key, String value) {
rwLock.writeLock().lock();
try {
data.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}


4. Semaphore
What it does: Controls how many threads can access a resource at the same time using "permits".
How it works:

Permit behavior:
Acquired: When acquire() is called and permits are available
Released: When release() is called, returning permit to the pool
Queuing: Threads wait until a permit becomes available
import java.util.concurrent.Semaphore;
public class ConnectionPool {
private final Semaphore semaphore;
public ConnectionPool(int maxConnections) {
semaphore = new Semaphore(maxConnections); // Max 10 concurrent connections
}
public void useConnection() throws InterruptedException {
semaphore.acquire(); // Get a permit (wait if none available)
try {
// Use the connection
System.out.println("Using connection");
Thread.sleep(1000);
} finally {
semaphore.release(); // Return the permit
}
}
// Try with timeout
public boolean tryUseConnection() {
if (semaphore.tryAcquire(2, TimeUnit.SECONDS)) {
try {
// Use connection
} finally {
semaphore.release();
}
return true;
}
return false; // Couldn't get connection in time
}
}


5. Atomic Classes
What it does: Lock-free thread safety for simple operations using hardware-level atomic instructions.
How it works:

No explicit locking:
No acquire/release: Operations are atomic at hardware level
Wait-free: Threads never block, they retry if conflict occurs
Visibility: Changes are immediately visible to all threads
import java.util.concurrent.atomic.*;
public class Statistics {
private AtomicInteger counter = new AtomicInteger(0);
private AtomicLong totalTime = new AtomicLong(0);
private AtomicReference<String> status =
new AtomicReference<>("IDLE");
// Thread-safe increment, no lock needed
public void recordRequest(long duration) {
counter.incrementAndGet(); // Atomic: counter++
totalTime.addAndGet(duration); // Atomic: totalTime += duration
}
// Compare and set atomically
public boolean startProcessing() {
return status.compareAndSet("IDLE", "PROCESSING");
// Only sets to PROCESSING if current value is IDLE
}
// Get average without locking
public double getAverage() {
int count = counter.get();
return count == 0 ? 0 : (double) totalTime.get() / count;
}
}


6. Concurrent Hash Map
What it does: Thread-safe HashMap that allows concurrent reads and writes without locking the entire map.
How it works:

Lock behavior:
Reads: Never lock, always proceed
Writes: Lock only the specific bucket being modified
Iterations: Weakly consistent, may not reflect latest updates
import java.util.concurrent.atomic.*;
public class Statistics {
private AtomicInteger counter = new AtomicInteger(0);
private AtomicLong totalTime = new AtomicLong(0);
private AtomicReference<String> status =
new AtomicReference<>("IDLE");
// Thread-safe increment, no lock needed
public void recordRequest(long duration) {
counter.incrementAndGet(); // Atomic: counter++
totalTime.addAndGet(duration); // Atomic: totalTime += duration
}
// Compare and set atomically
public boolean startProcessing() {
return status.compareAndSet("IDLE", "PROCESSING");
// Only sets to PROCESSING if current value is IDLE
}
// Get average without locking
public double getAverage() {
int count = counter.get();
return count == 0 ? 0 : (double) totalTime.get() / count;
}
}

