Standard collections like ArrayList and HashMap are not thread-safe. The java.util.concurrent package provides concurrent alternatives designed for multi-threaded access.

ConcurrentHashMap

Thread-safe hash map with fine-grained locking (since Java 8, uses CAS and synchronized buckets):

  ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

map.put("Alice", 1);
map.putIfAbsent("Bob", 2);
map.compute("Alice", (key, val) -> val == null ? 1 : val + 1);
map.merge("Charlie", 1, Integer::sum);

// Thread-safe iteration — weakly consistent (no ConcurrentModificationException)
map.forEach((k, v) -> System.out.println(k + " = " + v));
  

Atomic Compound Operations

  // ❌ Not atomic — race condition
if (!map.containsKey(key)) {
    map.put(key, computeValue());
}

// ✅ Atomic
map.computeIfAbsent(key, k -> computeValue());
  

CopyOnWriteArrayList

Creates a new copy of the underlying array on every write — ideal for read-heavy, write-rare scenarios:

  CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("Alice");
list.add("Bob");

// Safe iteration even if list is modified during iteration
for (String name : list) {
    list.add("Charlie"); // no ConcurrentModificationException
}
  

Trade-off: Writes are expensive (full array copy). Use when reads vastly outnumber writes (e.g., listener lists).

CopyOnWriteArraySet

Same copy-on-write semantics for a thread-safe set.

BlockingQueue

Supports blocking put and take operations — the foundation of producer-consumer patterns:

  BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);

// Producer
queue.put("task-1"); // blocks if queue is full

// Consumer
String task = queue.take(); // blocks if queue is empty
  

Implementations

Class Bounded Notes
ArrayBlockingQueue Yes Array-backed, single lock
LinkedBlockingQueue Optional Linked nodes, two locks
PriorityBlockingQueue No Priority ordering
SynchronousQueue Zero capacity Direct handoff
DelayQueue No Elements available after delay

Producer-Consumer Example

  BlockingQueue<Task> queue = new LinkedBlockingQueue<>(50);

// Producers
executor.submit(() -> {
    while (running) {
        queue.put(generateTask());
    }
});

// Consumers
for (int i = 0; i < 4; i++) {
    executor.submit(() -> {
        while (running) {
            Task task = queue.take();
            process(task);
        }
    });
}
  

ConcurrentLinkedQueue

Unbounded non-blocking queue using CAS operations:

  ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
queue.offer("item");
String item = queue.poll();
  

Higher throughput than BlockingQueue when blocking is not needed.

ConcurrentSkipListMap / ConcurrentSkipListSet

Sorted concurrent map and set:

  ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>();
map.put(3, "three");
map.put(1, "one");
map.headMap(3); // views are thread-safe
  

Choosing the Right Collection

Scenario Collection
Shared cache / map ConcurrentHashMap
Listener list (few writes) CopyOnWriteArrayList
Task queue with backpressure ArrayBlockingQueue
High-throughput handoff SynchronousQueue
Sorted concurrent map ConcurrentSkipListMap
Lock-free queue ConcurrentLinkedQueue

Best Practices

  • Prefer concurrent collections over wrapping with Collections.synchronizedMap()
  • Use atomic methods (computeIfAbsent, merge) instead of check-then-act
  • Do not use ConcurrentHashMap with null keys or values — they throw NPE
  • Choose CopyOnWrite* only for read-dominated workloads