Java Concurrent Collections Tutorial: Runnable Map, List, and Queue Examples

Choose a Java concurrent collection from its access pattern, then run three deterministic examples that show atomic updates, snapshot traversal, and bounded hand-off.

KnowledgeGate Team

Exam prep & CS education

Updated 29 Aug 20265 min read

Two threads can perform valid operations on a shared HashMap or ArrayList, yet their combined result can still be wrong and iteration unsafe. Collection choice depends on the access pattern, and operation-level guarantees do not make a sequence of operations indivisible.

What a concurrent collection actually protects

A concurrent collection coordinates its documented individual operations while several threads access it. It does not automatically make a sequence such as "check, then update" one indivisible action.

Suppose four workers record 20 tags across java, dsa, and sql. An ordinary HashMap needs externally controlled access. Collections.synchronizedMap adds synchronisation, but its API contract still requires care during iteration. ConcurrentHashMap provides atomic operations such as putIfAbsent, compute, and merge.

Choose by operation pattern, not a familiar class name. Thread-safe storage is only one part of thread-safe design.

Choose the collection from the access pattern

Access pattern

Suitable collection

Main trade-off

Shared key/value updates

ConcurrentHashMap

Strong atomic per-key operations, but a multi-key workflow is not one transaction.

Small list with very frequent traversal and rare changes

CopyOnWriteArrayList

Snapshot traversal is convenient, while every structural change copies the backing array.

Unbounded non-blocking FIFO hand-off

ConcurrentLinkedQueue

Producers do not wait for capacity, so the queue supplies no backpressure.

Bounded producer/consumer hand-off

ArrayBlockingQueue

Capacity limits memory growth and can make producers wait.

Sorted concurrent keys

ConcurrentSkipListMap

Keys stay ordered, with more machinery than an unordered map.

The Coding & DSA category connects these choices to their underlying data structures.

Decision tree mapping shared-access patterns to the matching Java concurrent collection.

Count 20 tags with ConcurrentHashMap

Each worker receives five tags, and merge atomically updates one key.

import java.util.*;
import java.util.concurrent.*;

public class ConcurrentTagCounter {
    public static void main(String[] args) throws Exception {
        List<List<String>> inputs = List.of(
            List.of("java", "dsa", "java", "sql", "java"),
            List.of("dsa", "java", "dsa", "sql", "java"),
            List.of("java", "dsa", "sql", "sql", "dsa"),
            List.of("dsa", "java", "dsa", "java", "sql")
        );
        ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
        ExecutorService executor = Executors.newFixedThreadPool(4);
        List<Future<?>> tasks = new ArrayList<>();

        for (List<String> input : inputs) {
            tasks.add(executor.submit(() ->
                input.forEach(tag -> counts.merge(tag, 1, Integer::sum))));
        }
        for (Future<?> task : tasks) task.get();
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
        System.out.println(new TreeMap<>(counts));
    }
}

Count before trusting the output. java = 3 + 2 + 1 + 2 = 8; dsa = 1 + 2 + 2 + 2 = 7; sql = 1 + 1 + 2 + 1 = 5. Check: 8 + 7 + 5 = 20. Output:

{dsa=7, java=8, sql=5}

The TreeMap only makes display order deterministic. merge is atomic per key, not a transaction across unrelated keys. Separate get and put calls can let two workers read the same old count and overwrite one increment. If needed, learn the language and collections foundation first.

Traverse a snapshot with CopyOnWriteArrayList

Under the Java API contract, a CopyOnWriteArrayList iterator sees the array snapshot present when it was created.

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

public class ListenerSnapshots {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> listeners =
            new CopyOnWriteArrayList<>(List.of("email", "sms"));
        Iterator<String> eventOne = listeners.iterator();
        listeners.add("audit");

        List<String> first = new ArrayList<>();
        eventOne.forEachRemaining(first::add);
        System.out.println("event 1 -> " + first);
        System.out.println("event 2 -> " + new ArrayList<>(listeners));
    }
}

