Thread Safety Basics
Thread safety means that a program behaves correctly and predictably when accessed by multiple threads simultaneously. A thread-safe component maintains data consistency and correctness regardless of execution interleavings. This is a foundational concept for Java concurrency and a high-frequency interview topic.
What Is Thread Safety?
A class or method is thread-safe if:
- It produces correct results when used by multiple threads
- It does not corrupt shared state
- It requires no external synchronization by callers (ideally)
Why Thread Safety Matters
Without thread safety, concurrent programs can suffer from:
- Race conditions
- Lost updates
- Inconsistent reads
- Data corruption
- Hard-to-reproduce bugs
Common Causes of Thread-Safety Issues
- Shared mutable state
- Non-atomic operations
- Lack of visibility guarantees
- Improper synchronization
Example (Not Thread-Safe)
class Counter {
int count = 0;
void increment() {
count++; // read-modify-write (not atomic)
}
}
Multiple threads → incorrect count.
Core Concepts Behind Thread Safety
1) Atomicity
Operations should be indivisible.
- count++ ❌ (not atomic)
- AtomicInteger.incrementAndGet() ✔
2) Visibility
Changes made by one thread must be visible to others.
Achieved via:
- synchronized
- volatile
- java.util.concurrent utilities
3) Ordering (Happens-Before)
Ensures a defined execution order so reads see the latest writes.
• synchronized and volatile establish happens-before relationships.
Ways to Achieve Thread Safety in Java
1️⃣ Synchronization (synchronized)
Ensures mutual exclusion + visibility.
synchronized void increment() {
count++;
}
2️⃣ Volatile Variables
Ensures visibility, not atomicity.
volatile boolean running = true;
Use for flags, not counters.
3️⃣ Atomic Classes
Lock-free, thread-safe primitives.
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
4️⃣ Immutable Objects
No state changes → inherently thread-safe.
final class User {
private final String name;
User(String name) { this.name = name; }
}
5️⃣ Thread-Local State
Each thread gets its own copy.
ThreadLocal<Integer> tl = ThreadLocal.withInitial(() -> 0);
6️⃣ Concurrent Collections
Designed for safe concurrent access.
Examples:
- ConcurrentHashMap
- CopyOnWriteArrayList
- BlockingQueue
Thread-Safe vs Not Thread-Safe (Examples)
| Component | Thread-Safe |
|---|---|
| String | ✔ (immutable) |
| StringBuilder | ❌ |
| StringBuffer | ✔ (synchronized) |
| ArrayList | ❌ |
| Vector | ✔ (legacy, synchronized) |
| HashMap | ❌ |
| ConcurrentHashMap | ✔ |
Thread Safety vs Synchronization
| Aspect | Thread Safety | Synchronization |
|---|---|---|
| Goal | Correct concurrent behavior | Mutual exclusion |
| Scope | Design-level property | Implementation tool |
| Overhead | Varies | Can be high |
| Always required | ❌ | ❌ |
Best Practices (Production-Grade)
- Prefer immutability where possible
- Minimize shared mutable state
- Keep synchronized sections small
- Use atomic classes for counters
- Prefer concurrent collections over manual locking
- Avoid exposing internal mutable state
Common Beginner Mistakes
- Assuming volatile makes code fully thread-safe
- Synchronizing too much (performance hit)
- Using HashMap in concurrent code
- Sharing mutable objects without protection
- Ignoring visibility issues
Interview-Ready Answers
Short Answer
Thread safety ensures correct behavior when multiple threads access shared data concurrently.
Detailed Answer
Thread safety in Java ensures that shared mutable state is accessed in a controlled way using synchronization, immutability, volatile variables, atomic classes, or concurrent collections. It prevents race conditions, ensures visibility, and maintains data consistency in multithreaded programs.
Key Takeaway
Thread safety is a design property, not just a keyword.
Use the right tool for the problem—immutability, atomic operations, or synchronization—to achieve correctness without unnecessary overhead.
Thread Safety Examples (Quick Reference)
1) Non-Thread-Safe Increment (Race Condition)
class Counter {
int count = 0;
void inc() {
count++;
}
public static void main(String[] args) throws Exception {
Counter c = new Counter();
Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) c.inc(); });
Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) c.inc(); });
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(c.count);
}
}
Result
Often < 2000 → not thread-safe
2) Making It Thread-Safe with synchronized
class Counter {
int count = 0;
synchronized void inc() {
count++;
}
}
Why
Mutual exclusion on the object lock
3) Thread-Safe Using Synchronized Block (Preferred)
class Counter {
int count = 0;
void inc() {
synchronized (this) {
count++;
}
}
}
Why
Smaller critical section → better performance
4) Thread-Safe with AtomicInteger (Lock-Free)
import java.util.concurrent.atomic.AtomicInteger;
class Demo {
static AtomicInteger count = new AtomicInteger();
public static void main(String[] args) {
count.incrementAndGet();
System.out.println(count.get());
}
}
Why
Atomic operations, no explicit locks
5) Visibility Issue (Not Thread-Safe)
class Flag {
boolean running = true;
public static void main(String[] args) {
Flag f = new Flag();
new Thread(() -> {
while (f.running) {}
System.out.println("Stopped");
}).start();
f.running = false;
}
}
Problem
Update may not be visible to other thread
6) Fix Visibility with volatile
class Flag {
volatile boolean running = true;
}
Why
Guarantees visibility, not atomicity
7) Immutable Object (Inherently Thread-Safe)
final class User {
private final int id;
User(int id) {
this.id = id;
}
int getId() {
return id;
}
}
Why
State never changes
8) Thread-Local Variables (Per-Thread State)
class Demo {
static ThreadLocal<Integer> tl = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
new Thread(() -> {
tl.set(10);
System.out.println(tl.get());
}).start();
new Thread(() -> {
System.out.println(tl.get());
}).start();
}
}
Output
10
0
9) Non-Thread-Safe Collection
import java.util.*;
class Demo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
// Concurrent modification can break
}
}
10) Thread-Safe Collection (Synchronized Wrapper)
import java.util.*;
class Demo {
public static void main(String[] args) {
List<Integer> list =
Collections.synchronizedList(new ArrayList<>());
list.add(1);
System.out.println(list);
}
}
11) Thread-Safe Collection (Concurrent)
import java.util.concurrent.*;
class Demo {
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> list =
new CopyOnWriteArrayList<>();
list.add(1);
System.out.println(list);
}
}
Why
Safe iteration without locking
12) Thread-Safe Read-Modify-Write (Wrong Way)
class Demo {
volatile int x = 0;
void inc() {
x++; // ❌ not atomic
}
}
Trap
volatile ≠ atomic
13) Thread-Safe Read-Modify-Write (Correct)
class Demo {
int x = 0;
synchronized void inc() {
x++;
}
}
14) Object Escape (Breaks Thread Safety)
class Escape {
public Escape() {
new Thread(() -> System.out.println(this)).start();
}
}
Problem
this escapes before construction finishes
15) Thread-Safe Singleton (Double-Checked Locking)
class Singleton {
private static volatile Singleton instance;
private Singleton() {}
static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
16) Thread-Safe with ReentrantLock
import java.util.concurrent.locks.*;
class Counter {
int count = 0;
Lock lock = new ReentrantLock();
void inc() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
17) Thread-Safe Initialization with static
class Demo {
static final int X = init();
static int init() {
return 10;
}
}
Why
Class loading is thread-safe
18) Check-Then-Act Race Condition
if (!map.containsKey(k)) {
map.put(k, v); // ❌ race
}
Fix
map.putIfAbsent(k, v);
19) Thread Safety vs Performance (Concept)
// More locks → safer
// More locks → slower
Rule
Synchronize minimum necessary code
20) Interview Summary – Thread Safety Basics
Thread safety = Correct behavior under concurrent access
Key Points
- Atomicity + Visibility + Ordering
- synchronized → mutual exclusion
- volatile → visibility only
- Immutability → safest
- Prefer concurrent utilities over manual locking