| 
 | 1 | + | 
 | 2 | +# 1. ArrayBlockingQueue简介 #  | 
 | 3 | + | 
 | 4 | +在多线程编程过程中,为了业务解耦和架构设计,经常会使用并发容器用于存储多线程间的共享数据,这样不仅可以保证线程安全,还可以简化各个线程操作。例如在“生产者-消费者”问题中,会使用阻塞队列(BlockingQueue)作为数据容器,关于BlockingQueue可以[看这篇文章](https://juejin.im/post/5aeebd02518825672f19c546)。为了加深对阻塞队列的理解,唯一的方式是对其实验原理进行理解,这篇文章就主要来看看ArrayBlockingQueue和LinkedBlockingQueue的实现原理。  | 
 | 5 | + | 
 | 6 | +# 2. ArrayBlockingQueue实现原理 #  | 
 | 7 | + | 
 | 8 | +阻塞队列最核心的功能是,能够可阻塞式的插入和删除队列元素。当前队列为空时,会阻塞消费数据的线程,直至队列非空时,通知被阻塞的线程;当队列满时,会阻塞插入数据的线程,直至队列未满时,通知插入数据的线程(生产者线程)。那么,多线程中消息通知机制最常用的是lock的condition机制,关于condition可以[看这篇文章的详细介绍](https://juejin.im/post/5aeea5e951882506a36c67f0)。那么ArrayBlockingQueue的实现是不是也会采用Condition的通知机制呢?下面来看看。  | 
 | 9 | + | 
 | 10 | +## 2.1 ArrayBlockingQueue的主要属性   | 
 | 11 | + | 
 | 12 | +ArrayBlockingQueue的主要属性如下:  | 
 | 13 | + | 
 | 14 | +/** The queued items */  | 
 | 15 | +final Object[] items;  | 
 | 16 | + | 
 | 17 | +/** items index for next take, poll, peek or remove */  | 
 | 18 | +int takeIndex;  | 
 | 19 | + | 
 | 20 | +/** items index for next put, offer, or add */  | 
 | 21 | +int putIndex;  | 
 | 22 | + | 
 | 23 | +/** Number of elements in the queue */  | 
 | 24 | +int count;  | 
 | 25 | + | 
 | 26 | +/*  | 
 | 27 | + * Concurrency control uses the classic two-condition algorithm  | 
 | 28 | + * found in any textbook.  | 
 | 29 | + */  | 
 | 30 | + | 
 | 31 | +/** Main lock guarding all access */  | 
 | 32 | +final ReentrantLock lock;  | 
 | 33 | + | 
 | 34 | +/** Condition for waiting takes */  | 
 | 35 | +private final Condition notEmpty;  | 
 | 36 | + | 
 | 37 | +/** Condition for waiting puts */  | 
 | 38 | +private final Condition notFull;  | 
 | 39 | + | 
 | 40 | +从源码中可以看出ArrayBlockingQueue内部是采用数组进行数据存储的(`属性items`),为了保证线程安全,采用的是`ReentrantLock lock`,为了保证可阻塞式的插入删除数据利用的是Condition,当获取数据的消费者线程被阻塞时会将该线程放置到notEmpty等待队列中,当插入数据的生产者线程被阻塞时,会将该线程放置到notFull等待队列中。而notEmpty和notFull等中要属性在构造方法中进行创建:  | 
 | 41 | + | 
 | 42 | +public ArrayBlockingQueue(int capacity, boolean fair) {  | 
 | 43 | + if (capacity <= 0)  | 
 | 44 | + throw new IllegalArgumentException();  | 
 | 45 | + this.items = new Object[capacity];  | 
 | 46 | + lock = new ReentrantLock(fair);  | 
 | 47 | + notEmpty = lock.newCondition();  | 
 | 48 | + notFull = lock.newCondition();  | 
 | 49 | +}  | 
 | 50 | + | 
 | 51 | +接下来,主要看看可阻塞式的put和take方法是怎样实现的。  | 
 | 52 | + | 
 | 53 | +## 2.2 put方法详解  | 
 | 54 | + | 
 | 55 | +` put(E e)`方法源码如下:  | 
 | 56 | + | 
 | 57 | +public void put(E e) throws InterruptedException {  | 
 | 58 | + checkNotNull(e);  | 
 | 59 | + final ReentrantLock lock = this.lock;  | 
 | 60 | + lock.lockInterruptibly();  | 
 | 61 | + try {  | 
 | 62 | +//如果当前队列已满,将线程移入到notFull等待队列中  | 
 | 63 | + while (count == items.length)  | 
 | 64 | + notFull.await();  | 
 | 65 | +//满足插入数据的要求,直接进行入队操作  | 
 | 66 | + enqueue(e);  | 
 | 67 | + } finally {  | 
 | 68 | + lock.unlock();  | 
 | 69 | + }  | 
 | 70 | +}  | 
 | 71 | + | 
 | 72 | + | 
 | 73 | +该方法的逻辑很简单,当队列已满时(`count == items.length`)将线程移入到notFull等待队列中,如果当前满足插入数据的条件,就可以直接调用` enqueue(e)`插入数据元素。enqueue方法源码为:  | 
 | 74 | + | 
 | 75 | +private void enqueue(E x) {  | 
 | 76 | + // assert lock.getHoldCount() == 1;  | 
 | 77 | + // assert items[putIndex] == null;  | 
 | 78 | + final Object[] items = this.items;  | 
 | 79 | +//插入数据  | 
 | 80 | + items[putIndex] = x;  | 
 | 81 | + if (++putIndex == items.length)  | 
 | 82 | + putIndex = 0;  | 
 | 83 | + count++;  | 
 | 84 | +//通知消费者线程,当前队列中有数据可供消费  | 
 | 85 | + notEmpty.signal();  | 
 | 86 | +}  | 
 | 87 | + | 
 | 88 | +enqueue方法的逻辑同样也很简单,先完成插入数据,即往数组中添加数据(`items[putIndex] = x`),然后通知被阻塞的消费者线程,当前队列中有数据可供消费(`notEmpty.signal()`)。  | 
 | 89 | + | 
 | 90 | +## 2.3 take方法详解   | 
 | 91 | + | 
 | 92 | +take方法源码如下:  | 
 | 93 | + | 
 | 94 | + | 
 | 95 | +public E take() throws InterruptedException {  | 
 | 96 | + final ReentrantLock lock = this.lock;  | 
 | 97 | + lock.lockInterruptibly();  | 
 | 98 | + try {  | 
 | 99 | +//如果队列为空,没有数据,将消费者线程移入等待队列中  | 
 | 100 | + while (count == 0)  | 
 | 101 | + notEmpty.await();  | 
 | 102 | +//获取数据  | 
 | 103 | + return dequeue();  | 
 | 104 | + } finally {  | 
 | 105 | + lock.unlock();  | 
 | 106 | + }  | 
 | 107 | +}  | 
 | 108 | + | 
 | 109 | +take方法也主要做了两步:1. 如果当前队列为空的话,则将获取数据的消费者线程移入到等待队列中;2. 若队列不为空则获取数据,即完成出队操作`dequeue`。dequeue方法源码为:  | 
 | 110 | + | 
 | 111 | +private E dequeue() {  | 
 | 112 | + // assert lock.getHoldCount() == 1;  | 
 | 113 | + // assert items[takeIndex] != null;  | 
 | 114 | + final Object[] items = this.items;  | 
 | 115 | + @SuppressWarnings("unchecked")  | 
 | 116 | +//获取数据  | 
 | 117 | + E x = (E) items[takeIndex];  | 
 | 118 | + items[takeIndex] = null;  | 
 | 119 | + if (++takeIndex == items.length)  | 
 | 120 | + takeIndex = 0;  | 
 | 121 | + count--;  | 
 | 122 | + if (itrs != null)  | 
 | 123 | + itrs.elementDequeued();  | 
 | 124 | + //通知被阻塞的生产者线程  | 
 | 125 | +notFull.signal();  | 
 | 126 | + return x;  | 
 | 127 | +}  | 
 | 128 | + | 
 | 129 | +dequeue方法也主要做了两件事情:1. 获取队列中的数据,即获取数组中的数据元素(`(E) items[takeIndex]`);2. 通知notFull等待队列中的线程,使其由等待队列移入到同步队列中,使其能够有机会获得lock,并执行完成功退出。  | 
 | 130 | + | 
 | 131 | +从以上分析,可以看出put和take方法主要是通过condition的通知机制来完成可阻塞式的插入数据和获取数据。在理解ArrayBlockingQueue后再去理解LinkedBlockingQueue就很容易了。  | 
 | 132 | + | 
 | 133 | + | 
 | 134 | +# 3. LinkedBlockingQueue实现原理 #  | 
 | 135 | +LinkedBlockingQueue是用链表实现的有界阻塞队列,当构造对象时为指定队列大小时,队列默认大小为`Integer.MAX_VALUE`。从它的构造方法可以看出:  | 
 | 136 | + | 
 | 137 | +public LinkedBlockingQueue() {  | 
 | 138 | + this(Integer.MAX_VALUE);  | 
 | 139 | +}  | 
 | 140 | + | 
 | 141 | + | 
 | 142 | +# 3.1 LinkedBlockingQueue的主要属性 #  | 
 | 143 | + | 
 | 144 | + | 
 | 145 | +LinkedBlockingQueue的主要属性有:  | 
 | 146 | + | 
 | 147 | +/** Current number of elements */  | 
 | 148 | +private final AtomicInteger count = new AtomicInteger();  | 
 | 149 | + | 
 | 150 | +/**  | 
 | 151 | + * Head of linked list.  | 
 | 152 | + * Invariant: head.item == null  | 
 | 153 | + */  | 
 | 154 | +transient Node<E> head;  | 
 | 155 | + | 
 | 156 | +/**  | 
 | 157 | + * Tail of linked list.  | 
 | 158 | + * Invariant: last.next == null  | 
 | 159 | + */  | 
 | 160 | +private transient Node<E> last;  | 
 | 161 | + | 
 | 162 | +/** Lock held by take, poll, etc */  | 
 | 163 | +private final ReentrantLock takeLock = new ReentrantLock();  | 
 | 164 | + | 
 | 165 | +/** Wait queue for waiting takes */  | 
 | 166 | +private final Condition notEmpty = takeLock.newCondition();  | 
 | 167 | + | 
 | 168 | +/** Lock held by put, offer, etc */  | 
 | 169 | +private final ReentrantLock putLock = new ReentrantLock();  | 
 | 170 | + | 
 | 171 | +/** Wait queue for waiting puts */  | 
 | 172 | +private final Condition notFull = putLock.newCondition();  | 
 | 173 | + | 
 | 174 | +可以看出与ArrayBlockingQueue主要的区别是,LinkedBlockingQueue在插入数据和删除数据时分别是由两个不同的lock(`takeLock`和`putLock`)来控制线程安全的,因此,也由这两个lock生成了两个对应的condition(`notEmpty`和`notFull`)来实现可阻塞的插入和删除数据。并且,采用了链表的数据结构来实现队列,Node结点的定义为:  | 
 | 175 | + | 
 | 176 | +static class Node<E> {  | 
 | 177 | + E item;  | 
 | 178 | + | 
 | 179 | + /**  | 
 | 180 | + * One of:  | 
 | 181 | + * - the real successor Node  | 
 | 182 | + * - this Node, meaning the successor is head.next  | 
 | 183 | + * - null, meaning there is no successor (this is the last node)  | 
 | 184 | + */  | 
 | 185 | + Node<E> next;  | 
 | 186 | + | 
 | 187 | + Node(E x) { item = x; }  | 
 | 188 | +}  | 
 | 189 | + | 
 | 190 | +接下来,我们也同样来看看put方法和take方法的实现。  | 
 | 191 | + | 
 | 192 | +## 3.2 put方法详解 ##  | 
 | 193 | + | 
 | 194 | +put方法源码为:  | 
 | 195 | + | 
 | 196 | +public void put(E e) throws InterruptedException {  | 
 | 197 | + if (e == null) throw new NullPointerException();  | 
 | 198 | + // Note: convention in all put/take/etc is to preset local var  | 
 | 199 | + // holding count negative to indicate failure unless set.  | 
 | 200 | + int c = -1;  | 
 | 201 | + Node<E> node = new Node<E>(e);  | 
 | 202 | + final ReentrantLock putLock = this.putLock;  | 
 | 203 | + final AtomicInteger count = this.count;  | 
 | 204 | + putLock.lockInterruptibly();  | 
 | 205 | + try {  | 
 | 206 | + /*  | 
 | 207 | + * Note that count is used in wait guard even though it is  | 
 | 208 | + * not protected by lock. This works because count can  | 
 | 209 | + * only decrease at this point (all other puts are shut  | 
 | 210 | + * out by lock), and we (or some other waiting put) are  | 
 | 211 | + * signalled if it ever changes from capacity. Similarly  | 
 | 212 | + * for all other uses of count in other wait guards.  | 
 | 213 | + */  | 
 | 214 | +//如果队列已满,则阻塞当前线程,将其移入等待队列  | 
 | 215 | + while (count.get() == capacity) {  | 
 | 216 | + notFull.await();  | 
 | 217 | + }  | 
 | 218 | +//入队操作,插入数据  | 
 | 219 | + enqueue(node);  | 
 | 220 | + c = count.getAndIncrement();  | 
 | 221 | +//若队列满足插入数据的条件,则通知被阻塞的生产者线程  | 
 | 222 | + if (c + 1 < capacity)  | 
 | 223 | + notFull.signal();  | 
 | 224 | + } finally {  | 
 | 225 | + putLock.unlock();  | 
 | 226 | + }  | 
 | 227 | + if (c == 0)  | 
 | 228 | + signalNotEmpty();  | 
 | 229 | +}  | 
 | 230 | + | 
 | 231 | +put方法的逻辑也同样很容易理解,可见注释。基本上和ArrayBlockingQueue的put方法一样。take方法的源码如下:  | 
 | 232 | + | 
 | 233 | +public E take() throws InterruptedException {  | 
 | 234 | + E x;  | 
 | 235 | + int c = -1;  | 
 | 236 | + final AtomicInteger count = this.count;  | 
 | 237 | + final ReentrantLock takeLock = this.takeLock;  | 
 | 238 | + takeLock.lockInterruptibly();  | 
 | 239 | + try {  | 
 | 240 | +//当前队列为空,则阻塞当前线程,将其移入到等待队列中,直至满足条件  | 
 | 241 | + while (count.get() == 0) {  | 
 | 242 | + notEmpty.await();  | 
 | 243 | + }  | 
 | 244 | +//移除队头元素,获取数据  | 
 | 245 | + x = dequeue();  | 
 | 246 | + c = count.getAndDecrement();  | 
 | 247 | + //如果当前满足移除元素的条件,则通知被阻塞的消费者线程  | 
 | 248 | +if (c > 1)  | 
 | 249 | + notEmpty.signal();  | 
 | 250 | + } finally {  | 
 | 251 | + takeLock.unlock();  | 
 | 252 | + }  | 
 | 253 | + if (c == capacity)  | 
 | 254 | + signalNotFull();  | 
 | 255 | + return x;  | 
 | 256 | +}  | 
 | 257 | + | 
 | 258 | +take方法的主要逻辑请见于注释,也很容易理解。  | 
 | 259 | + | 
 | 260 | +# 4. ArrayBlockingQueue与LinkedBlockingQueue的比较 #  | 
 | 261 | + | 
 | 262 | +**相同点**:ArrayBlockingQueue和LinkedBlockingQueue都是通过condition通知机制来实现可阻塞式插入和删除元素,并满足线程安全的特性;  | 
 | 263 | + | 
 | 264 | +**不同点**:1. ArrayBlockingQueue底层是采用的数组进行实现,而LinkedBlockingQueue则是采用链表数据结构;  | 
 | 265 | +2. ArrayBlockingQueue插入和删除数据,只采用了一个lock,而LinkedBlockingQueue则是在插入和删除分别采用了`putLock`和`takeLock`,这样可以降低线程由于线程无法获取到lock而进入WAITING状态的可能性,从而提高了线程并发执行的效率。  | 
0 commit comments