⚡ Advanced Concurrency · Advanced

Other concurrent collections in Java

CopyOnWriteArrayList, ConcurrentLinkedQueue, BlockingQueue producer-consumer.

🧩 The mysteryProducers create work faster than consumers can finish it. Without a plan the backlog grows until the heap explodes. The fix is a queue that knows how to say "wait".

CopyOnWriteArrayList

Every write copies the whole array. Iterators walk the array snapshot taken when the loop began: no locking, never a ConcurrentModificationException, never a change mid-loop. Reads are cheap, writes are costly: perfect for listener lists that are read constantly and changed rarely.

🔮 Predict it

Your turn

The loop adds to the list it's iterating. What prints?

var list = new CopyOnWriteArrayList<String>();
list.add("a");
list.add("b");
for (String s : list)
    list.add(s + s);
System.out.println(list.size());
  1. 4
  2. Throws ConcurrentModificationException
  3. Loops forever
Show the answer

4. The loop walks the snapshot [a, b], so it runs twice and adds "aa" and "bb". The new elements are really added, but this loop never visits them. With an ArrayList, this would throw.

A queue that never waits

ConcurrentLinkedQueue is a lock-free, unbounded FIFO queue. offer() adds; poll() removes the head, or returns **null when empty**. It never blocks. Good for handing off items when nobody needs to wait for them.

A queue that waits

A BlockingQueue adds **put(), which waits while the queue is full, and take(), which waits while it's empty. That's the backbone of producer-consumer. With capacity 2: put 1, put 2, put 3 waits**... a consumer's take() returns 1, frees a slot, and 3 goes in. Order stays FIFO.

BlockingQueue<Job> q =
    new ArrayBlockingQueue<>(100);
q.put(job);          // producer: waits if full
Job next = q.take(); // consumer: waits if empty

Unbounded vs bounded

✗ Unbounded
BlockingQueue<LogLine> q =
    new LinkedBlockingQueue<>();

The no-arg constructor is effectively unbounded. Fast producers fill the heap until OutOfMemoryError.

✓ Bounded
BlockingQueue<LogLine> q =
    new LinkedBlockingQueue<>(10_000);

When it's full, put() makes producers wait: natural back-pressure.

Pick the right tool

Listener lists (many reads, rare writes): CopyOnWriteArrayList. Bounded producer-consumer buffer: ArrayBlockingQueue. Unbounded, non-blocking FIFO: ConcurrentLinkedQueue. Sorted concurrent map: ConcurrentSkipListMap. Each trades something (write cost, blocking, ordering) for an access pattern.

💼 In the real world

The log shipper incident

A log shipper's producers outpaced its network consumer. They shared new LinkedBlockingQueue<>(), which kept growing until the app died with OutOfMemoryError. The fix was one argument: a capacity. Blocked producers are a visible slowdown; a dead JVM is an outage.

Key takeaways

  1. CopyOnWriteArrayList: cheap reads, costly writes, snapshot iterators
  2. ConcurrentLinkedQueue: lock-free; poll() returns null when empty
  3. BlockingQueue: put() waits when full, take() waits when empty
  4. A bounded queue gives natural back-pressure
🤯 Did you know?

LinkedBlockingQueue's no-arg constructor uses a capacity of Integer.MAX_VALUE: over 2.1 billion slots, unbounded for any practical purpose.

Practice questions

What does this print?

var list = new CopyOnWriteArrayList<>(List.of(1, 2));
for (int x : list) {
    list.add(x * 10);
}
System.out.println(list);
  1. [1, 2, 10, 20]
  2. Throws ConcurrentModificationException
  3. [1, 2, 10, 20, 100, 200]
  4. [1, 2]
Check your answer

[1, 2, 10, 20]. The loop iterates over the snapshot that existed when it started, so the new elements are added but never visited.

What does this print?

var q = new ArrayBlockingQueue<Integer>(2);
Thread producer = new Thread(() -> {
    for (int i = 1; i <= 3; i++) {
        try { q.put(i); }
        catch (InterruptedException e) { return; }
    }
});
producer.start();
System.out.println(q.take() + "" + q.take() + q.take());
  1. 123
  2. 12
  3. 321
  4. Throws IllegalStateException
Check your answer

123. The queue is FIFO. When it's full, the third put() simply waits until main's take() frees a slot, so all three values arrive in order.

Next: ready-made coordination tools (latches, barriers, semaphores, phasers) so you never hand-roll wait/notify again.