A class is thread-safe if it behaves correctly when accessed from multiple threads concurrently, with no additional synchronization or coordination required from the calling code, and regardless of how the runtime interleaves those threads.
"Correct" means it keeps its invariants
Correctness is the key word. A thread-safe class always satisfies its own specification: its invariants hold and its postconditions are honored no matter the timing of concurrent calls. A size that can go negative, a balance that loses an update, or an iterator that throws under concurrent modification — all are correctness violations. Thread safety is about preserving invariants across the visibility, atomicity, and ordering hazards the JMM allows.
"No external synchronization" is the contract
The crucial half of the definition is who is responsible for locking. If callers must wrap every access in their own synchronized block to be safe, the class is not thread-safe — it's merely usable in a thread-safe way by a disciplined caller. ArrayList is not thread-safe; CopyOnWriteArrayList is. Thread safety is encapsulated: all the coordination lives inside the class.
// NOT thread-safe: check-then-act on shared state has a race
class Counter {
private int count;
public int next() { return ++count; } // read, increment, write — not atomic
}
// Thread-safe: the class itself guarantees correctness
class SafeCounter {
private final AtomicInteger count = new AtomicInteger();
public int next() { return count.incrementAndGet(); }
}
Beyond a single class
Thread safety doesn't compose for free. Two thread-safe collections combined in a compound action (if (!map.containsKey(k)) map.put(k, v)) can still race — each call is atomic, but the pair is not. That's why APIs add atomic compound operations like putIfAbsent and computeIfAbsent.