skip to content

Is HashSet thread-safe, and how do you safely share a set of unique elements across multiple threads?

level: middleimportance: should knowfreq 52%

answer

  1. HashSet/LinkedHashSet are NOT thread-safe
  2. Fail-fast iterator throws ConcurrentModificationException
  3. synchronizedSet = one lock; still lock manually to iterate
  4. ConcurrentHashMap.newKeySet() for high write concurrency (weakly-consistent iterators)
  5. Set.copyOf/Set.of = immutable, no locks needed

basics

~10 s

No, HashSet is not thread-safe. If several threads modify it at once you can corrupt it or get errors. Use a concurrent set or external synchronization to share one safely.

solid answer

~40 s

HashSet (and LinkedHashSet) are not thread-safe. Concurrent modification without coordination can corrupt the internal structure, lose updates, or, during iteration, throw ConcurrentModificationException via the fail-fast iterator. For shared mutable access you have options. Collections.synchronizedSet(new HashSet<>()) wraps every method in a single lock — simple but a bottleneck, and you must still manually synchronize on the set while iterating. For high concurrency prefer ConcurrentHashMap.newKeySet(), a lock-striped concurrent set with weakly-consistent iterators that never throw CME. If the set is built once and only read afterwards, make it immutable with Set.copyOf or Set.of so no synchronization is needed at all. If writes are rare versus reads, a CopyOnWriteArraySet can fit. The right choice depends on the read/write ratio and contention.

code

java · 13 lines
java
// NOT safe under concurrent writes:
Set<String> unsafe = new HashSet<>();

// Option A: coarse lock; remember to sync around iteration too
Set<String> sync = Collections.synchronizedSet(new HashSet<>());
synchronized (sync) { for (String s : sync) { /* ... */ } }

// Option B: concurrent set, weakly-consistent iterator, no CME
Set<String> concurrent = ConcurrentHashMap.newKeySet();
concurrent.add("x"); // many threads can do this concurrently

// Option C: immutable, inherently thread-safe (read-only)
Set<String> immutable = Set.copyOf(unsafe);

go deeper

for a junior

Knows HashSet is not thread-safe and that a concurrent or synchronized alternative is needed for shared writes.

for a middle

Lists the safe options (synchronizedSet, ConcurrentHashMap.newKeySet, immutable) and knows the fail-fast iterator throws CME.

for a senior

Chooses among options by read/write ratio and contention, explains weakly-consistent vs fail-fast iterators, and the compound-action atomicity gap.

for a principal

Designs concurrency strategy across a system (immutability by default, lock-striping where needed), reasons about throughput/contention trade-offs and historical resize-corruption hazards.

## What 'thread-safe' means A class is **thread-safe** if multiple threads can use the same instance concurrently without external locking and still behave correctly. **HashSet is not thread-safe.** Neither is LinkedHashSet. They assume a single thread (or external coordination) mutates them. ## What goes wrong without coordination If two threads `add`/`remove` on the same HashSet at the same time: - **Lost updates**: one thread's change overwrites another's. - **Structural corruption**: a concurrent resize/rehash can leave the bucket array in a broken state — in older JDKs this even caused infinite loops; generally you can end up with missing or duplicated elements. - **ConcurrentModificationException (CME)**: HashSet's iterator is **fail-fast** — it keeps a `modCount` and throws CME if the set changes structurally during iteration (even from the same thread, e.g. removing inside a for-each without the iterator's own `remove`). This is a *best-effort* bug detector, not a guarantee. ## The safe options, from simplest to most concurrent **1. Immutable (read-only after build) — best when possible.** If you populate the set once and only read it afterwards, wrap it so nobody can mutate it: `Set.copyOf(original)` or `Set.of(a, b, c)`. Immutable objects are inherently thread-safe — no locks needed, no resize races. **2. `Collections.synchronizedSet(new HashSet<>())`.** Returns a wrapper that guards every method with one intrinsic lock. Correct but coarse: all access serialises through a single lock (a bottleneck under contention). Caveat: **iteration is not automatically safe** — you must manually `synchronized (set) { for (... ) ... }` around the loop, or you can still get CME. **3. `ConcurrentHashMap.newKeySet()` — the modern default for a concurrent mutable set.** This is a Set view backed by ConcurrentHashMap. It uses fine-grained locking/CAS so many threads can write to different buckets simultaneously. Its iterators are **weakly consistent**: they reflect some state of the set, never throw CME, and tolerate concurrent modification. Allows high throughput. **4. `CopyOnWriteArraySet` — for read-heavy, write-rare sets.** Every mutation copies the whole backing array. Reads and iteration are lock-free and never throw CME, but writes are O(n) and expensive. Good for small sets read far more than written (e.g. listener lists). ## Choosing | Situation | Choice | |---|---| | Built once, then read-only | `Set.copyOf` / `Set.of` (immutable) | | Low contention, simplicity | `Collections.synchronizedSet` | | High write concurrency | `ConcurrentHashMap.newKeySet()` | | Many reads, rare writes | `CopyOnWriteArraySet` | ## Key reminder Wrapping with `synchronizedSet` does **not** make compound actions atomic (e.g. check-then-add). For those you still need to hold the lock across the whole compound operation, or use a concurrent set's atomic methods.

  • Why might removing an element inside a for-each loop over a HashSet throw ConcurrentModificationException even in a single thread?
    The for-each uses a fail-fast iterator that tracks modCount. Calling set.remove() structurally modifies the set behind the iterator's back, so on the next iteration it detects the mismatch and throws CME. Use the iterator's own remove() (or removeIf) instead.
  • Does Collections.synchronizedSet make a check-then-act operation like 'add if absent' atomic?
    No. Each individual method is synchronized, but a compound check-then-act spans two calls and another thread can interleave between them. You must hold the lock across the whole operation, or use a concurrent set's atomic add (which returns false if already present).

A HashSet is a shared whiteboard with no rules: if two people write at once, you get a smeared mess. synchronizedSet hands around a single marker (one writer at a time). A concurrent set divides the board into zones so people can write in different zones simultaneously.

saying these in an interview costs you the question

  • Claiming HashSet is thread-safe because operations are 'fast'
  • Thinking synchronizedSet makes iteration or compound actions automatically safe
  • Believing the fail-fast iterator reliably catches all concurrency bugs
  • Using ConcurrentHashMap.newKeySet() but expecting strongly-consistent snapshot iteration

context