LinkedBlockingQueue多线程安全的保障
相信看过我的ArrayBlockingQueue的博客,对于我们分析LinkedBlockingQueue会有一定的帮助,这两个阻塞队列也可以作为我们阻线程池中阻塞对列的选择。
LinkedBlockingQueue采用的是带头节点链表的数据结构。
我们来通过这个类的成员后具体的代码来进行分析LinkedBlockingQueue所采用的实现多线程安全的手段是什么。
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
//用来表示链表的容量大小,如果用户不进行设置的话,那么 capacity = Integer.MAX_VALUE
private final int capacity;
//这个AtomicInteger 类中有一个重要的成员private volatile int value;
//采用这个 AtomicInteger 中的value进行记录当前元素的数量,
// AtomicInteger 中对value值进行更改的时候,采用的是CAS进行更改,
//所以在 LinkedBlockingQueue中调用AtomicIntege的方法虽value进行更改的话是可以保证线程安全的,因为1.value采用类volatile保证了其值的可见性,
//2是因为采用CAS的形式对其值进行更改,保证其原子性。
private final AtomicInteger count = new AtomicInteger();
//用来记录头节点
transient Node<E> head;
//用来记录尾结点
private transient Node<E> last;
//对于LinkedBlockingQueue来说我们采用的是两个ReentrantLock 的对象
//分别为takeLock 和 putlock,将添加和拿出的操作进行区分。
//notEmpty 为 takeLock的阻塞队列。
//notFull 为 putlock的阻塞队列
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();
从上边的成员的介绍,你们可能知道了,关于记录阻塞队列中的元素个数,我们知道是线程安全的因为采用了Volitile和CAS,才保证的count个数具有准确定,
但你们一定很迷惑,为什么head和last这两个成员没有任何的修饰,虽然count的准确性保证了,但对于头节点和尾结点的准确性是如何保障的?
我们都知道在进行删除或添加的时候,都需要对头节点和尾结点进行更改
那我们举个例子,我们采用普通的链表的删除和添加的方法
当一个线程想要删除的时候,那么会将head的值先copy一份,然后当线程的时间片段到了,而此时刚好有个线程想要添加,那么此时这个线程会先将tail的值copy一份,然后时间片段到了,想要删除的线程抢占到cpu资源开始执行删除操作,那么将会使头节点的后驱节点设为null,那就说明tail这个节点不会再被引用了,然后当删除线程进行删除完后,添加节点的线程继续添加,因为添加线程已经将tail的值copy一份,所以线程还是一句当前的tail的值进行添加,那么这个tail还是没有删除前的那个tail,此时线程添加的时候,将会添加到tail的后面,那么此时这个阻塞队列就会成为这个局面,由于tail节点不再被引用导致添加的新节点也不会被引用。导致真正内存中的队列只剩下一个头节点,把本来应该要加入的新节点丢失了。
但LinkedBlockingQueue中采用的节点的删除法是将头节点的后驱节点中中的内容进行清除,然后将头节点删除,将头节点的后驱节点作为删除后的头节点,这样的话,对于就不会造成上述所说的节点缺失的问题,
这个问题是解决了,但是对于不同的线程来说,都保存的是head或者last的复制本,并且也是对其复制本进行操作,会带来一个问题就是最终内存中的head和last的值回因为多线程的问题,导致内存中的head和last不准确
那么LinkedBlockingQueue采取的措施是什么呢。
针对上边的成员来说,我们看到有两个Reentrantlock锁,一个是takelock一个是putlock,那么LinkedBlockingQueue就是采用这两个锁来保证线程安全的。
我们来看看添加操作和删除操作,只展示部分代码说明问题
public boolean offer(E e) {
final ReentrantLock putLock = this.putLock;
putLock.lock();//采用的是putlock
try {
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
}
} finally {
putLock.unlock();
}
}
put()
public void put(E e) throws InterruptedException {
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();//采用的是putlock
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
我们从上边的方法就可以看出来,对于添加操作,采用的是putlock锁,
那不用说对于删除操作采用的是takelock,
我们再来看put和offer两个方法调用的核心方法enqueue(node);
发现这个方法只有一行有效代码,并且只对last节点进行了操作
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
poll和take方法代用的核心方法我们同样发现了,只用了head节点,或者说只对head节点进行操作。
private E dequeue() {
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
那我们来总结一下成员head和last为什么没有任何关于线程安全的修饰,同样可以保证多线程安全呢
因为对于LinkedBlockingQueue来说,添加操作采用了putlock锁来保证添加操作的同步,同时又因为添加操作只是对last进行修改,那可以说明last这个变量具有准确性
。同样删除操作采用takelock锁才保证删除操作的同步,同时删除只对head变量进行操作,所以head也是具有准确性的。