Java 并发 --- 阻塞队列之LinkedBlockingQueue源码分析
来源:互联网 发布:搞笑淘宝买家秀图聊天 编辑:程序博客网 时间:2024/05/16 16:10
前面我们分析了ArrayBlockingQueue,ArrayBlockingQueue是基于数组来实现的阻塞队列,通过可重入锁和Condition 实现了ArrayBlockingQueue的线程安全和条件阻塞,说到数组,一般就会有链表,今天我们一起来看看另一个阻塞队列—LinkedBlockingQueue
在看本文之前,最好要有 AbstractQueuedSynchronizer,ReentrantLock,Condition的知识,下面是我个人分析的博客,仅供参考:
Java 并发 —AbstractQueuedSynchronizer-共享模式与Condition
Java 并发 —ReentrantLock源码分析
LinkedBlockingQueue 介绍(jdk 1.8)
LinkedBlockingQueue 是一个用链表实现的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行操作。
继承体系
LinkedBlockingQueue 实现了BlockingQueue接口,该接口中定义了阻塞的方法接口,
LinkedBlockingQueue 继承了AbstractQueue,具有了队列的行为。
LinkedBlockingQueue 实现了Serializable接口,可以序列化。
数据结构
public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable { //队列存储节点 static class Node<E> { E item; //元素值 //节点后继 Node<E> next; Node(E x) { item = x; } } /** The capacity bound, or Integer.MAX_VALUE if none 队列容量 */ private final int capacity; /** Current number of elements 队列含有的数据个数 */ private final AtomicInteger count = new AtomicInteger(); //队列头,默认不会序列化 (transient 修饰) transient Node<E> head; //队列尾 ,默认不会序列化(transient 修饰) 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(); ... }
可重入锁是独占式的锁,如果用可重入锁来控制对 队列的操作访问,那么此队列将是线程安全的,阻塞操作,那么什么情况下该阻塞,什么情况下不阻塞,这个是由Condition来控制的。
notEmpty :表示的是队列不为空,符合这种条件那么可以进行出队操作,否则将会阻塞,直到队列不为空为止。
notFull: 表示是队列没有满,符合这种条件的可以进行入队操作,否则将会阻塞。
在ArrayBlockingQueue中,入队和出队都是用的同一个可重入锁,而LinkedBlockingQueue 对于出队和入队使用了不同的可重入锁来控制,ArrayBlockingQueue 入队和出队是不能同时并发的,而在LinkedBlockingQueue 中出队和出队是可以同时并发执行的(锁不一样)。
正是如此,对于count(队列中元素的个数)使用了原子类AtomicInteger,来保证对count的操作具有原子性。
构造方法
1、默认构造方法
/** * Creates a {@code LinkedBlockingQueue} with a capacity of * {@link Integer#MAX_VALUE}. */ public LinkedBlockingQueue() { this(Integer.MAX_VALUE); }
在LinkedBlockingQueue 中可以不用指定队列大小,默认是用Integer.MAX_VALUE 来作为队列的大小。
2、指定队列大小
/** * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity. * * @param capacity the capacity of this queue * @throws IllegalArgumentException if {@code capacity} is not greater * than zero */ public LinkedBlockingQueue(int capacity) { if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; //初始化 队头和队尾 last = head = new Node<E>(null); }
队头和队尾初始化为一个空节点(不存储元素值的节点)
3、通过集合初始化
public LinkedBlockingQueue(Collection<? extends E> c) { //设置队列大小为Integer.MAX_VALUE,而不是集合的大小 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(); } }
通过集合初始化队列,队列的大小不是集合的大小,而是Integer.MAX_VALUE,获取入队锁,然后依次向队列中添加元素,在finally 中释放锁,这样能保证就是出现异常,也能正确的释放锁。
入队
LinkedBlockingQueue提供了诸多方法,可以将元素加入队列尾部
1、add(E e)
将指定的元素插入到此队列的尾部,在成功时返回 true,如果此队列已满,则抛出 IllegalStateException
public boolean add(E e) { //调用offer 方法 完成 if (offer(e)) return true; else throw new IllegalStateException("Queue full"); }
2、offer(E e)
将指定的元素插入到此队列的尾部在成功时返回 true,如果此队列已满,则返回 false。
在前面的add 中,内部调用了offer 方法,我们也可以直接调用offer 方法来完成入队操作。
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>(e); //入队锁 final ReentrantLock putLock = this.putLock; //入队锁 加锁 putLock.lock(); try { //如果还可以入队,则入队 if (count.get() < capacity) { enqueue(node); //原子增加 count的值,返回旧值(注意是返回旧值) c = count.getAndIncrement(); //c+1 表示入队后的队列中元素的个数,如果此时没有队满,那么not Full条件满足,唤醒阻塞在notFull条件上的一个线程 if (c + 1 < capacity) notFull.signal(); } } finally { //释放入队锁 putLock.unlock(); } //c 是旧值,如果c==0 表示原来队列为空,现在入队了一个元素,则不为空了,则notEmpty 条件满足,唤醒阻塞在notEmpty条件上的一个线程 if (c == 0) signalNotEmpty(); return c >= 0; }
从offer中可以看出来:LinkedBlockingQueue和ArrayBlockingQueue一样,存储的元素是不能为空的,ArrayList和LinkedList可以存储空元素。
/** * Signals a waiting take. Called only from put/offer (which do not * otherwise ordinarily lock takeLock.) */ private void signalNotEmpty() { final ReentrantLock takeLock = this.takeLock; //出队锁 加锁 takeLock.lock(); try { notEmpty.signal(); } finally { takeLock.unlock(); } }
notEmpty 用于出队控制条件上,当队列不为空时,notEmpty 条件满足,则可以出队,因此需要获取出队锁,在入队的同时,可以出队,在出队的同时也可以出队,只要满足notEmpty 或者notFull 条件就可以了。
3、offer(E e, long timeout, TimeUnit unit)
将指定的元素插入此队列的尾部,如果该队列已满,则在到达指定的等待时间之前等待。
/** * Inserts the specified element at the tail of this queue, waiting if * necessary up to the specified wait time for space to become available. * * @return {@code true} if successful, or {@code false} if * the specified waiting time elapses before space is available * @throws InterruptedException {@inheritDoc} * @throws NullPointerException {@inheritDoc} */ public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { //入队元素不能为空 if (e == null) throw new NullPointerException(); long nanos = unit.toNanos(timeout); int c = -1; final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; //入队锁 可中断的上锁 putLock.lockInterruptibly(); try { //如果队满,则进行限时等待 while (count.get() == capacity) { //超时,返回false if (nanos <= 0) return false; // 未超时,notFull 条件不满足,则在notFull条件上等待 nanos = notFull.awaitNanos(nanos); } //出队 enqueue(new Node<E>(e)); //c 是入队前的旧值 c = count.getAndIncrement(); //notFull 条件满足 if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } //notEmpty条件满足 if (c == 0) signalNotEmpty(); return true; }
这里在获取锁的时候和前面的不一样,lockInterruptibly 表示可以中断的,前面的加锁对中断不敏感,也就是说,在前面的获取锁的方式中,别的线程对当前线程中断,当前线程不会理会(会记录中断状态),而可中断的获取锁,其它线程中断该线程的时候,会抛出中断异常,举个例子:如果队列满了,但是超时时间还没有到,此时如果不想执行入队操作了,那么就可以中断当前线程(注意:中断不是立即取消,中断后也可能会执行入队操作,参考:Java 并发 —中断机制)。
4、put(E e)
将指定的元素插入此队列的尾部,如果该队列已满,则等待。
/** * Inserts the specified element at the tail of this queue, waiting if * necessary for space to become available. * * @throws InterruptedException {@inheritDoc} * @throws NullPointerException {@inheritDoc} */ 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 { //如果队满,则notFull 条件不满足,阻塞在该条件上,直到条件满足,或者发生中断 while (count.get() == capacity) { notFull.await(); } enqueue(node); c = count.getAndIncrement(); if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } if (c == 0) signalNotEmpty(); }
当队列满时,notFull 条件不满足,因此会阻塞在该方法上(await()),这里用的while循环,因为当线程从notFull 条件阻塞中唤醒时,需要重新检查(可以此时被其它线程抢先了,导致还是不满足)。
出队
和入队一样,LinkedBlockingQueue 也提供了很多出队的方法。
1、poll()
获取并移除此队列的头,如果此队列为空,则返回 null
public E poll() { final AtomicInteger count = this.count; //如多队列中没有元素,返回false 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 为出队前的队列大小 c = count.getAndDecrement(); // 元素出队前,队列>1 那么出队后就>0,notEmpty条件满足 if (c > 1) notEmpty.signal(); } } finally { //释放锁 takeLock.unlock(); } //出队前,队满,出队后则队列不满,notFull条件满足,唤醒阻塞在notFull条件上的一个线程 if (c == capacity) signalNotFull(); return x; }
/** * Signals a waiting put. Called only from take/poll. */ private void signalNotFull() { final ReentrantLock putLock = this.putLock; //入队锁 加锁 putLock.lock(); try { notFull.signal(); } finally { putLock.unlock(); } }
2、poll(long timeout, TimeUnit unit)
获取并移除此队列的头部,在指定的等待时间前等待。
public E poll(long timeout, TimeUnit unit) throws InterruptedException { E x = null; int c = -1; long nanos = unit.toNanos(timeout); final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); try { //如果队列为空,则进行超时等待 while (count.get() == 0) { if (nanos <= 0) return null; //notEmpty 条件上阻塞 nanos = notEmpty.awaitNanos(nanos); } x = dequeue(); c = count.getAndDecrement(); if (c > 1) notEmpty.signal(); } finally { takeLock.unlock(); } if (c == capacity) signalNotFull(); return x; }
在poll()方法上增加超时等待,如果队列为空,则等待指定的时间,如果在超时发生前,队列不空,则可以出队,否则发生超时,返回false。
因为可以超时等待,因此使用的可以中断的上锁,同时抛出中断异常,当在超时未发生时,如果不想再继续等待,那么就可以中断该线程,让其从阻塞等待中返回(中断并不一定就可以马上取消任务)。
3、take() :
获取并移除此队列的头部,在元素变得可用之前一直等待
public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; // 出队锁,可中断加锁 takeLock.lockInterruptibly(); try { // 如果队空,则notEmpty 条件不满足,阻塞在notEmpty条件上,直到队列不空,或者发生中断。 while (count.get() == 0) { notEmpty.await(); } //出队 x = dequeue(); c = count.getAndDecrement(); //notEmpty 条件满足 if (c > 1) notEmpty.signal(); } finally { takeLock.unlock(); } //notFull 条件满足 if (c == capacity) signalNotFull(); return x; }
4、peek()
调用此方法,可以返回队头元素,但是元素并不出队。
public E peek() { //队列为空,返回false if (count.get() == 0) return null; final ReentrantLock takeLock = this.takeLock; //虽然不会出队,但是会访问队头元素,因此还是需要加锁 takeLock.lock(); try { //head 是一个空节点(不存储具体元素值的节点),因此head.next 才是第一个元素节点 Node<E> first = head.next; if (first == null) // 队空,返回false return null; else return first.item; //返回元素组 } finally { takeLock.unlock(); } }
在前面我们虽然判断了队列是否为空,但是仍然可能出现队列为空,为什么呢,因为我们判断队列空的地方,并没有获取出队锁,那么如果队列此时只有一个元素,那么队列不为空,在出队锁加锁前,如果其他线程抢先了一步进行了出队,那么此时队列变为空了,当前线程是感应不到的。
序列化
在LinkedBlockingQueue 我们看到其部分属性被声明为了transient,被transient 修饰的属性默认不会被序列化, 在 Java集合之ArrayList 中我们具体阐释过序列化,可以参考了解一下。
因为该阻塞队列是基于链表实现的,物理上是不连续的,因此不能通过ArrayBlockingQueue 那么来默认序列化,需要我们自己”手动”序列化。
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { //出队锁,入队锁都 加锁 fullyLock(); try { // Write out any hidden stuff, plus capacity //先进行默认序列化工作 s.defaultWriteObject(); // Write out all elements in the proper order. //将队列中的元素,依次进行序列化 for (Node<E> p = head.next; p != null; p = p.next) s.writeObject(p.item); // Use trailing null as sentinel //标记队列序列化结束,可以结合反序列化来看 s.writeObject(null); } finally { fullyUnlock(); } }
writeObject 就是序列化过程,这个并不复制,就是把队列中的数据依次序列化即可,反序列化就是一个逆过程。
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { // Read in capacity, and any hidden stuff //进行默认反序列化 s.defaultReadObject(); //初始化队列大小为0 count.set(0); last = head = new Node<E>(null); // Read in all elements and place in queue for (;;) { //不断读取元素,添加到队列中,知道读到null 值。 E item = (E)s.readObject(); if (item == null) break; add(item); } }}
总结
- LinkedBlockingQueue 底层是基于链表实现的队列,容量指定后,不会改变,可以不指定容量,默认容量为Integer.MAX_VALUE。
- LinkedBlockingQueue 线程安全,和Vector不一样,Vector 中用的synchronized
关键字进行线程同步,LinkedBlockingQueue 中通过ReentrantLock来完成的。 - LinkedBlockingQueue和ArrayBlockingQueue 不一样,前者对于出队和入队使用了不同的可重入锁来完成,后者使用了一个可重入锁来完成,因此前者的并发高于后者,后者比前者更线程安全,LinkedBlockingQueue 并不完全线程安全,因为入队和出队是不同的锁,因此二者可以并发执行,虽然对队列大小count的操作都是原子性的,但是并没有被volatile修饰,因此并不能实时感应到count 被修改,例如:
//出队操作 c = count.getAndDecrement(); //1 if (c > 1) //2 notEmpty.signal(); //入队操作 c = count.getAndIncrement(); //3 if (c + 1 < capacity) //4 notFull.signal();
上面代码中,假设线程A进行入队操作,线程B进行出队操作,出队后,队列中此时没元素,假设B执行了代码1 后,时间片没有了,轮到A操作,A入队了很多元素,然后轮到B继续执行,此时变量c的值并不是最新的,因此后续的判断会不准确,但是并不会破坏数据结构。
- Java 并发 --- 阻塞队列之LinkedBlockingQueue源码分析
- 阻塞队列LinkedBlockingQueue源码分析
- 阻塞队列LinkedBlockingQueue源码分析
- java多线程并发处理之阻塞队列LinkedBlockingQueue用法
- 深入剖析java并发之阻塞队列LinkedBlockingQueue与ArrayBlockingQueue
- JDK源码分析之主要阻塞队列实现类LinkedBlockingQueue
- java-并发集合-阻塞队列 LinkedBlockingQueue 演示
- Java 并发 --- 阻塞队列之ArrayBlockingQueue源码分析
- Java 并发 --- 阻塞队列之PriorityBlockingQueuey源码分析
- Java 并发 --- 阻塞队列之DelayQueue源码分析
- Java 并发 --- 阻塞队列之SynchronousQueue源码分析
- Java 并发 --- 阻塞队列之LinkedTransferQueue源码分析
- Java 并发 --- 阻塞队列之LinkedBlockingDeque源码分析
- Java 并发 --- 非阻塞队列之ConcurrentLinkedQueue源码分析
- 阻塞队列之LinkedBlockingQueue
- JDK源码分析之主要阻塞队列实现类ArrayBlockingQueue -- java消息队列/java并发编程/阻塞队列
- 并发队列ConcurrentLinkedQueue和阻塞队列LinkedBlockingQueue
- 并发队列ConcurrentLinkedQueue和阻塞队列LinkedBlockingQueue
- 了解OpenGL之第一个OpenGL程序
- 使用VMware Workstation 12 创建虚拟机的方法
- maven的学习1
- thinkphp 模板输出表格
- nodejs开启gzip压缩,使用compression包
- Java 并发 --- 阻塞队列之LinkedBlockingQueue源码分析
- 方法定义中的具体事项
- 高精度模板
- java简单界面实现
- Day01
- 总结5(作业3)
- 在Android Studio中实现OpenCV人脸检测
- 排序算法学习经验(一)
- vector的实现【C++】