Quick answer: yes, they are thread safe. But do not leave it there ...
Firstly, a small house, BlockingQueue is an interface, and any implementation that is not thread safe will violate the documented contract. The link you included was LinkedBlockingQueue to LinkedBlockingQueue , which has some trick.
The link you included makes an interesting observation, but there are two locks inside LinkedBlockingQueue . However, he does not understand that the edge case when a โsimpleโ implementation has foul is actually being processed, so the take and put methods are more complicated than you might expect at first.
LinkedBlockingQueue optimized to avoid using the same lock both in reading and writing, this reduces competition, however, for the correct behavior, it relies on the queue to be empty. When there are elements inside the queue, pressing and pop-points are not in the same memory area, and rivalry can be avoided. However, when the queue is empty, competition cannot be eliminated, so additional code is needed to handle this common โedgeโ. This is a common tradeoff between code complexity and performance / scalability.
The question then becomes, how LinkedBlockingQueue know when a queue is empty / not empty and thus processes threads? The answer is that it uses AtomicInteger and Condition as two additional parallel data structures. The AtomicInteger element AtomicInteger used to check if the queue length is zero, and the Condition is used to wait for a signal to notify the waiting thread when the queue is probably in the desired state. This additional coordination has overhead, however, in measurements it was shown that with an increase in the number of simultaneous flows, that the overhead of this method is lower than the competition, which is introduced using a single lock.
Below I copied the code from LinkedBlockingQueue and added comments explaining how they work. At a high level, take() first blocks all other calls to take() , and then put() signals as needed. put() works in a similar way, first it blocks all other put() calls, and then take() signals if necessary.
From the put() method:
// putLock coordinates the calls to put() only; further coordination // between put() and take() follows below putLock.lockInterruptibly(); try { // block while the queue is full; count is shared between put() and take() // and is safely visible between cores but prone to change between calls // a while loop is used because state can change between signals, which is // why signals get rechecked and resent.. read on to see more of that while (count.get() == capacity) { notFull.await(); } // we know that the queue is not full so add enqueue(e); c = count.getAndIncrement(); // if the queue is not full, send a signal to wake up // any thread that is possibly waiting for the queue to be a little // emptier -- note that this is logically part of 'take()' but it // has to be here because take() blocks itself if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } if (c == 0) signalNotEmpty();
From take()
takeLock.lockInterruptibly(); try { // wait for the queue to stop being empty while (count.get() == 0) { notEmpty.await(); } // remove element x = dequeue(); // decrement shared count c = count.getAndDecrement(); // send signal that the queue is not empty // note that this is logically part of put(), but // for thread coordination reasons is here if (c > 1) notEmpty.signal(); } finally { takeLock.unlock(); } if (c == capacity) signalNotFull();