"Thread safety" and "concurrency" are often used interchangeably, but they are not the same. In Java multithreading, understanding the difference between them is critical to writing robust, efficient, and correct applications.
This guide dives deep into what thread safety and concurrency really mean, how they differ, when they overlap, and how to master both in practice.
🧠 Definitions: What They Actually Mean
🔒 Thread Safety
A thread-safe program behaves correctly when accessed by multiple threads, regardless of the timing or interleaving of those threads. It avoids issues like:
- Race conditions
- Visibility problems
- Inconsistent state
Thread safety focuses on correctness under concurrency.
🧵 Concurrency
Concurrency is about doing multiple tasks at once. It enables performance improvements by overlapping operations (CPU, I/O, etc.), and is achieved using:
- Multiple threads
- Asynchronous processing
- Parallelism (when combined with multiple cores)
Concurrency focuses on performance and responsiveness.
🔍 Key Differences
Feature | Thread Safety | Concurrency |
---|---|---|
Primary Goal | Correctness | Performance |
Required For Concurrency | No | Yes |
Requires Synchronization | Usually | Not necessarily |
Example Concern | Preventing race conditions | Processing tasks in parallel |
Analogy | Secure bank vault access | Multiple bank tellers |
⚙️ Thread Lifecycle Context
State | Thread-Safe Impact | Concurrent Usage |
---|---|---|
NEW | Not yet affecting shared state | Preparing tasks |
RUNNABLE | May read/write shared data | Doing parallel work |
BLOCKED | Waiting on locks | Can reduce performance |
WAITING | Waiting for coordination | Common in producer-consumer |
TERMINATED | Thread complete | Completed task |
🔐 Ensuring Thread Safety
Option 1: synchronized
public synchronized void add(int value) {
this.total += value;
}
Option 2: ReentrantLock
lock.lock();
try {
this.total += value;
} finally {
lock.unlock();
}
Option 3: Atomic Variables
AtomicInteger count = new AtomicInteger();
count.incrementAndGet();
Option 4: Concurrent Collections
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
🧰 Leveraging Concurrency
Thread + Runnable
new Thread(() -> doWork()).start();
ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> doWork());
Fork/Join Framework
ForkJoinPool pool = ForkJoinPool.commonPool();
pool.submit(new RecursiveTask<>());
Virtual Threads (Java 21)
Thread.startVirtualThread(() -> fetchData());
⚠️ Common Pitfalls
- Thread-safe ≠ concurrent: A
synchronized
block may be thread-safe but serializes access (no concurrency). - Concurrent ≠ thread-safe: Running tasks in parallel on shared state can corrupt data if not managed correctly.
- Over-synchronization: Reduces concurrency and causes contention.
- Under-synchronization: Leads to data races and undefined behavior.
📦 Real-World Scenarios
✅ Thread-Safe But Not Concurrent
public synchronized void writeLog(String message) {
writer.write(message);
}
Only one thread can log at a time—safe but not concurrent.
✅ Concurrent But Not Thread-Safe
List<String> list = new ArrayList<>();
Runnable task = () -> list.add("value"); // Race condition!
Multiple threads run in parallel but corrupt shared list.
✅ Both Thread-Safe and Concurrent
ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
Allows safe parallel access.
📌 What's New in Java?
Java 8
- Lambdas for Runnable, Callable
ConcurrentHashMap.computeIfAbsent()
CompletableFuture
Java 9
- Flow API (Reactive Streams)
Java 11
- Minor improvements to
CompletableFuture
and HTTP client
Java 17
- Pattern matching and sealed classes for thread-safe APIs
Java 21
- ✅ Virtual Threads
- ✅ Structured Concurrency
- ✅ Scoped Values for better context isolation
✅ Best Practices
- Identify shared mutable state and guard it
- Use immutable objects whenever possible
- Favor high-level concurrency utilities (
ExecutorService
,ConcurrentHashMap
) - Apply synchronization only when necessary
- Validate performance impact of locks
- Monitor thread contention and CPU usage with profiling tools
🚫 Anti-Patterns
- Using
ArrayList
orHashMap
with multiple threads - Overusing
synchronized
on public methods - Ignoring
volatile
orfinal
for visibility - Blindly using thread pools without sizing
- Assuming single-threaded code is safe when ported to parallel model
🧰 Design Patterns That Help
- Worker Thread: Submit work to a pool
- Immutable Object: Eliminate need for synchronization
- Guarded Suspension: Wait until condition holds
- Monitor Object: Encapsulate synchronized access
- Thread-Local Storage: Store per-thread variables
📘 Conclusion and Key Takeaways
- Thread safety ensures correctness under concurrency
- Concurrency enables performance, not correctness
- A system can be one, both, or neither
- Use the right tool for the job—choose between locking, atomicity, or high-level constructs
- With Java 21, thread safety and concurrency are easier to manage using virtual threads and structured concurrency
❓ FAQ
1. Can I write concurrent code without threads?
Yes, via async programming, reactive streams, or coroutines.
2. Is synchronized
always thread-safe?
Not necessarily—depends on logic inside and how shared state is used.
3. What if I use volatile
only?
It ensures visibility, not atomicity.
4. Is ConcurrentHashMap
always safe?
Yes, for individual operations. Compound actions may still need synchronization.
5. Are virtual threads thread-safe?
Virtual threads are lightweight, but thread safety rules still apply.
6. Can I mix concurrent and non-concurrent code?
Yes, but shared state access must be guarded appropriately.
7. Why does thread safety reduce performance?
Because it limits concurrency via synchronization or blocking.
8. How to know if code is thread-safe?
Test with race condition tools, code audits, and use safe data structures.
9. Can immutable objects be shared freely?
Yes. They are inherently thread-safe.
10. Is JavaScript concurrent?
JavaScript is single-threaded but asynchronous. Not concurrent like Java threads.