← Back to Home

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

  1. Shared mutable state
  2. Non-atomic operations
  3. Lack of visibility guarantees
  4. 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