Output:

event 1 -> [email, sms]
event 2 -> [email, sms, audit]

Its iterator does not support mutation. This fits a short listener or configuration list read thousands of times between rare changes. It is poor for a write-heavy cart, log, or task list because every structural update copies the backing array. It is not a generally faster ArrayList.

Build a bounded producer-consumer hand-off

An ArrayBlockingQueue combines FIFO hand-off with capacity. One producer supplies values and a sentinel, while one consumer squares positive values.

import java.util.*;
import java.util.concurrent.*;

public class BoundedSquares {
    public static void main(String[] args) throws Exception {
        ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);
        List<Integer> result = new ArrayList<>();
        ExecutorService executor = Executors.newFixedThreadPool(2);

        Future<?> producer = executor.submit(() -> {
            try {
                for (int value : List.of(12, 7, 25, -1)) queue.put(value);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        Future<?> consumer = executor.submit(() -> {
            try {
                for (int value; (value = queue.take()) != -1; )
                    result.add(value * value);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        producer.get();
        consumer.get();
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
        System.out.println(result);
    }
}

FIFO order gives [144, 49, 625] because 12 x 12 = 144, 7 x 7 = 49, and 25 x 25 = 625. If the consumer has not removed an item, the fourth put waits while three slots are occupied. Interleaving remains scheduler-dependent.

put and take wait when needed; offer and poll provide non-waiting or timed alternatives. A sentinel works only when its marker cannot be valid data and every consumer receives a termination signal.

Producer-consumer timeline for an ArrayBlockingQueue of capacity 3, ending with squared results 144, 49, and 625.

Five traps that survive a thread-safe collection

  1. containsKey then put: another thread can act between the calls. Use putIfAbsent, computeIfAbsent, or merge.

  2. isEmpty then remove: the state can change after the check. Use one queue operation such as poll or take.

  3. Treating map traversal as frozen: ConcurrentHashMap traversal is weakly consistent by its API contract. Accept that view or create an explicit snapshot when the workflow needs one.

  4. Frequent writes to CopyOnWriteArrayList: repeated copying is the wrong access model. Choose a structure designed for the real mutation pattern.

  5. Updating several collections as one action: a safe container does not make the group atomic. Use explicit coordination or redesign state ownership. Process Synchronization and Semaphores explains that wider critical-section problem.

Check null rules class by class. ConcurrentHashMap rejects null keys and values; ArrayBlockingQueue and ConcurrentLinkedQueue reject null elements.

How interviews and objective questions test the topic

Interview and objective questions test guarantees rather than names:

  • 10,000 traversals between rare listener changes: choose CopyOnWriteArrayList because snapshot reads dominate and copying is rare.

  • containsKey(k) followed by put(k, 1): replace the compound sequence with putIfAbsent or merge, depending on the intended update.

  • An iterator is created before adding audit: predict [email, sms] because that iterator retains its creation-time snapshot.

For related concurrency revision, work through Threads and Process Creation MCQs. KnowledgeGate also has 300+ Java questions available for broader practice.

Try a 20-minute loop: classify four scenarios for 5 minutes, type and run an example for 10, then rewrite one compound update with an atomic API for 5.

Short version and next step

  • ConcurrentHashMap handles shared mappings and atomic per-key updates.

  • CopyOnWriteArrayList handles read-heavy snapshot traversal.

  • ConcurrentLinkedQueue handles unbounded non-blocking FIFO access.

  • A BlockingQueue implementation handles bounded hand-off and backpressure.

None of them makes a multi-step business operation atomic by itself.

Add one more java entry to W1. The result becomes {dsa=7, java=9, sql=5}, with 7 + 9 + 5 = 21. Then change the queue capacity from 3 to 2. Waiting may change, but FIFO output stays [144, 49, 625]. Continue with the DSA using Java course for Java implementations and interview-oriented practice.