Skip to content

Commit 47d7f4d

Browse files
committed
Add thread-safe ConcurrentHashMap implementation with coverage tests
- Implemented `ConcurrentHashMap` using separate chaining with linked lists for collision handling. - Ensured thread safety with bucket-level locking using `ReentrantLock`. - Added methods: `put`, `get`, `remove`, and `containsKey`. - Supported `null` keys and values. - Added unit tests in `ConcurrentHashMapTest`: - Verified basic operations, null key handling, and edge cases. - Simulated concurrent access with multiple threads.
1 parent 12935c2 commit 47d7f4d

File tree

2 files changed

+329
-0
lines changed

2 files changed

+329
-0
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package com.thealgorithms.datastructures.hashmap.hashing;
2+
3+
import java.util.concurrent.locks.ReentrantLock;
4+
5+
/**
6+
* A thread-safe implementation of a HashMap using separate chaining with linked lists
7+
* and ReentrantLocks for concurrency control.
8+
*
9+
* @param <K> the type of keys maintained by this map
10+
* @param <V> the type of mapped values
11+
*/
12+
@SuppressWarnings("rawtypes")
13+
public class ConcurrentHashMap<K, V> {
14+
private final int hashSize;
15+
private final Bucket<K, V>[] buckets;
16+
private final ReentrantLock[] locks;
17+
18+
/**
19+
* Constructs a ConcurrentHashMap with the specified hash size.
20+
*
21+
* @param hashSize the number of buckets in the hash map
22+
*/
23+
@SuppressWarnings("unchecked")
24+
public ConcurrentHashMap(int hashSize) {
25+
this.hashSize = hashSize;
26+
this.buckets = new Bucket[hashSize];
27+
this.locks = new ReentrantLock[hashSize];
28+
for (int i = 0; i < hashSize; i++) {
29+
buckets[i] = new Bucket<>();
30+
locks[i] = new ReentrantLock();
31+
}
32+
}
33+
34+
/**
35+
* Computes the hash code for the specified key.
36+
* Null keys are hashed to bucket 0.
37+
*
38+
* @param key the key for which the hash code is to be computed
39+
* @return the hash code corresponding to the key
40+
*/
41+
private int computeHash(K key) {
42+
if (key == null) {
43+
return 0; // Use a special bucket (e.g., bucket 0) for null keys
44+
}
45+
int hash = key.hashCode() % hashSize;
46+
return hash < 0 ? hash + hashSize : hash;
47+
}
48+
49+
/**
50+
* Inserts the specified key-value pair into the hash map.
51+
* If the key already exists, the value is updated.
52+
*
53+
* @param key the key to be inserted
54+
* @param value the value to be associated with the key
55+
*/
56+
public void put(K key, V value) {
57+
int hash = computeHash(key);
58+
locks[hash].lock();
59+
try {
60+
buckets[hash].put(key, value);
61+
} finally {
62+
locks[hash].unlock();
63+
}
64+
}
65+
66+
/**
67+
* Retrieves the value associated with the specified key.
68+
*
69+
* @param key the key whose associated value is to be returned
70+
* @return the value associated with the specified key, or null if the key does not exist
71+
*/
72+
public V get(K key) {
73+
int hash = computeHash(key);
74+
locks[hash].lock();
75+
try {
76+
return buckets[hash].get(key);
77+
} finally {
78+
locks[hash].unlock();
79+
}
80+
}
81+
82+
/**
83+
* Removes the key-value pair associated with the specified key from the hash map.
84+
*
85+
* @param key the key whose key-value pair is to be removed
86+
*/
87+
public void remove(K key) {
88+
int hash = computeHash(key);
89+
locks[hash].lock();
90+
try {
91+
buckets[hash].remove(key);
92+
} finally {
93+
locks[hash].unlock();
94+
}
95+
}
96+
97+
/**
98+
* Checks if the hash map contains the specified key.
99+
*
100+
* @param key the key to check
101+
* @return true if the key exists, false otherwise
102+
*/
103+
public boolean containsKey(K key) {
104+
int hash = computeHash(key);
105+
locks[hash].lock();
106+
try {
107+
return buckets[hash].containsKey(key);
108+
} finally {
109+
locks[hash].unlock();
110+
}
111+
}
112+
113+
/**
114+
* A nested static class representing a bucket in the hash map.
115+
* Each bucket uses a linked list to store key-value pairs.
116+
*
117+
* @param <K> the type of keys maintained by this bucket
118+
* @param <V> the type of mapped values
119+
*/
120+
private static class Bucket<K, V> {
121+
private Node<K, V> head;
122+
123+
public void put(K key, V value) {
124+
Node<K, V> node = findNode(key);
125+
if (node != null) {
126+
node.value = value;
127+
} else {
128+
Node<K, V> newNode = new Node<>(key, value);
129+
newNode.next = head;
130+
head = newNode;
131+
}
132+
}
133+
134+
public V get(K key) {
135+
Node<K, V> node = findNode(key);
136+
return node != null ? node.value : null;
137+
}
138+
139+
public void remove(K key) {
140+
if (head == null) {
141+
return;
142+
}
143+
if ((key == null && head.key == null) || (head.key != null && head.key.equals(key))) {
144+
head = head.next;
145+
return;
146+
}
147+
Node<K, V> current = head;
148+
while (current.next != null) {
149+
if ((key == null && current.next.key == null) || (current.next.key != null && current.next.key.equals(key))) {
150+
current.next = current.next.next;
151+
return;
152+
}
153+
current = current.next;
154+
}
155+
}
156+
157+
public boolean containsKey(K key) {
158+
return findNode(key) != null;
159+
}
160+
161+
private Node<K, V> findNode(K key) {
162+
Node<K, V> current = head;
163+
while (current != null) {
164+
if ((key == null && current.key == null) || (current.key != null && current.key.equals(key))) {
165+
return current;
166+
}
167+
current = current.next;
168+
}
169+
return null;
170+
}
171+
}
172+
173+
/**
174+
* A nested static class representing a node in the linked list.
175+
*
176+
* @param <K> the type of key maintained by this node
177+
* @param <V> the type of value maintained by this node
178+
*/
179+
private static class Node<K, V> {
180+
private final K key;
181+
private V value;
182+
private Node<K, V> next;
183+
184+
public Node(K key, V value) {
185+
this.key = key;
186+
this.value = value;
187+
}
188+
}
189+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package com.thealgorithms.datastructures.hashmap.hashing;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
class ConcurrentHashMapTest {
8+
9+
@Test
10+
void testPutAndGet() {
11+
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>(10);
12+
map.put(1, "Value1");
13+
map.put(2, "Value2");
14+
map.put(3, "Value3");
15+
16+
assertEquals("Value1", map.get(1));
17+
assertEquals("Value2", map.get(2));
18+
assertEquals("Value3", map.get(3));
19+
assertNull(map.get(4)); // Non-existent key
20+
}
21+
22+
@Test
23+
void testUpdateValue() {
24+
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>(10);
25+
map.put(1, "Value1");
26+
map.put(1, "UpdatedValue1");
27+
28+
assertEquals("UpdatedValue1", map.get(1)); // Verify updated value
29+
}
30+
31+
@Test
32+
void testRemove() {
33+
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>(10);
34+
map.put(1, "Value1");
35+
map.put(2, "Value2");
36+
37+
map.remove(1);
38+
assertNull(map.get(1)); // Verify removal
39+
assertEquals("Value2", map.get(2)); // Ensure other keys are unaffected
40+
}
41+
42+
@Test
43+
void testContainsKey() {
44+
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>(10);
45+
map.put(1, "Value1");
46+
map.put(2, "Value2");
47+
48+
assertTrue(map.containsKey(1));
49+
assertTrue(map.containsKey(2));
50+
assertFalse(map.containsKey(3)); // Non-existent key
51+
}
52+
53+
@Test
54+
void testNullKey() {
55+
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(10);
56+
map.put(null, "NullValue");
57+
58+
assertEquals("NullValue", map.get(null)); // Verify null key handling
59+
map.remove(null);
60+
assertNull(map.get(null)); // Verify null key removal
61+
}
62+
63+
@Test
64+
void testConcurrency() throws InterruptedException {
65+
ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>(10);
66+
67+
Thread writer1 = new Thread(() -> {
68+
for (int i = 0; i < 50; i++) {
69+
map.put(i, i * 10);
70+
}
71+
});
72+
73+
Thread writer2 = new Thread(() -> {
74+
for (int i = 50; i < 100; i++) {
75+
map.put(i, i * 10);
76+
}
77+
});
78+
79+
Thread reader = new Thread(() -> {
80+
for (int i = 0; i < 100; i++) {
81+
map.get(i);
82+
}
83+
});
84+
85+
writer1.start();
86+
writer2.start();
87+
reader.start();
88+
89+
writer1.join();
90+
writer2.join();
91+
reader.join();
92+
93+
for (int i = 0; i < 100; i++) {
94+
assertEquals(i * 10, map.get(i));
95+
}
96+
}
97+
98+
@Test
99+
void testRemoveNonExistentKey() {
100+
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>(10);
101+
map.put(1, "Value1");
102+
map.remove(2); // Attempt to remove a non-existent key
103+
104+
assertEquals("Value1", map.get(1)); // Ensure existing key remains
105+
assertNull(map.get(2)); // Confirm non-existent key remains null
106+
}
107+
108+
@Test
109+
void testEmptyMap() {
110+
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>(10);
111+
assertNull(map.get(1)); // Test get on empty map
112+
assertFalse(map.containsKey(1)); // Test containsKey on empty map
113+
}
114+
115+
@Test
116+
void testMultipleThreadsSameKey() throws InterruptedException {
117+
ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>(10);
118+
119+
Thread writer1 = new Thread(() -> {
120+
for (int i = 0; i < 100; i++) {
121+
map.put(1, i);
122+
}
123+
});
124+
125+
Thread writer2 = new Thread(() -> {
126+
for (int i = 100; i < 200; i++) {
127+
map.put(1, i);
128+
}
129+
});
130+
131+
writer1.start();
132+
writer2.start();
133+
134+
writer1.join();
135+
writer2.join();
136+
137+
assertNotNull(map.get(1)); // Ensure key exists
138+
assertTrue(map.get(1) >= 0 && map.get(1) < 200); // Value should be within range
139+
}
140+
}

0 commit comments

Comments
 (0)