diff --git a/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/ConcurrentHashMap.java b/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/ConcurrentHashMap.java new file mode 100644 index 000000000000..8068402001f4 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/hashmap/hashing/ConcurrentHashMap.java @@ -0,0 +1,189 @@ +package com.thealgorithms.datastructures.hashmap.hashing; + +import java.util.concurrent.locks.ReentrantLock; + +/** + * A thread-safe implementation of a HashMap using separate chaining with linked lists + * and ReentrantLocks for concurrency control. + * + * @param the type of keys maintained by this map + * @param the type of mapped values + */ +@SuppressWarnings("rawtypes") +public class ConcurrentHashMap { + private final int hashSize; + private final Bucket[] buckets; + private final ReentrantLock[] locks; + + /** + * Constructs a ConcurrentHashMap with the specified hash size. + * + * @param hashSize the number of buckets in the hash map + */ + @SuppressWarnings("unchecked") + public ConcurrentHashMap(int hashSize) { + this.hashSize = hashSize; + this.buckets = new Bucket[hashSize]; + this.locks = new ReentrantLock[hashSize]; + for (int i = 0; i < hashSize; i++) { + buckets[i] = new Bucket<>(); + locks[i] = new ReentrantLock(); + } + } + + /** + * Computes the hash code for the specified key. + * Null keys are hashed to bucket 0. + * + * @param key the key for which the hash code is to be computed + * @return the hash code corresponding to the key + */ + private int computeHash(K key) { + if (key == null) { + return 0; // Use a special bucket (e.g., bucket 0) for null keys + } + int hash = key.hashCode() % hashSize; + return hash < 0 ? hash + hashSize : hash; + } + + /** + * Inserts the specified key-value pair into the hash map. + * If the key already exists, the value is updated. + * + * @param key the key to be inserted + * @param value the value to be associated with the key + */ + public void put(K key, V value) { + int hash = computeHash(key); + locks[hash].lock(); + try { + buckets[hash].put(key, value); + } finally { + locks[hash].unlock(); + } + } + + /** + * Retrieves the value associated with the specified key. + * + * @param key the key whose associated value is to be returned + * @return the value associated with the specified key, or null if the key does not exist + */ + public V get(K key) { + int hash = computeHash(key); + locks[hash].lock(); + try { + return buckets[hash].get(key); + } finally { + locks[hash].unlock(); + } + } + + /** + * Removes the key-value pair associated with the specified key from the hash map. + * + * @param key the key whose key-value pair is to be removed + */ + public void remove(K key) { + int hash = computeHash(key); + locks[hash].lock(); + try { + buckets[hash].remove(key); + } finally { + locks[hash].unlock(); + } + } + + /** + * Checks if the hash map contains the specified key. + * + * @param key the key to check + * @return true if the key exists, false otherwise + */ + public boolean containsKey(K key) { + int hash = computeHash(key); + locks[hash].lock(); + try { + return buckets[hash].containsKey(key); + } finally { + locks[hash].unlock(); + } + } + + /** + * A nested static class representing a bucket in the hash map. + * Each bucket uses a linked list to store key-value pairs. + * + * @param the type of keys maintained by this bucket + * @param the type of mapped values + */ + private static class Bucket { + private Node head; + + public void put(K key, V value) { + Node node = findNode(key); + if (node != null) { + node.value = value; + } else { + Node newNode = new Node<>(key, value); + newNode.next = head; + head = newNode; + } + } + + public V get(K key) { + Node node = findNode(key); + return node != null ? node.value : null; + } + + public void remove(K key) { + if (head == null) { + return; + } + if ((key == null && head.key == null) || (head.key != null && head.key.equals(key))) { + head = head.next; + return; + } + Node current = head; + while (current.next != null) { + if ((key == null && current.next.key == null) || (current.next.key != null && current.next.key.equals(key))) { + current.next = current.next.next; + return; + } + current = current.next; + } + } + + public boolean containsKey(K key) { + return findNode(key) != null; + } + + private Node findNode(K key) { + Node current = head; + while (current != null) { + if ((key == null && current.key == null) || (current.key != null && current.key.equals(key))) { + return current; + } + current = current.next; + } + return null; + } + } + + /** + * A nested static class representing a node in the linked list. + * + * @param the type of key maintained by this node + * @param the type of value maintained by this node + */ + private static class Node { + private final K key; + private V value; + private Node next; + + public Node(K key, V value) { + this.key = key; + this.value = value; + } + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/ConcurrentHashMapTest.java b/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/ConcurrentHashMapTest.java new file mode 100644 index 000000000000..dcccbf399771 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/hashmap/hashing/ConcurrentHashMapTest.java @@ -0,0 +1,140 @@ +package com.thealgorithms.datastructures.hashmap.hashing; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class ConcurrentHashMapTest { + + @Test + void testPutAndGet() { + ConcurrentHashMap map = new ConcurrentHashMap<>(10); + map.put(1, "Value1"); + map.put(2, "Value2"); + map.put(3, "Value3"); + + assertEquals("Value1", map.get(1)); + assertEquals("Value2", map.get(2)); + assertEquals("Value3", map.get(3)); + assertNull(map.get(4)); // Non-existent key + } + + @Test + void testUpdateValue() { + ConcurrentHashMap map = new ConcurrentHashMap<>(10); + map.put(1, "Value1"); + map.put(1, "UpdatedValue1"); + + assertEquals("UpdatedValue1", map.get(1)); // Verify updated value + } + + @Test + void testRemove() { + ConcurrentHashMap map = new ConcurrentHashMap<>(10); + map.put(1, "Value1"); + map.put(2, "Value2"); + + map.remove(1); + assertNull(map.get(1)); // Verify removal + assertEquals("Value2", map.get(2)); // Ensure other keys are unaffected + } + + @Test + void testContainsKey() { + ConcurrentHashMap map = new ConcurrentHashMap<>(10); + map.put(1, "Value1"); + map.put(2, "Value2"); + + assertTrue(map.containsKey(1)); + assertTrue(map.containsKey(2)); + assertFalse(map.containsKey(3)); // Non-existent key + } + + @Test + void testNullKey() { + ConcurrentHashMap map = new ConcurrentHashMap<>(10); + map.put(null, "NullValue"); + + assertEquals("NullValue", map.get(null)); // Verify null key handling + map.remove(null); + assertNull(map.get(null)); // Verify null key removal + } + + @Test + void testConcurrency() throws InterruptedException { + ConcurrentHashMap map = new ConcurrentHashMap<>(10); + + Thread writer1 = new Thread(() -> { + for (int i = 0; i < 50; i++) { + map.put(i, i * 10); + } + }); + + Thread writer2 = new Thread(() -> { + for (int i = 50; i < 100; i++) { + map.put(i, i * 10); + } + }); + + Thread reader = new Thread(() -> { + for (int i = 0; i < 100; i++) { + map.get(i); + } + }); + + writer1.start(); + writer2.start(); + reader.start(); + + writer1.join(); + writer2.join(); + reader.join(); + + for (int i = 0; i < 100; i++) { + assertEquals(i * 10, map.get(i)); + } + } + + @Test + void testRemoveNonExistentKey() { + ConcurrentHashMap map = new ConcurrentHashMap<>(10); + map.put(1, "Value1"); + map.remove(2); // Attempt to remove a non-existent key + + assertEquals("Value1", map.get(1)); // Ensure existing key remains + assertNull(map.get(2)); // Confirm non-existent key remains null + } + + @Test + void testEmptyMap() { + ConcurrentHashMap map = new ConcurrentHashMap<>(10); + assertNull(map.get(1)); // Test get on empty map + assertFalse(map.containsKey(1)); // Test containsKey on empty map + } + + @Test + void testMultipleThreadsSameKey() throws InterruptedException { + ConcurrentHashMap map = new ConcurrentHashMap<>(10); + + Thread writer1 = new Thread(() -> { + for (int i = 0; i < 100; i++) { + map.put(1, i); + } + }); + + Thread writer2 = new Thread(() -> { + for (int i = 100; i < 200; i++) { + map.put(1, i); + } + }); + + writer1.start(); + writer2.start(); + + writer1.join(); + writer2.join(); + + assertNotNull(map.get(1)); // Ensure key exists + assertTrue(map.get(1) >= 0 && map.get(1) < 200); // Value should be within range + } +} \ No newline at end of file