Concurrent Collections

Thread-safe collection — ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue

Read: ~28 min Advanced 5 practice problems Live code runner

1. Why Not Just synchronized a HashMap?

You could wrap a HashMap with Collections.synchronizedMap, but every single operation then grabs one global lock — turning a hot data structure into a bottleneck. Worse, HashMap itself is not safe for concurrent writes even to different keys — you can corrupt its internal arrays. java.util.concurrent gives you purpose-built collections that are fast and safe.

Collections.synchronizedMap দিয়ে wrap করলেও পুরো map-এর উপর একটি global lock পড়ে — bottleneck। আর HashMap-এ আলাদা key-তে concurrent write-ই internal array corrupt করতে পারে। java.util.concurrent package দ্রুত এবং নিরাপদ — দুই-ই।
Rule: if more than one thread touches a collection, reach for a concurrent implementation. Collections.synchronizedX should almost never appear in new code.

2. ConcurrentHashMap

The flagship. Internally divided into segments with separate locks (historically) or lock-free tree bins (Java 8+). Multiple threads can read and write concurrently without contending on a single lock. getOrDefault, computeIfAbsent, merge, and forEach are all atomic at the entry level.

Flagship collection। ভেতরে একাধিক segment বা lock-free tree bin (Java 8+) — একাধিক thread একসাথে read-write করতে পারে। computeIfAbsent, merge, forEach সব entry-level atomic।
Main.java
import java.util.*;
import java.util.concurrent.*;

class Main {
    public static void main(String[] args) throws Exception {
        ConcurrentHashMap<String, Integer> hits = new ConcurrentHashMap<>();
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                hits.merge("home", 1, Integer::sum);
            }
        };
        Thread a = new Thread(task), b = new Thread(task);
        a.start(); b.start();
        a.join();  b.join();
        System.out.println("home hits = " + hits.get("home"));  // 2000
    }
}

3. CopyOnWriteArrayList

Every mutation copies the entire backing array. That is expensive for writes and free for reads — iterators never see a mutation mid-flight. Use it when reads vastly outnumber writes: event listeners, rarely-changing config.

প্রতিটি mutation পুরো array কপি করে — write ব্যয়বহুল কিন্তু read সম্পূর্ণ free ও consistent। read >> write এমন জায়গায় ব্যবহার করুন — event listener, rarely-changing config।
Main.java
import java.util.concurrent.*;

class Main {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> listeners = new CopyOnWriteArrayList<>();
        listeners.add("metrics");
        listeners.add("audit");

        // iteration is safe even if another thread adds during the loop
        for (String l : listeners) {
            System.out.println("notify " + l);
            listeners.add("logger");  // no ConcurrentModificationException
        }
        System.out.println("final = " + listeners);
    }
}

4. BlockingQueue — The Producer/Consumer Pattern

A BlockingQueue is the textbook channel between threads. Producers call put (blocks if full), consumers call take (blocks if empty). Common implementations: ArrayBlockingQueue (bounded), LinkedBlockingQueue (optionally bounded).

BlockingQueue — thread-এর মধ্যে channel। producer put (full হলে block), consumer take (empty হলে block)। ArrayBlockingQueue (bounded), LinkedBlockingQueue (optionally bounded)।
Main.java
import java.util.concurrent.*;

class Main {
    public static void main(String[] args) throws Exception {
        BlockingQueue<Integer> q = new ArrayBlockingQueue<>(3);

        Thread producer = new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    q.put(i);
                    System.out.println("put " + i);
                }
                q.put(-1);  // sentinel
            } catch (InterruptedException e) {}
        });

        Thread consumer = new Thread(() -> {
            try {
                while (true) {
                    int v = q.take();
                    if (v == -1) break;
                    System.out.println("got " + v);
                }
            } catch (InterruptedException e) {}
        });

        producer.start(); consumer.start();
        producer.join();  consumer.join();
    }
}
Producer → BlockingQueue → Consumer Producer put(item) BlockingQueue(capacity 3) [ 1 | 2 | 3 ] Consumer take() Figure 40.1 — producer full হলে block, consumer empty হলে block।

5. ConcurrentLinkedQueue — Lock-Free

A non-blocking, lock-free queue based on CAS. It does not block when empty (poll returns null) — useful when the consumer wants to do other work instead of waiting.

CAS-ভিত্তিক lock-free queue। empty হলে block করে না — poll null ফেরত দেয়। consumer অপেক্ষার বদলে অন্য কাজ করতে চাইলে ভালো।
Main.java
import java.util.concurrent.*;

class Main {
    public static void main(String[] args) {
        ConcurrentLinkedQueue<String> q = new ConcurrentLinkedQueue<>();
        q.offer("a"); q.offer("b"); q.offer("c");

        String v;
        while ((v = q.poll()) != null) System.out.println(v);
        System.out.println("empty: " + q.isEmpty());
    }
}

6. Picking the Right Collection

NeedUseবাংলায়
Shared key-value cacheConcurrentHashMapshared key-value cache
Read-mostly listCopyOnWriteArrayListবেশি read কম write তালিকা
Bounded producer/consumerArrayBlockingQueuebounded producer-consumer
Unbounded producer/consumerLinkedBlockingQueueunbounded producer-consumer
Lock-free FIFOConcurrentLinkedQueuelock-free FIFO
Ordered set / mapConcurrentSkipListMap / Setsorted concurrent map/set
Delayed tasksDelayQueueনির্দিষ্ট delay-এর পর available

