Assumming that the head of the queue is a sentinel node.

Composition
Use permits.getAndIncrement/Decrement to modify the size of queue.
Each lock has an associated condition:
enqLock is used to notify waiting enqueuers when the queue is no longer full;deqLock is used to notify waiting dequeuers when the queue is no longer empty.<aside> 🔍 Lost Wakeup
Consider the following scenario:
Consumers A and B both try to dequeue an item from an empty queue, both detect the queue is empty, and both block on the notEmpty condition.
Producer C enqueues an item in the buffer, and signals notEmpty, waking A. Before A can acquire the lock, however, another producer D puts a second item in the queue, and because the queue is not empty, it does not signal notEmpty.
Then A acquires the lock and removes the first item, but B, victim of a lost wakeup, waits forever, even though there is an item in the queue to be consumed.
Solution: always use signalAll and notifyAll
</aside>
In this approach, the enq() and deq() share no locks, but do share an atomic counter.
Therefore, we can split counter:
enqSidePermitsdeqSidePermitsdeqLock and transfers permitsdeqLock and enqLockEliminate permits, the enq() method always enqueues its item, and deq() throws EmptyException if there is no item to dequeue.