LinkedBlockingQueue是一个基于单向链表实现的可选容量的阻塞队列,队列的头节点是等待时间最长的元素,队列的尾节点是等待时间最短的元素。新元素直接插入到尾节点的后面,成为新的尾节点,队列的检索操作在队列的头部获取元素。通常情况下,链表相比数组有更高的吞吐量,但是在大多数的并发应用程序中有不可预测的性能。LinkedBlockingQueue的构造方法可以设定容量大小,不指定容量大小,默认容量是Integer的最大值。
结构
LinkedBlockingQueue使用一个单向链表来实现队列,该队列至少有一个节点,头节点不存储元素。
/** * Node节点类 */ static class Node<E> { E item; /** * - 后继节点node=head.next * - node为null表示head没有后继队列为空 */ Node<E> next; Node(E x) { item = x; } } /** * 队列中的头节点 * head.item == null */ transient Node<E> head; /** * 队列的尾节点 * last.next == null */ private transient Node<E> last;
属性
/** 队列容量,最大值是Integer的最大值 */ private final int capacity; /** 当前队列中元素的个数 */ private final AtomicInteger count = new AtomicInteger(); /** 出队的锁 */ private final ReentrantLock takeLock = new ReentrantLock(); /** 当队列为空时保存出队的线程 */ private final Condition notEmpty = takeLock.newCondition(); /** 入队的锁 */ private final ReentrantLock putLock = new ReentrantLock(); /** 当队列满时保存入队的线程 */ private final Condition notFull = putLock.newCondition();
LinkedBlockingQueue使用了两把锁,两个condition队列,一个是入队锁,一个是出队锁,在并发时,通过入队锁保证了线程入队列的串行化,在出队时,其中一个线程出队时,其他线程都将阻塞。虽然入队和出队同时只能有一个线程进行操作,但是可以同一时刻可以有两个线程共同操作,一个入队一个出队,所以在保证线程的时候,使用了AtomicInteger变量来表示当前队列中元素的个数,AtomicInteger是一个提供原子操作的Integer类,通过线程安全进行加减操作.这样就保证了入队和出队元素个数的一致性。相比于ArrayBlockingQueue来说,它极大了提高了吞吐量.在相同的场景下,使用LinkedBlockingQueue应该可以达到更好的效果。
构造方法
/** * 默认构造创建一个容量是Integer的最大值的队列 */ public LinkedBlockingQueue() { this(Integer.MAX_VALUE); } /** * 创建一个指定容量大小的队列,初始化链表,head=last=null */ public LinkedBlockingQueue(int capacity) { if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; last = head = new Node<E>(null); } /** * 传入一个集合,遍历集合,当集合中的元素为null,直接抛出空指针异常, * 元素不为空时,直接把当前元素封装成一个node节点入队,如果队列中节点个数达到 * 队列的最大容量,直接抛出异常 */ public LinkedBlockingQueue(Collection<? extends E> c) { this(Integer.MAX_VALUE); final ReentrantLock putLock = this.putLock; putLock.lock(); // Never contended, but necessary for visibility try { int n = 0; for (E e : c) { if (e == null) throw new NullPointerException(); if (n == capacity) throw new IllegalStateException("Queue full"); enqueue(new Node<E>(e)); ++n; } count.set(n); } finally { putLock.unlock(); } }
put
public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); // 元素不能为null int c = -1; Node<E> node = new Node(e); // 以当前元素新建node节点 final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; putLock.lockInterruptibly(); // 入队时使用响应中断锁 try { // 当队列满了将线程放入condition队列等待 while (count.get() == capacity) { notFull.await(); } enqueue(node); // 入队操作 c = count.getAndIncrement(); // 再次获取一下入队之前的元素个数 if (c + 1 < capacity) // 如果队列还有剩余容量,唤醒等待的入队线程 notFull.signal(); } finally { putLock.unlock(); } if (c == 0) // 如果队列本来为空,现在加入元素后,直接唤醒出队线程 signalNotEmpty(); }
添加元素首先进行非空判断,如果为空直接抛出空指针异常,然后把当前节点封装成一个node,此时进行加锁,把并行变成串行,只有一个线程才能进行添加操作,当队列满了的时候,让当前线程释放锁,进入condition等待队列,其他线程竞争到锁,如果队列仍然是满的,也会释放锁加入到condition队列,当有线程取出队列中的一个元素时,会发出唤醒信号,condition队列上的线程重新去竞争锁,继续进行入队操作,再次获取一下节点入队之前的元素个数,如果队列还有容量,直接唤醒notfull等待队列上的线程。最后再次判断添加元素之前队列是否为空,如果为空,现在添加了元素,就可以唤醒等待出队的线程。
/** * 把当前节点追到到队列的尾节点 */ private void enqueue(Node<E> node) { last = last.next = node; }
看一下signalNotEmpty方法。
/** * 使用take锁,释放notEmpty队列上的等待线程 */ private void signalNotEmpty() { final ReentrantLock takeLock = this.takeLock; takeLock.lock(); try { notEmpty.signal(); } finally { takeLock.unlock(); } }
offer方法与put的区别在于当队列满了的时候在进行添加元素直接返回false。offer(E e, long timeout, TimeUnit unit)方法和offer方法很相似,只不过是在给定时间内如果没有入队成功,直接返回false。
take
public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); try { while (count.get() == 0) { // 队列为空将线程加入notEmpty等待队列中 notEmpty.await(); } x = dequeue(); // 出队 c = count.getAndDecrement(); // 再次获取一下出队之前的元素个数 if (c > 1) // 如果队列中还有元素,释放notEmpty等待队列上的线程 notEmpty.signal(); } finally { takeLock.unlock(); } if (c == capacity) // 再出队之前队列满了直接唤醒入队线程 signalNotFull(); return x; }
take和put的过程很相似,当队列为空,将线程加入到notEmpty的等待队列中,当队列不为空,就进行出队操作,再次确定一下出队前的元素个数,如果元素个数>1,唤醒notEmpty等待队列上的线程。最后再判断一下,如果队列本来就满了,现在正好取出了一个元素,唤醒入队的线程。
/** * 出队时取队列的最前面的元素,要注意头节点是一个空节点 */ private E dequeue() { Node<E> h = head; Node<E> first = h.next; h.next = h; // 将当前头节点的引用指向自己,便于gc head = first; // 将头节点的next作为头节点,返回该节点的元素,并把将节点置为空 E x = first.item; first.item = null; return x; }
/** * 使用put锁,唤醒notfull队列上的线程 */ private void signalNotFull() { final ReentrantLock putLock = this.putLock; putLock.lock(); try { notFull.signal(); } finally { putLock.unlock(); } }
poll方法与take相比的不同在于当队列为空时直接返回null。poll(long timeout, TimeUnit unit)则是在指定的时间内没有获取到元素直接返回null。
peek
public E peek() { if (count.get() == 0) return null; final ReentrantLock takeLock = this.takeLock; takeLock.lock(); try { Node<E> first = head.next; if (first == null) return null; else return first.item; } finally { takeLock.unlock(); } }
peek是返回队列中head的下一个元素,并不是取出。
put和take可以允许两个线程在链表两端同时进行入队和出队操作,但是一端只能有一个线程执行,通过创建了两把锁来进行线程安全。入队和出队是相互联系的:当多线程中执行入队操作时,a线程抢占了putlock,添加了一个元素,此时正好队列满了,a线程释放锁,线程b获取了锁,但是马上释放锁,加入到notFull的等待队列,线程c加入到了notFull等待队列上b的后面。这时一个线程取出了一个元素,调用signalNotNull方法,通知notFull等待队列,线程b获取了锁,插入一个元素,如果这时检查发现还可以再添加一个元素,调用signal,唤醒c插入元素。出队与此过程相同。
remove
public boolean remove(Object o) { if (o == null) return false; // 要查找的元素为null,直接返回false fullyLock(); // 要同时上两把锁 try { for (Node<E> trail = head, p = trail.next; p != null; trail = p, p = p.next) { // 从头节点的下一个节点遍历 if (o.equals(p.item)) { // 如果找到直接移除,返回true unlink(p, trail); return true; } } return false; // 没找到返回false } finally { fullyUnlock(); } }
remove操作的时候要同时上两把锁,这是因为要从头往后遍历,涉及到队列的两端,需要对两端加锁。
void unlink(Node<E> p, Node<E> trail) { p.item = null; // 把p置为null trail.next = p.next; // 改变p的连接,便于gc if (last == p) // 如果删除的是尾节点 last = trail; // 直接把p的前节点设为尾节点 if (count.getAndDecrement() == capacity) // 如果要删除前的队列满了,直接唤醒notfull等待队列上的线程 notFull.signal(); }