✅ Do

  • Use concurrent collections in multi-threaded code
  • Prefer computeIfAbsent/merge over get+put
  • Bound your queues in production

⚠️ Avoid

  • Collections.synchronizedMap in hot paths
  • Compound read-modify-write with plain HashMap
  • Unbounded queues — they hide producer/consumer imbalance

7. Vocabulary

TermMeaningবাংলায়
ConcurrentHashMapHigh-throughput thread-safe map.high-throughput thread-safe map।
Copy-on-writeEach mutation copies the underlying array.প্রতিটি mutation array কপি করে।
BlockingQueueQueue where put/take can block on full/empty.full/empty-তে block-able queue।
Lock-freeUses CAS instead of mutex locks.mutex-এর বদলে CAS।
Producer-consumerPattern where one thread produces items another consumes.এক thread উৎপাদন, অন্যটি ব্যবহার।
Back-pressureSlowing the producer when the consumer is behind.consumer slow হলে producer-ও slow।

8. Practice Problems

  1. Count word frequencies in a list from two threads using ConcurrentHashMap.merge.
    দুটি thread থেকে word frequency গুনতে ConcurrentHashMap.merge ব্যবহার করুন।
    Show Answer (উত্তর দেখুন)
    Main.java
    import java.util.*;
    import java.util.concurrent.*;
    class Main {
        public static void main(String[] args) throws Exception {
            ConcurrentHashMap<String, Integer> m = new ConcurrentHashMap<>();
            List<String> words = List.of("a","b","a","c","b","a");
            Runnable r = () -> words.forEach(w -> m.merge(w, 1, Integer::sum));
            Thread t1 = new Thread(r), t2 = new Thread(r);
            t1.start(); t2.start(); t1.join(); t2.join();
            System.out.println(m);
        }
    }
  2. Explain in 2 sentences when CopyOnWriteArrayList is a good fit.
    দুই বাক্যে বলুন — CopyOnWriteArrayList কখন ভালো।
    Show Answer (উত্তর দেখুন)

    Answer: It is ideal when reads vastly outnumber writes and iterations must never see mid-write state — event listener lists, rarely-updated configuration, observer registrations. The copy-per-write cost makes it a bad choice for any list that changes on every request.

    read >> write এবং iteration-এ mid-write state না দেখতে হলে উপযুক্ত — event listener, config, observer list। প্রতিটি request-এ পাল্টায় এমন list-এ খারাপ।

  3. Implement a producer/consumer using LinkedBlockingQueue with 3 items.
    LinkedBlockingQueue দিয়ে ৩টি item-এর producer/consumer বানান।
    Show Answer (উত্তর দেখুন)
    Main.java
    import java.util.concurrent.*;
    class Main {
        public static void main(String[] args) throws Exception {
            BlockingQueue<String> q = new LinkedBlockingQueue<>();
            Thread p = new Thread(() -> {
                try {
                    q.put("rice"); q.put("oil"); q.put("salt"); q.put("STOP");
                } catch (InterruptedException e) {}
            });
            Thread c = new Thread(() -> {
                try {
                    while (true) {
                        String s = q.take();
                        if (s.equals("STOP")) break;
                        System.out.println("consumed " + s);
                    }
                } catch (InterruptedException e) {}
            });
            p.start(); c.start();
            p.join();  c.join();
        }
    }
  4. Cache a computed value per key with computeIfAbsent.
    computeIfAbsent দিয়ে key-ভিত্তিক cache বানান।
    Show Answer (উত্তর দেখুন)
    Main.java
    import java.util.concurrent.*;
    class Main {
        public static void main(String[] args) {
            ConcurrentHashMap<Integer, Long> cache = new ConcurrentHashMap<>();
            for (int i : new int[]{5, 10, 5, 10, 15}) {
                long v = cache.computeIfAbsent(i, k -> {
                    System.out.println("  computing " + k);
                    return (long) k * k;
                });
                System.out.println(i + " -> " + v);
            }
        }
    }
  5. In 3 sentences, say why Collections.synchronizedMap is a bottleneck compared to ConcurrentHashMap.
    তিন বাক্যে বলুন — Collections.synchronizedMap কেন ConcurrentHashMap-এর চেয়ে ধীর।
    Show Answer (উত্তর দেখুন)

    Answer: Collections.synchronizedMap wraps every operation — including pure reads — in a single lock on the whole map, so concurrent readers serialize one after another. ConcurrentHashMap partitions the data and uses fine-grained or lock-free updates per bucket, allowing many threads to operate in parallel without contention. On typical workloads the difference is an order of magnitude, and it grows with core count.

    Collections.synchronizedMap প্রতিটি operation (pure read সহ) পুরো map-এর single lock-এ চালায় — concurrent reader-রাও serial হয়ে যায়। ConcurrentHashMap ভেতরে partition + fine-grained/lock-free — অনেক thread parallel-এ কাজ করে। core বাড়ালে পার্থক্য আরো বাড়ে।

Summary — Module 40

For threaded code, reach for java.util.concurrent collections. ConcurrentHashMap is the go-to shared map; CopyOnWriteArrayList is a read-mostly list; BlockingQueue is the producer/consumer channel. They are faster and safer than wrapping plain collections with Collections.synchronizedX.

multi-thread কোডে java.util.concurrent collection ব্যবহার করুন। ConcurrentHashMap = shared map, CopyOnWriteArrayList = read-mostly list, BlockingQueue = producer/consumer channel। Collections.synchronizedX-এর চেয়ে দ্রুত ও নিরাপদ।

Next Module → Java Memory Model ও happens-before।