Short answer
CME comes from a fail-fast modCount check meant to catch single-thread bugs, but it's documented as best-effort, so you can't rely on it to detect every concurrent misuse.
Explanation
Collections.synchronizedMap wraps every method in a lock, giving coarse mutual exclusion but you still must externally synchronize iteration and it serializes all access. ConcurrentHashMap instead allows concurrent reads and fine-grained (bucket/bin-level) updates with weakly-consistent iterators that never throw CME, which is why it scales far better under contention.
Common mistakes
- Relying on ConcurrentModificationException to detect races.
- Thinking Collections.synchronizedList makes iteration thread-safe.