作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2018/22ff/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。
由于版权原因,请阅读原文 --> 聊聊 JDK 阻塞队列源码(ReentrantLock实现)

作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2018/22ff/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。
作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2018/22ff/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。
项目中用到了一个叫做 Disruptor 的队列,今天楼主并不是要介绍 Disruptor 而是想巩固一下基础扒一下 JDK 中的阻塞队列,听到队列相信大家对其并不陌生,在我们现实生活中队列随处可见,最经典的就是去银行办理业务等。
当然在计算机世界中,队列是属于一种数据结构,队列采用的FIFO(first in firstout),新元素(等待进入队列的元素)总是被插入到尾部,而读取的时候总是从头部开始读取。在计算中队列一般用来做排队(如线程池的等待排队,锁的等待排队),用来做解耦(生产者消费者模式),异步等等。
JDK 中的队列
在JDK中的队列都实现了 java.util.Queue 接口,在队列中又分为两类,一类是线程不安全的,ArrayDeque,LinkedList等等,还有一类都在java.util.concurrent包下属于线程安全,而在我们真实的环境中,我们的机器都是属于多线程,当多线程对同一个队列进行操作的时,如果使用线程不安全会出现数据丢失等无法预测的事情,所以我们这个时候只能选择线程安全的队列。下面是我们今天要探讨的两个队列
| 队列名字 |
是否加锁 |
数据结构 |
关键技术点 |
是否有锁 |
是否有界 |
| ArrayBlockingQueue |
是 |
数组array |
ReentrantLock |
有锁 |
有界 |
| LinkedBlockingQueue |
是 |
链表 |
ReentrantLock |
有锁 |
有界 |
ArrayBlockingQueue 源码分析
ArrayBlockingQueue 的原理就是使用一个可重入锁(ReentrantLock )和这个锁生成的两个条件对象进行并发控制,ArrayBlockingQueue是一个有界的阻塞队列,初始化的时候必须要指定队列长度,且指定长度之后不允许进行修改。
成员变量属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| final Object[] items;
int takeIndex;
int putIndex;
int count;
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
|
主要方法源码实现
- add:添加元素到队列里,添加成功返回true,由于容量满了添加失败会抛出
IllegalStateException异常;
- offer:添加元素到队列里,添加成功返回true,添加失败返回false;
- put:添加元素到队列里,如果容量满了会阻塞直到容量不满;
- poll:删除队列头部元素,如果队列为空,返回null。否则返回元素;
- remove:基于对象找到对应的元素,并删除。删除成功返回true,否则返回false;
- take:删除队列头部元素,如果队列为空,一直阻塞到队列有元素并删除。
add方法:
1 2 3 4 5 6
| public boolean add(E e) { if (offer(e)) return true; else throw new IllegalStateException("Queue full"); }
|
offer方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public boolean offer(E e) { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lock(); try { if (count == items.length) return false; else { insert(e); return true; } } finally { lock.unlock(); } }
|
put方法:
1 2 3
| public void put(E e) { xfer(e, true, ASYNC, 0); }
|
我们可以看到,如果队列满了则返回false,如果没有满调用insert。整个方法是通过可重入锁来锁住的,并且最终释放。
接着看一下insert方法:
1 2 3 4 5 6
| private void insert(E x) { items[putIndex] = x; putIndex = inc(putIndex); ++count; notEmpty.signal(); }
|
这里insert被调用的时候就会唤醒notEmpty上等待的线程进行take操作。
再看一下put方法:
1 2 3 4 5 6 7 8 9 10 11 12
| public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == items.length) notFull.await(); insert(e); } finally { lock.unlock(); } }
|
通过上面代码我们可以知道,add方法和offer方法不会阻塞线程,put方法如果队列满了会阻塞线程,直到有线程消费了队列里的数据才有可能被唤醒。
紧接着我们看一下poll方法:
1 2 3 4 5 6 7 8 9
| public E poll() { final ReentrantLock lock = this.lock; lock.lock(); try { return (count == 0) ? null : extract(); } finally { lock.unlock(); } }
|
看看这个extract方法,extract的翻译过来就是提取的意思:
1 2 3 4 5 6 7 8 9
| private E extract() { final Object[] items = this.items; E x = this.<E>cast(items[takeIndex]); items[takeIndex] = null; takeIndex = inc(takeIndex); --count; notFull.signal(); return x; }
|
看一下take方法:
1 2 3 4 5 6 7 8 9 10 11
| public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) notEmpty.await(); return extract(); } finally { lock.unlock(); } }
|
remove方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public boolean remove(Object o) { if (o == null) return false; final Object[] items = this.items; final ReentrantLock lock = this.lock; lock.lock(); try { for (int i = takeIndex, k = count; k > 0; i = inc(i), k--) { if (o.equals(items[i])) { removeAt(i); return true; } } return false; } finally { lock.unlock(); } }
|
再看一下removeAt方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| private void removeAt(int i) { final Object[] items = this.items; if (i == takeIndex) { items[takeIndex] = null; takeIndex = inc(takeIndex); } else { for (;;) { int nexti = inc(i); if (nexti != putIndex) { items[i] = items[nexti]; i = nexti; } else { items[i] = null; putIndex = i; break; } } } --count; notFull.signal(); }
|
LinkedBlockingQueue 源码分析
LinkedBlockingQueue是一个使用链表完成队列操作的阻塞队列。链表是单向链表,而不是双向链表。
成员变量属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| private final int capacity;
private final AtomicInteger count = new AtomicInteger(0);
private 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();
|
主要方法源码实现
由于文章篇幅问题对于LinkedBlockingQueue我们主要分析以下几个方法:
- offer:添加元素到队列里,添加成功返回true,添加失败返回false;
- put:添加元素到队列里,如果容量满了会阻塞直到容量不满;
- poll:删除队列头部元素,如果队列为空,返回null。否则返回元素;
- remove:基于对象找到对应的元素,并删除。删除成功返回true,否则返回false;
- take:删除队列头部元素,如果队列为空,一直阻塞到队列有元素并删除。
offer方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public boolean offer(E e) { if (e == null) throw new NullPointerException(); final AtomicInteger count = this.count; if (count.get() == capacity) return false; int c = -1; Node<E> node = new Node(e); final ReentrantLock putLock = this.putLock; putLock.lock(); try { if (count.get() < capacity) { enqueue(node); c = count.getAndIncrement(); if (c + 1 < capacity) notFull.signal(); } } finally { putLock.unlock(); } if (c == 0) signalNotEmpty(); return c >= 0; }
|
put方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); int c = -1; Node<E> node = new Node(e); final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; putLock.lockInterruptibly(); try { while (count.get() == capacity) { notFull.await(); } enqueue(node); c = count.getAndIncrement(); if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } if (c == 0) signalNotEmpty(); }
|
poll方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public E poll() { final AtomicInteger count = this.count; if (count.get() == 0) return null; E x = null; int c = -1; final ReentrantLock takeLock = this.takeLock; takeLock.lock(); try { if (count.get() > 0) { x = dequeue(); c = count.getAndDecrement(); if (c > 1) notEmpty.signal(); } } finally { takeLock.unlock(); } if (c == capacity) signalNotFull(); return x; }
|
take方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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(); c = count.getAndDecrement(); if (c > 1) notEmpty.signal(); } finally { takeLock.unlock(); } if (c == capacity) signalNotFull(); return x; }
|
remove方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public boolean remove(Object o) { if (o == null) return false; fullyLock(); try { for (Node<E> trail = head, p = trail.next; p != null; trail = p, p = p.next) { if (o.equals(p.item)) { unlink(p, trail); return true; } } return false; } finally { fullyUnlock(); } }
|
紧接着来看一下 fullyLock与fullyUnlock方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
void fullyLock() { putLock.lock(); takeLock.lock(); }
void fullyUnlock() { takeLock.unlock(); putLock.unlock(); }
|
LinkedBlockingQueue的take方法对于没数据的情况下会阻塞,poll方法删除链表头结点,remove方法删除指定的对象。
需要注意的是remove方法由于要删除的数据的位置不确定,需要2个锁同时加锁。
小结
文章有点长,JDK中的阻塞队列线程安全的主要有ArrayBlockingQueue,LinkedBlockingQueue,LinkedTransferQueue,DelayQueue四种,今天楼主把ArrayBlockingQueue,LinkedBlockingQueue放在一起介绍主要原因是这两者都是使用可重入锁 ReentrantLock实现的线程安全。
当然二者也有很大的不同,主要是:
1,ArrayBlockingQueue只有1个锁,添加数据和删除数据的时候只能有1个被执行,不允许并行执行。
而LinkedBlockingQueue有2个锁,放元素锁和取元素锁,添加数据和删除数据是可以并行进行的,当然添加数据和删除数据的时候只能有1个线程各自执行。
2,ArrayBlockingQueue中放入数据阻塞的时候,需要消费数据才能唤醒。
而LinkedBlockingQueue中放入数据阻塞的时候,因为它内部有2个锁,可以并行执行放入数据和消费数据,不仅在消费数据的时候进行唤醒插入阻塞的线程,同时在插入的时候如果容量还没满,也会唤醒插入阻塞的线程。
参考链接

作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2018/22ff/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。