接下来我准备阅读BlockingQueue的源码。
BlockingQueue,顾名思义,线程安全的队列。有多种实现,不同实现适用于不同的场景。
下面这张图是对阻塞队列方法的总结,分为 会抛出异常的/返回特定值的/阻塞的/超时返回的。其实不用特意去记,只要看方法的返回值就能判断出来。比如入队方法如果返回的是Boolean,那么就是如果队列满了返回false否则入队返回true;如果是void就是阻塞直到入队成功。
Java的线程安全集合类都保证了这一happens before语义:如果写入的结果保证对另一线程可见,必须要保证写happens-before读。
ArrayBlockingQueue
基于数组实现的有界阻塞队列。
ArrayBlockingQueue维护了如下变量,包括基于数组实现队列需要的一些基本变量和用于多线程同步控制的一些变量,非常清晰,源码注释中也写的很明白。
这里items和count两个变量都没有用volatile修饰,那么如何保证可见性?下面的源码可以看到是通过一个互斥锁来控制所有对共享变量的访问,对队列的crud方法都需要获取互斥锁,一方面是为了互斥,一方面是共享变量的可见性。锁的语义已经包含了可见性。
同步控制相关:
ArrayBlockingQueue的同步控制使用了two-condition算法。(notEmpty 和 notFull两个Condition变量)。
其实就是入队一个元素就notEmpty.signal()一下,出队一个元素就notFull.signal()一下。默认为非公平,也可以在构造函数中指定公平锁模式。一般认为非公平模式性能比公平模式要好,为什么呢?比如队列现在为空,此时入队一个元素后执行signal(),此时恰好有一个消费者线程拉取元素,那这样刚好,这个消费者直接拿到元素,不用阻塞消费者,也不用唤醒一个阻塞状态的等待者,节省了大量开销。
我们前面提到入队的方法有好几个版本,分为阻塞直到入队成功、timeout的、不阻塞直接返回Boolean的。。。。如何实现呢?其实真正入队的方法只有一个:enqueue(E e)
这个方法就是一个普通的入队操作,同步控制设计为执行这个方法的线程必定已经获得了互斥锁。然后不同的入队方法的真正入队操作就是通过调用这个方法。下面贴一下几种不同的入队方法。
出队操作也同理。ArrayBlockingQueue就暂时分析到这里。
LinkedBlockingQueue
使用Node节点来存储元素:
同步控制
不像ArrayBlockingQueue简单粗暴的用一个互斥锁,LinkedBlockingQueue使用了"two lock queue" 算法,就是队头和队尾各一把锁,把锁的粒度变细来提高并发度。
但是,这样有一些共享变量单单获得一把锁并不能保证其原子性,比如队列中元素的个数,LinkedBlockingQueue将其设计为原子类来保证操作的原子性:
这样通过cas的方式来保证加减1操作的原子性。
同样,真正的入队方法只有enqueue(Node e)
final AtomicInteger count = this.count;
final ReentrantLock putLock = this.putLock;
在很多地方都出现过和这两句看似多余的语句,其实是Doug Lea大神坚持认为从本地变量访问比直接访问对象的成员变量性能要高一点,因为访问堆栈变量比访问堆内变量性能要高,而且堆栈内的变量很有可能被放入寄存器。
Can anyone explain why we use "final AtomicInteger …
还有一个细节是LinkedBlockingQueue的入队操作都有如下判断:
出队操作都有如下判断:
看到这里有点奇怪,不应该是入队就notEmpty.signal(),出队就notFull.signal()吗?其实不应该这样做。这样做虽然可以,但是出队和入队都要拿两把锁,这样分两把锁就失去意义了,还不如只用一把锁。因为这里有两把锁,takeLock和putLock,分别用来锁队头和队尾,
notEmpty是属于takeLock的一个Condition,
notFull是属于putLock的一个Condition,
我们可以做一个优化,效果是大多时候入队和出队仅仅需要拿到一把锁。
入队的时候拿到的是putLock,
- 由于有可能是非公平锁模式,所以此时可能还有线程挂在notFull上面,那就入队元素之后,如果此时队列还没满,就顺便唤醒一下。
- 如果之前队列是空的,那么有可能有线程挂在notEmpty上面,所以入队之后判断一下,如果之前队列是空的(c==0),就 signalNotEmpty();
出队同理,不再赘述。
这与ArrayBlockingQueue不同,贴上ArrayBlockingQueue方便对比:
可以看到ArrayBlockingQueue由于只有一把锁,比较简单粗暴,入队就notEmpty.signal()一下,出队就notFull.signal()一下,同时挂起的线程用while+judge防止虚假唤醒。
先写到这啦。