Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Java ConcurrentLinkedQueue: size is not an admission limit

Last updated: 1 Oct 20264 min read
tutorial
IntermediateBy AITrove Editorial

ConcurrentLinkedQueue is a nonblocking shared queue whose size calculation traverses elements and cannot reserve capacity for a producer.

Reserve before publishing

Checking size() and then calling offer() is two separate actions. Another producer can add between them. size() itself walks the queue and may report a count that changes during the walk. If a service must limit queued work, pair the queue with an explicit permit budget or use a bounded BlockingQueue. The example reserves one of two queued slots before publication and releases it only after poll removes an item.

The permit accounts for queued records, not work already removed by a consumer. That distinction matters if a worker performs long tasks after polling. For a limit on both queued and executing tasks, release the permit when processing finishes instead. Bounded blocking queues offer a simpler admission boundary when blocking semantics fit.

Read weak snapshots honestly

A ConcurrentLinkedQueue iterator can proceed while other threads modify the queue; it is weakly consistent, not a transactional snapshot. A monitoring dashboard may sample it, but a financial reconciliation cannot use one traversal as a complete inventory without another consistency boundary. Null elements are rejected, allowing poll to use null for emptiness.

The program is deliberately single-threaded so its output is reproducible. The synchronization property comes from the queue and semaphore contracts, not from a timing-dependent stress run.

Working program

Java
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Semaphore;

public class DispatchAdmissionGate {
    private final ConcurrentLinkedQueue<String> pending = new ConcurrentLinkedQueue<>();
    private final Semaphore queuedSlots = new Semaphore(2);

    boolean submit(String dispatchId) {
        if (!queuedSlots.tryAcquire()) return false;
        try {
            pending.add(dispatchId);
            return true;
        } catch (RuntimeException failure) {
            queuedSlots.release();
            throw failure;
        }
    }

    String take() {
        String dispatchId = pending.poll();
        if (dispatchId != null) queuedSlots.release();
        return dispatchId;
    }

    public static void main(String[] args) {
        DispatchAdmissionGate gate = new DispatchAdmissionGate();
        System.out.println(gate.submit("load-47"));
        System.out.println(gate.submit("load-82"));
        System.out.println(gate.submit("load-15"));
        System.out.println(gate.take());
        System.out.println(gate.submit("load-15"));
    }
}

Output

Output
true
true
false
load-47
true

Cost and ownership

Queue offer and poll have expected constant-time behavior; size is O(n) and unsuitable for hot-path admission. The semaphore retains a fixed permit count. This sample is an in-memory process boundary, not a durable queue: a crash loses pending IDs.

Common Mistakes

  • Do not use size() as a concurrent capacity check.
  • Do not release a permit before the item leaves the queue.
  • Do not treat a weak iterator as a complete audit snapshot.

Read next

Java blocking queues: bounded capacity and backpressure, Java semaphores: permit accounting and bounded access, Java ConcurrentHashMap: atomic updates and weakly consistent reads, Java bounded executors: test saturation and rejected work.

java
concurrentlinkedqueue-admission
Storage details