Understanding ConcurrentHashMap in Kotlin/Java
Thread-Safe Data Management in Kotlin/Java.

Senior Android Engineer from Bangladesh. Love to contribute in Open-Source. Indie Music Producer.
Search for a command to run...
Thread-Safe Data Management in Kotlin/Java.

Senior Android Engineer from Bangladesh. Love to contribute in Open-Source. Indie Music Producer.
No comments yet. Be the first to comment.
A practical guide to choosing the right coroutine primitive for your Android/Kotlin projects

This guide explains what Channels are, when to use them, how to use them correctly, and when NOT to use them β in simple, professional language. 1. What problem do Channels solve? In Kotlin coroutines, you often have multiple coroutines running at t...

Learn how to build preview-safe ViewModels in Jetpack Compose using Hilt.

HandlerInterceptor vs Filter in Spring Boot: A Practical Guide with JNDI Injection Prevention

Simple In-Memory Caching: A Tiny Trick with Massive Impact

In multi-threaded programming, managing data consistency and thread safety is paramount. A common challenge is efficiently managing a shared resource without compromising on performance. In Java and Kotlin, the ConcurrentHashMap is a key tool for achieving this balance. This article will explore what ConcurrentHashMap is, its use cases, internal workings, best practices, pros, and cons, all explained in a manner accessible to beginners.
ConcurrentHashMap is a thread-safe variant of the HashMap designed for concurrent access by multiple threads. Unlike HashMap, which can be corrupted by concurrent modifications, ConcurrentHashMap allows safe and efficient read/write operations in a multi-threaded environment without locking the entire map.
ConcurrentHashMap is ideal for scenarios where:
Multiple threads frequently read and write to the map.
High performance is required without sacrificing thread safety.
Lock contention must be minimized.
Common use cases include:
Caching shared data in web applications.
Maintaining session information in multi-threaded servers.
Real-time analytics where data is constantly updated.
To understand ConcurrentHashMap, it's essential to grasp its underlying mechanisms:
Earlier versions of
ConcurrentHashMap(before Java 8) used segmentation. The map was divided into segments, each acting as an independentHashMapwith its own lock. This reduced contention because only the segment containing the key had to be locked during updates.
In Java 8,
ConcurrentHashMapintroduced lock stripping, which uses finer-grained locking. It employs a technique called "lock-free reading," allowing reads without acquiring locks. Writes are managed using a combination of locks and CAS (Compare-And-Swap) operations to ensure thread safety.
ConcurrentHashMapuses an array ofNode<K, V>objects. Each node contains a key, value, hash, and a reference to the next node (for handling collisions). The main operations (get, put, remove) work as follows:
Get Operation:
Calculate the hash of the key.
Locate the appropriate bin (index in the array).
Traverse the linked list at that bin to find the key.
Return the corresponding value without acquiring a lock.
Put Operation:
Calculate the hash of the key.
Locate the appropriate bin.
Acquire a lock on the bin.
Check if the key exists; if so, update the value.
If the key doesn't exist, add a new node.
Release the lock.
Remove Operation:
Calculate the hash of the key.
Locate the appropriate bin.
Acquire a lock on the bin.
Traverse the linked list to find the key.
Remove the node if the key is found.
Release the lock.
CAS is an atomic operation used to update variables without locks. It checks if a variable has a specific value and, if so, updates it to a new value atomically. ConcurrentHashMap uses CAS to implement non-blocking updates for some operations, reducing the need for locks and improving performance.
To fully grasp how ConcurrentHashMap operates under the hood, let's look at some detailed examples in Kotlin for the main operations: get, put, and remove.
The get operation retrieves a value associated with a key. This operation is lock-free, allowing concurrent reads without blocking.
Example:
import java.util.concurrent.ConcurrentHashMap
fun main() {
val map = ConcurrentHashMap<String, Int>()
map["apple"] = 1
map["banana"] = 2
val key = "apple"
val hash = key.hashCode() // Calculate the hash of the key
val index = hash and (map.size - 1) // Determine the bin index
// Retrieve the value without acquiring a lock
val value = map[key]
println("Value for key '$key': $value")
}
Explanation:
The hash of the key "apple" is calculated.
The bin index is determined by applying a bitwise AND operation between the hash and the map's size minus one.
The value is retrieved by accessing the appropriate bin without acquiring a lock.
The put operation adds a key-value pair to the map. This operation may involve acquiring a lock on the bin to ensure thread safety during updates.
Example:
import java.util.concurrent.ConcurrentHashMap
fun main() {
val map = ConcurrentHashMap<String, Int>()
val key = "orange"
val value = 3
val hash = key.hashCode() // Calculate the hash of the key
val index = hash and (map.size - 1) // Determine the bin index
// Acquire a lock on the bin
synchronized(map) {
// Check if the key already exists
if (map.containsKey(key)) {
map[key] = value // Update the value
} else {
map[key] = value // Add a new key-value pair
}
}
println("Value for key '$key': ${map[key]}")
}
Explanation:
The hash of the key "orange" is calculated.
The bin index is determined similarly to the get operation.
A lock is acquired on the bin to ensure thread safety.
The key-value pair is added to the map, or the existing value is updated.
The remove operation deletes a key-value pair from the map. This operation also involves acquiring a lock on the bin.
Example:
import java.util.concurrent.ConcurrentHashMap
fun main() {
val map = ConcurrentHashMap<String, Int>()
map["apple"] = 1
map["banana"] = 2
val key = "banana"
val hash = key.hashCode() // Calculate the hash of the key
val index = hash and (map.size - 1) // Determine the bin index
// Acquire a lock on the bin
synchronized(map) {
// Remove the key-value pair if it exists
if (map.containsKey(key)) {
map.remove(key)
}
}
println("Value for key '$key' after removal: ${map[key]}")
}
Explanation:
The hash of the key "banana" is calculated.
The bin index is determined similarly to the previous operations.
A lock is acquired on the bin to ensure thread safety.
The key-value pair is removed if it exists.
ConcurrentHashMap uses CAS operations for some updates to avoid locking. This is an atomic operation that helps in maintaining thread safety with better performance.
Example:
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
fun main() {
val map = ConcurrentHashMap<String, AtomicInteger>()
map["apple"] = AtomicInteger(1)
val key = "apple"
val newValue = 2
// CAS operation to update the value
val currentValue = map[key]
if (currentValue != null) {
currentValue.compareAndSet(currentValue.get(), newValue)
}
println("Value for key '$key': ${map[key]?.get()}")
}
Explanation:
An AtomicInteger is used to hold the value associated with the key "apple".
The compareAndSet method performs the CAS operation, updating the value atomically if it matches the expected current value.
To make the most of ConcurrentHashMap, follow these best practices:
Use ConcurrentHashMap for High Concurrency:
Avoid Excessive Locking:
ConcurrentHashMap handles it internally.Use Appropriate Methods:
putIfAbsent, remove(key, value), and computeIfAbsent.Limit Size:
Thread-safe Iteration:
ConcurrentHashMap's iterators and methods like forEach, search, and reduce for thread-safe operations.Thread Safety:
Performance:
Scalability:
Ease of Use:
Memory Overhead:
HashMap due to additional structures for thread safety.Complexity:
Inefficiency for Single-threaded Use:
HashMap for single-threaded applications due to additional overhead.Hereβs a simple example to illustrate how to use ConcurrentHashMap in Kotlin:
import java.util.concurrent.ConcurrentHashMap
fun main() {
val map = ConcurrentHashMap<String, Int>()
// Add elements to the map
map["apple"] = 1
map["banana"] = 2
// Retrieve an element
println("Value for key 'apple': ${map["apple"]}")
// Update an element
map["apple"] = 3
println("Updated value for key 'apple': ${map["apple"]}")
// Remove an element
map.remove("banana")
println("Value for key 'banana' after removal: ${map["banana"]}")
// Compute if absent
map.computeIfAbsent("orange") { 4 }
println("Value for key 'orange': ${map["orange"]}")
}
ConcurrentHashMap is a powerful tool for managing shared data in multi-threaded applications. By understanding its internal workings and following best practices, you can leverage its capabilities to
achieve thread-safe and high-performance data management. While it has some drawbacks, the benefits far outweigh them in scenarios requiring concurrent access. Use ConcurrentHashMap wisely, and it will serve as a robust solution for your concurrent programming needs.
That's it for today. Happy Coding...