LinkedBlockingQueue是一个用链表实现的有界队列。此队列的默认长度和最大长度为Integer.MAX_VALUE。
一、属性变量
//队列容量 private final int capacity; //队列中的元素个数 private final AtomicInteger count = new AtomicInteger(); //队头 transient Node<E> head; //队尾 private transient Node<E> last; //出队的锁 private final ReentrantLock takeLock = new ReentrantLock(); //队列非空的监视器 private final Condition notEmpty = takeLock.newCondition(); //入队的锁 private final ReentrantLock putLock = new ReentrantLock(); //对类未满的监视器 private final Condition notFull = putLock.newCondition();
可以看到,与ArrayBlockingQueue不同,这里使用了两个锁,一个针对于出队的锁,另一个针对入队的锁
二、构造方法
public LinkedBlockingQueue() { this(Integer.MAX_VALUE); } public LinkedBlockingQueue(int capacity) { if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; last = head = new Node<E>(null); }
默认构造容量为Integer.MAX_VALUE,你也可以传入一个int值当做队列容量
三、操作方法
private void enqueue(Node<E> node) { last = last.next = node; }
入队的操作方法,只是简单的将入队节点连接在队尾,然后队尾指向入队元素
1、offer方法
public boolean offer(E e) { //入队元素判空 if (e == null) throw new NullPointerException(); final AtomicInteger count = this.count; //如果队列已满,返回false if (count.get() == capacity) return false; int c = -1; Node<E> node = new Node<E>(e); final ReentrantLock putLock = this.putLock; //加锁,只锁要进行入队的线程 putLock.lock(); try { //如果队列未满 if (count.get() < capacity) { //入队 enqueue(node); //队列长度加1,这里的c是入队前的长度 c = count.getAndIncrement(); //如果入队后队列未满,唤醒等待入队的线程 if (c + 1 < capacity) notFull.signal(); } } finally { putLock.unlock(); } //c等于0,说明队列里原本没有元素,唤醒的是因队列里没有元素而要出队的线程 if (c == 0) signalNotEmpty(); return c >= 0; }
可以看到offer方法并不阻塞线程,如果队列已满,只返回false
offer方法入队步骤如下:
1、入队元素判空
2、如果队列已满,返回false
3、定义一个int值c,用来判断这次入队是否成功
4、加锁,只锁要进行入队的线程
5、如果队列没有满(这里判断是因为步骤3可能有其他线程入队修改队列的元素个数),
进行入队,队列元素个数加1。并将原来的元素个数赋给c,如果入队后队列未满,
则唤醒入队的线程(因为出队线程和入队线程不是一个锁)。
6、释放锁
7、判断c的值,如果c==0,调用singalNotEmpty方法。说明队列里原来没有元素并且入队成功,唤醒因出队而阻塞的线程
我们看一下singalNotEmpty这个方法
private void signalNotEmpty() { final ReentrantLock takeLock = this.takeLock; takeLock.lock(); try { notEmpty.signal(); } finally { takeLock.unlock(); } }
这个方法是唤醒因出队而阻塞(也就是因take方法阻塞)的线程。这是为什么呢,因为可能有调用put方法而阻塞的线程在等待元素的出队。
2、put方法
public void put(E e) throws InterruptedException { //入队元素判空 if (e == null) throw new NullPointerException(); int c = -1; Node<E> node = new Node<E>(e); final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; //加锁 putLock.lockInterruptibly(); try { //如果队列已满,则阻塞线程 while (count.get() == capacity) { notFull.await(); } //入队 enqueue(node); //队列元素个数加1,给c赋予入队之前的元素个数 c = count.getAndIncrement(); //如果入队后队列未满,则唤醒入队的线程 if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } //唤醒要出队的线程 if (c == 0) signalNotEmpty(); }
可以看到,put方法是会阻塞的,在队列已满时,线程将被阻塞。只需将offer方法步骤5改为阻塞线程即可
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; }
出队的操作方法,很简单,先记录队头的后继节点,然后更新队头指向原队头的后继,记录原队头的元素值,将原队头的元素值置空,返回原队头的值。
3、poll方法
public E poll() { //获取队列元素个数 final AtomicInteger count = this.count; //如果队列为空,返回null if (count.get() == 0) return null; E x = null; //用来判断出队是否成功的 int c = -1; final ReentrantLock takeLock = this.takeLock; //加锁,锁住其他出队的线程 takeLock.lock(); try { //如果队列元素个数大于0 if (count.get() > 0) { //出队 x = dequeue(); //元素个数减1,返回出队前的元素个数给c c = count.getAndDecrement(); //如果出队后,队列仍可出队,唤醒等待出队的线程 if (c > 1) notEmpty.signal(); } } finally { takeLock.unlock(); } //如果队列出队后不为空,唤醒等待入队的线程 if (c == capacity) signalNotFull(); //返回队尾的值 return x; }
可以看到,poll与offer方法类似,返回特殊值,当没有可出队的元素时,返回null
poll方法出队步骤:
1、判断队列是否为空,为空则返回null,不为空执行下一步
2、定义int类型变量c,用来判断出队操作是否成功
3、加锁,锁住其他要出队的线程
4、如果队列有可出队的元素,则调用enqueue返回队头的值,元素个数减1,
将出队前的元素个数赋值给c,如果c大于1,说明队列可以继续出队,唤醒等待出队的线程
5、释放锁
6、如果成功出队后,唤醒等待入队的元素
7、返回队头的值
private void signalNotFull() { final ReentrantLock putLock = this.putLock; putLock.lock(); try { notFull.signal(); } finally { putLock.unlock(); } }
singalNotFull就是唤醒要入队的线程
4、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.await(); } //获取队头元素 x = dequeue(); //队列元素个数减1 c = count.getAndDecrement(); //如果队列可继续出队,则唤醒要出队的线程 if (c > 1) notEmpty.signal(); } finally { takeLock.unlock(); } //如果出队成功,唤醒要入队的线程 if (c == capacity) signalNotFull(); //返回队头的值 return x; }
可以看到,take方法与put方法类似,是可阻塞的。当没有元素可出队时,线程阻塞。
take方法出队执行步骤就是将poll方法的步骤4改为线程阻塞即可。