Java Concurrency Essentials: volatile, synchronized, and ThreadLocal

 

Java Concurrency Essentials: volatile, synchronized, and ThreadLocal

In Java, when multiple threads run at the same time, they often need to read or update shared data.
If not handled correctly, this leads to bugs that are hard to detect and reproduce.

Java provides three important mechanisms to handle this safely:

  • volatile

  • synchronized

  • ThreadLocal

Each solves a different concurrency problem.


🧠 The Core Problems in Multithreading

1️⃣ Visibility Problem

One thread updates a variable, but other threads do not see the updated value.

2️⃣ Race Condition

Multiple threads update the same variable at the same time, causing incorrect results.

3️⃣ Shared State Complexity

Threads compete for shared data, leading to:

  • locks

  • contention

  • performance issues


1️⃣ volatile Keyword

🔹 What Is volatile?

The volatile keyword ensures that changes made by one thread are immediately visible to other threads.

volatile boolean running = true;

🔍 What volatile Guarantees

✅ Visibility
✅ Ordering (happens-before)

❌ Atomicity
❌ Mutual exclusion


❌ Problem Without volatile

class Worker extends Thread { private boolean running = true; public void run() { while (running) { // do work } System.out.println("Stopped"); } public void stopWorker() { running = false; } }

🔴 The worker thread may never stop, because it keeps reading a cached value.


✅ Solution Using volatile

class Worker extends Thread { private volatile boolean running = true; public void run() { while (running) { // do work } System.out.println("Stopped"); } public void stopWorker() { running = false; } }

✔ All threads see the updated value immediately.


🏭 Real-World Use Cases of volatile

  • Shutdown flags

  • Feature toggles

  • Configuration refresh flags

  • Polling loops


⚠️ Important Limitation

volatile int count = 0; count++; // ❌ NOT thread-safe

Reason: count++ is a read–modify–write operation.


2️⃣ synchronized Keyword

🔹 What Is synchronized?

synchronized ensures that only one thread at a time can access a critical section of code.

It guarantees:

  • Mutual exclusion

  • Atomicity

  • Visibility


❌ Problem Without synchronized

class Counter { int count = 0; public void increment() { count++; // NOT thread-safe } }

Thread Code

class MyThread extends Thread { private Counter counter; MyThread(Counter counter) { this.counter = counter; } public void run() { for (int i = 0; i < 1000; i++) { counter.increment(); } } }

Main Method

public class Main { public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t1 = new MyThread(counter); Thread t2 = new MyThread(counter); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.count); } }

🔴 Output may be incorrect:

1738 // expected 2000

✅ Solution Using synchronized (Best Practice)

class Counter { private int count = 0; private final Object lock = new Object(); public void increment() { synchronized (lock) { count++; } } public int getCount() { return count; } }

✔ Only one thread updates count at a time
✔ No race conditions


🏭 Real-World Use Case of synchronized

Bank Account Example

class BankAccount { private int balance = 1000; public synchronized void withdraw(int amount) { if (balance >= amount) { balance -= amount; } } public synchronized void deposit(int amount) { balance += amount; } }

✔ Prevents double withdrawal
✔ Ensures data consistency


3️⃣ ThreadLocal

🔹 What Is ThreadLocal?

ThreadLocal provides one variable per thread, even though the variable appears shared.

ThreadLocal<String> userId = new ThreadLocal<>();

Each thread:

  • has its own copy

  • cannot see other threads’ values


✅ Simple ThreadLocal Example

class UserContext { static ThreadLocal<String> userId = new ThreadLocal<>(); } class MyTask extends Thread { private String id; MyTask(String id) { this.id = id; } public void run() { UserContext.userId.set(id); System.out.println( Thread.currentThread().getName() + " userId = " + UserContext.userId.get() ); UserContext.userId.remove(); } } public class Main { public static void main(String[] args) { new MyTask("U1").start(); new MyTask("U2").start(); } }

✅ Output

Thread-0 userId = U1 Thread-1 userId = U2

✔ Same variable
✔ Different value per thread
✔ No synchronization required


🏭 Real-World Use Cases of ThreadLocal

  • User context (userId, tenantId)

  • Request / trace ID in logging

  • Database connection per thread

  • DateFormat handling

  • Security context


⚠️ Important Warning

In thread pools, always clean up:

try { threadLocal.set(value); } finally { threadLocal.remove(); }

Otherwise → memory leaks.


🆚 Comparison Summary

FeaturevolatilesynchronizedThreadLocal
Shared dataYesYes❌ No
VisibilityN/A
Atomicity✅ (per thread)
Locking
PerformanceHighLowerHigh
Main useFlagsCountersContext data

🎯 When to Use What?

✔ Use volatile when:

  • One thread writes

  • Other threads read

  • Value is independent (flags)

✔ Use synchronized when:

  • Multiple threads update shared data

  • Atomicity and consistency are required

✔ Use ThreadLocal when:

  • Sharing can be avoided

  • Each thread needs its own state


📝 Interview-Ready One-Liner

volatile ensures visibility, synchronized ensures mutual exclusion, and ThreadLocal avoids shared state altogether.


🔚 Final Takeaway

Concurrency is not about choosing one keyword — it’s about choosing the right strategy.

  • Share safely → volatile or synchronized

  • Don’t share → ThreadLocal

No comments:

Post a Comment

Java Design Patterns With What • Why • When • Full Java Programs • Client Usage

  Java Design Patterns  With What • Why • When • Full Java Programs • Client Usage Introduction Design Patterns are proven solutions to...

Featured Posts