Understanding Multithreading in Java Through Collections and Atomics

1️⃣ HashMap / TreeMap / TreeSet (not thread-safe)

HashMap:
  • Structure: array of buckets + linked lists / trees (for collisions).
  • Under the hood: put/remove modifies the bucket array and possibly reorders the chains.
  • Problem with multithreading: two threads can simultaneously modify the same bucket → corrupt structure, element loss, infinite loop during iteration.
TreeMap / TreeSet:
  • Structure: red-black tree.
  • When inserting/deleting, a thread modifies multiple tree nodes.
  • Concurrent modifications → tree structure can If get/put/rebalance breaks, they can throw a ConcurrentModificationException or corrupt the balance.

Conclusion: External synchronization is needed, otherwise the map/set is easily corrupted.

2️⃣ SynchronizedMap / SynchronizedSortedMap / SynchronizedSortedSet

Under the hood: All methods are wrapped in synchronized(this) on the map/set object. Any operation locks the entire map/set for the duration of the operation. This guarantees data integrity, but prevents parallelism.


public V put(K key, V value) {
    synchronized (this) {
        return m.put(key, value);
    }
}

Read/write → one operation at a time, eliminating race conditions.

3️⃣ ConcurrentHashMap

Under the hood (Java 8+):

  • Array of buckets + tree nodes for collisions.
  • Primary synchronization:
    • get — completely lock-free.
    • put — CAS (Compare-And-Swap) at the bucket level or synchronized on the tree node only if needed.
    • Table expansion (resize) is partially synchronized on segments.
  • Concurrent put/get operations on different keys are performed in parallel without locking the entire map.
  • Race condition On value: If this is a mutable object within the map, CHM does not synchronize its fields.

4️⃣ ConcurrentSkipListMap / ConcurrentSkipListSet

  • Structure: Skip List (multi-layer linked list).
  • Operations use lock-free algorithms + CAS.
  • Each list level is partially locked or updated atomically.
  • Allows parallel operations on different keys.
  • Simultaneous changes to one key → race condition unless atomic methods are used (compute, putIfAbsent).

5️⃣ AtomicInteger / AtomicReference / AtomicLong

  • Use CAS CPU instructions (Compare-And-Swap).
  • Atomic and lock-free updates: the same value won't be lost across concurrent threads.
  • Example:
    
    AtomicInteger ai = new AtomicInteger(0);
    ai.incrementAndGet(); // atomic
    
  • No JVM- or object-level locks – pure CAS on the CPU.

Comparison tables: one element, two threads

Collection / Object Thread 1 → Thread 2 arrived a little later Thread 1 ↔ Thread 2 started simultaneously Under the Hood / Comment
HashMap ❌ race condition → value may be lost, structure is fine (if only one field) ❌ race condition → may break structure, data loss No synchronization, everything is at the discretion of CPU threads
TreeMap / TreeSet ❌ race condition → corrupt tree or data loss ❌ race condition → corrupt tree No thread safety, node manipulations are not atomic
SynchronizedMap / SynchronizedSortedMap / SynchronizedSortedSet ✅ safe, thread 2 is waiting ✅ safe, one thread is waiting for another The monitor locks the entire map/set for the operation
ConcurrentHashMap ⚠️ map structure is OK, but race condition: last assignment wins ⚠️ CAS at the bucket or node level → last assignment wins Structure Maps are lock-free, the object field is not protected.
ConcurrentSkipListMap / Set ⚠️ Skip list structure is valid, value race condition exists. ⚠️ Last assignment wins. Node-level CAS, value object is not atomic.
AtomicInteger / AtomicReference / AtomicLong ✅ Atomic, thread 2 waits in CAS only if there is a conflict, value is incremented correctly. ✅ Atomic, CAS guarantees one update → the second one retries. CPU CAS, lock-free, race condition excluded.
Now let's consider a more complex case with simultaneous modifications by two threads, and a new key being added by a third thread.

Table: one element edited by two threads + a new key added

Collection / Object Code Example Threads 1+2 edit an element Thread 3 adds a new key Under the Hood / Comment Data Loss / Corrupt
HashMap
static Map<String,Integer> map = new HashMap
       
        ();
       
❌ race condition, one update may be lost ❌ adding a new key may break the structure No synchronization, all threads change buckets simultaneously High risk of corruption
TreeMap / TreeSet
static TreeMap
       
         map = new TreeMap
        
         ();
        
       
❌ race condition, tree structure may break ❌ adding a new key changes tree nodes No thread safety, tree balancing breaks High risk of corruption
SynchronizedMap / SynchronizedSortedMap / SynchronizedSortedSet
static Map
       
         map = Collections.synchronizedMap(new HashMap
        
         ());
        
       
✅ Threads 1 and 2 execute sequentially (lock on monitor) ✅ Thread 3 waits for completion All methods are synchronized on the object, iteration requires a block Low, safe
ConcurrentHashMap

static ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<>();
map.computeIfAbsent("key", k -> new AtomicInteger()).incrementAndGet();
⚠️ Race condition on value, map structure is correct ✅ Adding a new key in parallel Lock-free buckets + CAS, lock-free reads Value may be lost, structure is fine
ConcurrentSkipListMap / Set

static ConcurrentSkipListMap<String, AtomicInteger> map = new ConcurrentSkipListMap<>();
⚠️ Race condition on value ✅ Adding a new key in parallel Skip list + CAS, lock-free Value may be lost, structure is fine
AtomicInteger / AtomicReference
AtomicInteger ai = new AtomicInteger();
ai.incrementAndGet();
✅ Atomic, both threads update the value correctly ✅ Adding a new key through a separate AtomicReference object is safe CPU-level CAS, lock-free Low, safe

