2

LinkedBlocking Queue has two locks, one for putting, one for taking. When the size of the queue is 1, I think two threads can lock and manipulate the queue simultaneously, which will cause undefined behavior. Am I wrong?

// method put:                             // method take:             
// put lock                                // take lock
 putLocK.lockInterruptibly();              takeLock.lockInterruptibly();                      
 ...                                       ...

 while(count.get() == capacity){           while(count.get() == 0){
   notFull.await();                          notEmpty.await();
 }                                         }
 enqueue(node);                            x = dequeue();

// method enqueue:                         // method dequeue:
  last = last.next = node;                 Node<E> h = head;    
 ...                                       Node<E> first = h.next;
                                           h.next = h;        
                                           head = first;     
                                           E x = first.item;     
                                           first.item = null;   
                                           return x;

Clearly put thread and take thread can lock when there's only one item in queue, therefore they will execute codes in method enqueue and dequeue respectively. I mean if take thread enters method dequeue, after all that pointer modification, doesn't collide with the codes in enqueue?

Links here says "However when the queue is empty then the contention cannot be avoided, and so extra code is required to handle this common 'edge' case"

Is BlockingQueue completely thread safe in Java

asap diablo
  • 178
  • 2
  • 13
  • 1
    FYI: There is no "simultaneously" when you're talking about shared objects and a conventional multi-processor computer system. In order for threads to communicate via shared objects, they must read and write locations in the system's main memory. A conventional system has only one memory bus, and every read and every write must go over that bus in single-file. A big part of understanding multi-processor problems is understanding and anticipating the different ways in which read and write requests can be _serialized_ by the hardware when threads _attempt_ to use the bus at the same time. – Solomon Slow May 13 '19 at 15:25
  • *I think two threads can lock and manipulate the queue simultaneously...* No. The whole point of locking is that two threads can't lock simultaneously. – shmosel May 14 '19 at 02:21
  • @shmosel Two threads, two different lock. One modifies the head, one modifies the tail. LinkedBlockingQueue is designed for putting and taking at the same time. – asap diablo May 14 '19 at 02:22

3 Answers3

1

put implementation of a LinkedBlockingQueue

public void put(E e) throws InterruptedException {
    // some lock and node code

    // the part that matters here
    try {

        while (count.get() == capacity) {
            notFull.await();
        }

        // put the item in the queue.
    } finally {
        // not important here
    }
}

Basically, in put, the calling thread waits for the capacity to be less than the max continuing.

Even though the thread putting the value on the queue grabs a lock that is different from the take thread, it waits to add it to the queue until the queue is not full.

take has a similar implementation with regards to notEmpty instead of notFull.

Matt
  • 902
  • 7
  • 20
  • What Ivan says is wrong, LinkedBlockingQueue does have two locks. I just wanna know how can it be thread-safe when size is 1. – asap diablo May 13 '19 at 16:01
  • If they respectively grab their own lock, which is "putLock" and "takeLock". In theory, the two threads can put and take the only element at the same time. BUT in practice, no, why? – asap diablo May 13 '19 at 16:06
  • It cannot happen at the same time because `take` can't _take_ unless there __is__ an item in the queue, and `put` can't _put_ unless there are __no__ items in the queue. In the case of the size being 1, one of the two can't do anything until the other happens, which is why it is safe – Matt May 13 '19 at 16:12
  • @asapdiablo I added some edits to my answer to help clarify better. – Matt May 13 '19 at 16:23
  • If put only happens in queue empty, then thread safe can be guaranteed. – asap diablo May 13 '19 at 16:23
  • You are correct. Again, this is assuming the queue size is 1. – Matt May 13 '19 at 16:25
  • no! Put can happen when queue is not empty. If put can't happen then the throughput of queue will be very low. So there's another reason why is thread safe. – asap diablo May 13 '19 at 16:46
  • By `put` can happen, I am assuming you mean you can call `put` when the queue is not empty. This is true. But it will then block the calling thread and wait until the queue is empty before it puts the element in. – Matt May 13 '19 at 18:12
  • In the source code, the put thread will block only if "count == capacity". Then assume there's one item in the queue, and capacity is 5, now take and put can both get their locks. If my opinion is true, then how does the queue guarantees thread safe? – asap diablo May 14 '19 at 01:28
1

The javadoc for BlockingQueue (the superclass of LinkedBlockingQueue) states this:

BlockingQueue implementations are thread-safe. All queuing methods achieve their effects atomically using internal locks or other forms of concurrency control.

The word "atomically" means that if two operations (for example a put and a take) happen simultaneously, then the implementation will ensure that they behave according to the contract. The effect will be as if the put happens before get or vice-versa. That applies to edge-cases as well, such as your example of a queue with one element.

In fact, since put and get are blocking operations, the relative ordering of the two operations won't matter. With offer / poll or add / remove the order does matter, but you can't control it.


Please note that the above is based solely on what the javadoc says. Assuming that I have interpreted the javadoc correctly, then it applies to all1 BlockingQueue implementations, irrespective of whether they use one or two locks ... or none at all. If a BlockingQueue implementation doesn't behave as above, that is a bug!

1 - All implementations that implement the API correctly. That should cover all of the Java SE classes.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • The doc itself only defines the behavior, I just wanna know how the implementation guarantees thread-safe when size = 1. – asap diablo May 13 '19 at 16:04
  • "I just wanna know how the implementation guarantees thread-safe when size = 1." - It is thread-safe. The javadoc says so, and if there was an implementation bug that caused it to not be thread-safe, it would have been found *years ago*. – Stephen C May 13 '19 at 22:38
0

After 2 days of search, I finally get it... When the queue has only one item, according to LinkedBlocking Queue's design, there are actually two nodes: the dummy head and really item(meanwhile last points to it). It's true that put thread and take thread can both get their lock, but they modify different parts of the queue.

Put thread will call

last = last.next = node; // last points to the only item in queue

Take thread will call

Node<E> h = head;
Node<E> first = h.next;  // first also points to the only item in queue
h.next = h; 
head = first;
E x = first.item;
first.item = null;
return x;

The intersection of these two threads is what last points to in put thread and what first points to in take thread. Notice that put thread only modifies last.item and take thread only modifies first.next. Although these two threads modifies the same object instance, they modifies the different member of it, and it won't bring about any thread conflicts.

asap diablo
  • 178
  • 2
  • 13