Skip to main content

Command Palette

Search for a command to run...

Handling concurrency in Java

Published
5 min readView as Markdown

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;
    }
}