Is BlockingQueue completely thread safe in Java

前端 未结 5 1719
不思量自难忘°
不思量自难忘° 2020-12-04 22:32

I know that the documentation says that the object is thread safe but does that mean that all access to it from all methods are thread safe? So if I call put()

相关标签:
5条回答
  • 2020-12-04 22:34

    Yes, all implementations of BlockingQueue are thread safe for put and take and all actions.

    The link just goes halfway...and is not covering the full details. It is thread safe.

    0 讨论(0)
  • 2020-12-04 22:35

    I think @Chris K has missed some points. "When the queue has elements within it, then the push and the pop points are not at the same region of memory and contention can be avoided. ", notice that when the queue has one element, head.next and tail points to the same node and put() and take() can both get locks and execute.

    I think empty and full condition can be solved by synchronized put() and take(). However when it comes to one element, the lb queue has a null dummy head node, which may has something to do with the thread safety.

    0 讨论(0)
  • 2020-12-04 22:41

    That answer is a little strange - for a start, BlockingQueue is an interface so it doesn't have any locks. Implementations such as ArrayBlockingQueue use the same lock for add() and take() so would be fine. Generally, if any implementation is not thread safe then it is a buggy implementation.

    0 讨论(0)
  • 2020-12-04 22:46

    The quick answer is yes, they are thread safe. But lets not leave it there ...

    Firstly a little house keeping, BlockingQueue is an interface, and any implementation that is not thread safe will be breaking the documented contract. The link that you included was referring to LinkedBlockingQueue, which has some cleverness to it.

    The link that you included makes an interesting observation, yes there are two locks within LinkedBlockingQueue. However it fails to understand that the edge case that a 'simple' implementation would have fallen foul of was in-fact being handled, which is why the take and put methods are more complicated than one would at first expect.

    LinkedBlockingQueue is optimized to avoid using the same lock on both reading and writing, this reduces contention however for correct behavior it relies on the queue not being empty. When the queue has elements within it, then the push and the pop points are not at the same region of memory and contention can be avoided. However when the queue is empty then the contention cannot be avoided, and so extra code is required to handle this common 'edge' case. This is a common trade off between code complexity and performance/scalability.

    The question then follows, how does LinkedBlockingQueue know when the queue is empty/not empty and thus handle the threading then? The answer is that it uses an AtomicInteger and a Condition as two extra concurrent data structures. The AtomicInteger is used to check whether the length of the queue is zero and the Condition is used to wait for a signal to notify a waiting thread when the queue is probably in the desired state. This extra coordination does have an overhead, however in measurements it has been shown that when ramping up the number of concurrent threads that the overheads of this technique are lower than the contention that is introduced by using a single lock.

    Below I have copied the code from LinkedBlockingQueue and added comments explaining how they work. At a high level, take() first locks out all other calls to take() and then signals put() as necessary. put() works in a similar way, first it blocks out all other calls to put() and then signals take() 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();
    
    0 讨论(0)
  • 2020-12-04 22:46

    I tried this implementation on Leetcode import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingDeque;

    class FooBar {
    
        private final BlockingQueue<Object> line =  new LinkedBlockingDeque<>(1);
        private static final Object PRESENT = new Object();
        private int n;
    
        public FooBar(int n) {
            this.n = n;
        }
    
        public void foo(Runnable printFoo) throws InterruptedException {
    
            for (int i = 0; i < n; i++) {
                line.put(PRESENT);
                // printFoo.run() outputs "foo". Do not change or remove this line.
                printFoo.run();
            }
        }
    
        public void bar(Runnable printBar) throws InterruptedException {
    
            for (int i = 0; i < n; i++) {
                line.take();
                // printBar.run() outputs "bar". Do not change or remove this line.
                printBar.run();
            }
        }
    }
    

    With n = 3, mosttimes I get a correct response of foobarfoobarfoorbar but sometimes I get barbarfoofoofoobar which is quite surprising. I resolved to use using ReentrantLock and Condition, @chris-k can you shed more light

    0 讨论(0)
提交回复
热议问题