阻塞队列对比、总览

Java中阻塞队列原理、特点、适用场景-LMLPHP

阻塞队列本质思想

阻塞队列都是线程安全的队列.
其最主要的功能就是当put元素的时候, 如果队列达到最大容量, 此时put线程可以自动阻塞,直到队列中有元素被取走.
当take元素的时候, 如果队列中元素为空, take线程会自动阻塞, 直到有元素被放入队列中.

那这个功能是怎样实现的呢?
其实最本质就是使用的Condition机制的await()和singnal(), 类似于synchronized中的wait()和notify().

当put元素时,发现队列中元素已满, 就调用await()方法, 阻塞当前线程, 而当有消费者获取元素后,不管有没有put线程被阻塞,都直接调用singnal()方法, 去尝试唤醒.这样就能达到效果.

Condition原理

Condition是要和lock配合使用的, 也就是Condition和ReentrantLock是绑定在一起的,而ReentrantLock底层实现其实是AQS.所以要想理解Condition的话,最好先理解AQS,不然可能会有云里雾里的感觉, 感兴趣的同学可以看看这篇文章

我们获取Condition对象可以使用lock.newCondition(),而这个方法实际上是会new出一个ConditionObject对象,该类是AQS的一个内部类.而AQS内部维护了一个同步队列,如果获取锁失败的话,会将线程放入该同步队列,同样的,condition内部也是使用同样的方式,其内部维护了一个等待队列

其大致原理如下:

  • 当awaitThread线程调用await方法后,会释放当前锁,挂起该线程, 将该线程加入到等待队列中

  • 当signalThread线程调用signal方法后,会将awaitThread线程从等待队列中移出,加入到同步队列中(此时没有唤醒),使awaitThread线程能够有机会获取到锁, 当signalThread线程真正释放锁之后, 处于同步队列中的awaitThread线程被唤醒,重新竞争获取锁, 然后执行剩下的代码.

主要队列讲解

ArrayBlockingQueue

ArrayBlockingQueue底层是以数组实现的有界阻塞队列.

我们讲所有阻塞队列都是线程安全的, 必然会用到锁, 它内部使用的是ReentrantLock.

那锁的是谁呢?

其实锁的就是这个数组对象, 因为数组是不可以变的, 存取元素都会操作这个数组.这样就能保证线程安全.

那数组是怎样实现队列的先进先出呢?

其实就是内部维护了两个指针, 每put一个新元素, puTIndex指针往后移一次,然后进行赋值; 取的时候就找takeIndex,取出后将该位置置为null.

这两个指针都是从左向右进行移动的, 移动到末尾会自动回到首位.
Java中阻塞队列原理、特点、适用场景-LMLPHP

LinkedBlockingQueue

LinkedBlockingQueue 是用链表实现的 无界 阻塞队列.

因为是链表实现的, 所以它可以动态扩容, 初始可以指定容量.

我们想一想对于链表来说, 我们每次put, 其实都需要新new一个节点, 而take则是取原有节点, 所以它读和写是不影响的.所以它有两把锁.

存放元素和获取元素是两个不同的锁对象, 这样就足以保证线程安全.所以它其实就实现了锁分离, 读写不会相互阻塞, put和take是可以同时进行的,在并发量高的时候性能会高一些

Java中阻塞队列原理、特点、适用场景-LMLPHP

Java中阻塞队列原理、特点、适用场景-LMLPHP

SynchronousQueue

SynchronousQueue是Java并发包中提供的一种特殊类型的阻塞队列。它是一种没有容量的队列,每个插入操作必须等待另一个线程的相应移除操作,反之亦然。

它完成线程间数据交换是同步的, 也就是当你put一个元素就会直接被阻塞, 只有这个元素被take之后,put线程才会被唤醒.

其实如果只有一个线程put,一个线程take,那这个还是很容易实现的
但是其实我们可能有很多个线程put,很多个线程take,又要完成同步的数据交换.
这个怎么实现呢?

首先如果很多个线程put, 那这些线程都会被阻塞, 那这些线程所带的元素应该放哪呢?
所以需要一个容器, 去存储元素,并不是很多人理解的SynchronousQueue是没有存储容器的

这个容器其实就是以Node节点构成的队列, 当put元素的时候, 把put线程中携带的元素封装成Node节点,同时该节点还存储了当前put线程,也就是绑定该线程.然后将该线程阻塞. 一直阻塞到该Nodo的节点被take之后, 再唤醒put线程.从而实现同步.

Java中阻塞队列原理、特点、适用场景-LMLPHP

由于SynchronousQueue需要一对线程进行插入和移除操作,因此需要额外的线程协调来保证操作的成功。如果某个线程插入元素而没有其他线程移除,或者某个线程移除元素而没有其他线程插入,那么可能会导致线程阻塞,甚至死锁。

LinkedTransferQueue

如果理解了LinkedBlockingQueue和SynchronousQueue,那LinkedTransferQueue也很好理解了.

但是它有一个很大的特点, 结合了LinkedBlockingQueue和SynchronousQueue的特点.
你可以自己控制放元素是否需要被阻塞

比如使用put方法就不会阻塞,立即返回
而使用transfer方法就会阻塞线程,等待被消费者消费

而取元素基本和SynchronousQueue一样,都会阻塞直到有新的元素可以被取出.

PriorityBlockingQueue

PriorityBlockingQueue是一个可以实现优先级任务的并发阻塞队列,它继承自BlockingQueue接口,实现了一个基于优先级的无界阻塞队列。它的特点是当多个线程同时插入元素时,会按照元素的优先级进行排序,并且可以保证在获取元素时,总是返回优先级最高的元素

那是怎样实现的呢?

它的底层数据结构是一个数组实现的平衡二叉树, 并且是一个最小堆,最小堆的特点是每个节点的值都小于或等于其子节点的值. 所以它的根节点一定是最小的值

Java中阻塞队列原理、特点、适用场景-LMLPHP

而每次返回的优先级最高的元素也就是这个根节点, 当前的最小值.
当然, 每次取元素或者存元素都需要排序. 性能也会受到一定影响.

DelayQueue

DelayQueue允许将任务按照延迟时间进行排序,保证了任务按照预定的时间顺序执行。这对于需要在特定时间执行任务的场景非常有用,比如定时任务、延迟队列等。

它底层使用的是PriorityQueue, 和上面讲的PriorityBlockingQueue很类似, 只不过本身没有阻塞的功能, 阻塞功能由DelayQueue自己实现.

而PriorityQueue队列会根据元素的延迟时间自动排序。元素的延迟时间越短,它就越靠近队列头部,即优先级高. 当获取元素时, 会判断头部元素是否已经超过延迟时间, 超过则进行取出, 否则就进行阻塞.

LinkedBlockingDeque

LinkedBlockingDeque是一个基于链表的双向阻塞队列,它可以在队列的两端进行插入和删除操作.

它的实现基本和LinkedBlockingQueue类似,但是可以支持两边操作, 比如在两边同时进行插入操作.
但是也是由于这个不同点, 为了两边同时操作时的线程安全, 它存取是用的同一把锁, 这也是一个区别点.

今天的分享就到这里了,有问题可以在评论区留言,均会及时回复呀.
我是bling,未来不会太差,只要我们不要太懒就行, 咱们下期见.
Java中阻塞队列原理、特点、适用场景-LMLPHP

10-06 19:17