Backend Development 13 min read

Analyzing Java Concurrency Models: CopyOnWriteArrayList, ConcurrentHashMap, and LinkedBlockingQueue

This article examines three Java concurrency implementations—CopyOnWriteArrayList, ConcurrentHashMap, and LinkedBlockingQueue—explaining their underlying principles, code structures, and how they achieve thread‑safe read/write operations using techniques such as copy‑on‑write, CAS, and separate locks.

Java Captain
Java Captain
Java Captain
Analyzing Java Concurrency Models: CopyOnWriteArrayList, ConcurrentHashMap, and LinkedBlockingQueue

Multithreaded programming is a classic challenge; as the JDK evolves, it offers increasingly sophisticated concurrency models. This article selects three examples that use different principles and analyzes their core mechanisms.

CopyOnWriteArrayList (COW)

The COW model, derived from the Linux fork command, is implemented in Java by CopyOnWriteArrayList . Reads are lock‑free because modifications create a new array copy, allowing get to simply return the element:

public E get(int index) {
    return (E) elements[index];
}

Adding an element requires synchronization, but it works on a fresh copy of the underlying array:

public synchronized boolean add(E e) {
    Object[] newElements = new Object[elements.length + 1];
    System.arraycopy(elements, 0, newElements, 0, elements.length);
    newElements[elements.length] = e;
    elements = newElements;
    return true;
}

Iterators operate on a snapshot of the array, preventing ConcurrentModificationException and disallowing modifications:

public Iterator
iterator() {
    Object[] snapshot = elements;
    return new CowIterator
(snapshot, 0, snapshot.length);
}

The iterator’s mutating methods throw UnsupportedOperationException , reflecting the immutable snapshot semantics.

ConcurrentHashMap (CAS‑based)

CAS (Compare‑And‑Swap) provides atomic compare‑and‑replace operations, typically via the Unsafe class. In JDK 1.8, ConcurrentHashMap builds on this to achieve high‑performance concurrency while using a bucket array of Node objects that may become linked lists or red‑black trees.

Key low‑level functions that rely on Unsafe are:

static final
Node
tabAt(Node
[] tab, int i) {
    return (Node
)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
static final
boolean casTabAt(Node
[] tab, int i, Node
c, Node
v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
static final
void setTabAt(Node
[] tab, int i, Node
v) {
    U.putOrderedObject(tab, ((long)i << ASHIFT) + ABASE, v);
}

The simplified putVal method shows how these primitives avoid global locking, using CAS for empty slots and falling back to synchronized blocks only when necessary:

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    for (Node
[] tab = table;;) {
        Node
f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<>(hash, key, value, null)))
                break; // no lock when adding to empty bin
        } else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            synchronized (f) {
                // ... handle existing bin (list or tree) ...
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

The get method demonstrates a lock‑free read path that first checks the snapshot, then traverses linked lists or red‑black trees as needed.

public V get(Object key) {
    Node
[] tab; Node
e; int n, h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if (e.hash == h && (e.key == key || (e.key != null && key.equals(e.key))))
            return e.val;
        // handle tree or list traversal ...
    }
    return null;
}

LinkedBlockingQueue (Read‑Write Separation)

This blocking queue achieves thread safety by separating locks for producers and consumers. It uses a final head/tail node pair, an AtomicInteger count, and two ReentrantLock instances ( putLock and takeLock ) with associated conditions.

private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();

The offer method locks only putLock while adding an element, updating the count atomically, and signalling waiting consumers when the queue transitions from empty to non‑empty.

public boolean offer(E e) {
    if (e == null) throw new NullPointerException();
    final AtomicInteger count = this.count;
    if (count.get() == capacity) return false;
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        if (count.get() < capacity) {
            enqueue(new Node<>(e));
            int c = count.getAndIncrement();
            if (c + 1 < capacity) notFull.signal();
        }
    } finally {
        putLock.unlock();
    }
    if (c == 0) signalNotEmpty();
    return c >= 0;
}

The poll method similarly locks only takeLock to remove the head element, decrement the count, and signal producers when space becomes available.

public E poll() {
    final AtomicInteger count = this.count;
    if (count.get() == 0) return null;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        if (count.get() > 0) {
            E x = dequeue();
            int c = count.getAndDecrement();
            if (c > 1) notEmpty.signal();
            return x;
        }
    } finally {
        takeLock.unlock();
    }
    if (c == capacity) signalNotFull();
    return null;
}

Overall, these three models illustrate different strategies—copy‑on‑write, CAS‑based lock striping, and separate producer/consumer locks—to achieve efficient, thread‑safe operations in Java’s concurrent collections.

JavaconcurrencyThread SafetyCASConcurrentHashMapcopyonwritearraylistLinkedBlockingQueue
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.