🔹 Key points:

  • SynchronizedMap — methods lock the entire object, so Thread 1 and Thread 2 never execute concurrently.
  • ConcurrentHashMap — the map structure is safe, but the object's value is not atomic, so an AtomicInteger or the compute method is required.
  • Atomic objects are completely atomic, eliminating race conditions.

Comparison of Thread-Safe Sorted Collections

1️⃣ Collections.synchronizedSortedMap(new TreeMap<<())< /div>
Collections.synchronizedSortedMap(new TreeMap<<())
Thread A: ─────[took monitor]───────────────> Running
Thread B: ─────[waiting, monitor busy]─────┐
Thread C: ─────[waiting, monitor busy]─────┘
All operations block each other: get, put, remove, containsKey — only one thread can execute within the same thread at a time.
Output:
The real tree (red-black).
Full sorting is preserved.
But there is no parallelism — everyone is waiting.

2️⃣ ConcurrentSkipListMap
Level 3: 10 --------- 30 ---------------- 75 --------- 90
Level 2: 10 ---- 25 ---- 50 ---- 60 ---- 75 ---- 90
Level 1: 10 20 25 30 40 50 60 70 75 80 90

Thread A: inserts 35
Thread B: inserts 55
Thread C: reads 30

✔ All three operations can be performed concurrently because locks are only taken on small segments of the list.
✔ Key order is preserved, submaps work.

Output:
Not a tree, but a skip list.
Sorted keys like in TreeMap.
Multithreaded, parallel, without global locking.

Property SynchronizedSortedMap(TreeMap) ConcurrentSkipListMap
Structure Red-black tree Skip list
Key ordering Yes Yes
Thread safety Yes, via synchronized Yes, via fine-grained locks
Parallel operation No, single thread Yes, multiple threads simultaneously
SubMap/headMap support Yes Yes
Performance with many threads Drops Good

🔹 Conclusions

HashMap / TreeMap / TreeSet

  • Not thread-safe.
  • Any concurrent changes can lead to data loss, structure corruption, and infinite iterations.
  • Requires external synchronization (e.g., a synchronized block).

SynchronizedMap / SynchronizedSortedMap / SynchronizedSortedSet

  • Thread-safe, but all operations lock the entire collection.
  • Good for infrequent operations, but poor for high concurrency.
  • Iteration requires separate synchronization on the object.

ConcurrentHashMap

  • The map structure is safe for concurrent reading and writing of different keys (lock-free at the bucket level).
  • Race conditions are only possible on mutable values ​​if you are simply storing objects without atomic operations.
  • Solution: use AtomicInteger, AtomicReference, or methods like compute, computeIfAbsent.

ConcurrentSkipListMap / ConcurrentSkipListSet

  • The structure is correct, operations are lock-free via CAS.
  • Concurrent changes to the same key are not atomic at the value level – atomics are required.
  • Suitable if you need a sorted map Thread-safe set/map.

AtomicInteger / AtomicLong / AtomicReference

  • Fully atomic operations, lock-free.
  • No risk of losing updates, even if multiple threads simultaneously modify the value.
  • Often used in conjunction with ConcurrentHashMap to safely update values.

🔹 Rule of Thumb

  • If the collection is not thread-safe → either use external synchronization or replace it with a thread-safe version (ConcurrentHashMap, ConcurrentSkipListMap).
  • If you store mutable objects inside a map → use atomics or compute methods, otherwise values ​​may be lost.
  • If you need maximum concurrency → ConcurrentHashMap + AtomicInteger/Reference is the best option.
  • If you need simple synchronization without any hassle, → Collections.synchronizedMap/Set — but remember to lock the entire collection.

🌐 На русском
Total Likes:0
My social media channel
By sending an email, you agree to the terms of the privacy policy

Useful Articles:

Asynchrony and Reactivity in Java: CompletableFuture, Flow, and Virtual Threads
In modern Java development, there are three main approaches to asynchrony and concurrency: CompletableFuture — for single asynchronous tasks. Flow / Reactive Streams — for data flows with backpressur...
Understanding Multithreading in Java Through Collections and Atomics
1️⃣ HashMap / TreeMap / TreeSet (not thread-safe) HashMap: Structure: array of buckets + linked lists / trees (for collisions). Under the hood: put/remove modifies the bucket array and possibly reord...
Go vs. Java - Comparing Memory Models - Part 2: Atomic Operations, Preemption, Defer/Finally, Context, Escape Analysis, GC, False Sharing
Atomic operations Atomic operations ensure correct execution of variable operations without race conditions, guaranteeing a happens-before between reads and writes. Go example: import "sync/atomic" va...

New Articles:

Generics, Reflection and Channels - Go vs Java | Types - Language
In this article we will analyze advanced type system features in Go: generics (type parameters), reflection, and channel types for concurrency. We will compare Go and Java approaches, so Java develope...
Let's look at: Trace, Profiling, Integration Testing, Code Coverage, Mocking, Deadlock Detection in Go vs Java | Testing, Debugging and Profiling
Series: Go for Java Developers — analysis of trace, profiling and testing In this article we will analyze tools and practices for testing, debugging and profiling in Go. For a Java developer this wil...
Let's Break It Down: Rate Limiter, Non-Blocking Operations, and Scheduler: Go vs. Java | Concurrency Part 4
This article is dedicated to understanding the principles of concurrency and synchronization in Go and Java. We ll cover key approaches such as rate-limiter, non-blocking operations, and task scheduli...
Fullscreen image