文章目录
- 阻塞队列对比、总览
- 阻塞队列本质思想
- 主要队列讲解
- ArrayBlockingQueue
- LinkedBlockingQueue
- SynchronousQueue
- LinkedTransferQueue
- PriorityBlockingQueue
- DelayQueue
- LinkedBlockingDeque
阻塞队列对比、总览
阻塞队列本质思想
阻塞队列都是线程安全的队列.
其最主要的功能就是当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.
这两个指针都是从左向右进行移动的, 移动到末尾会自动回到首位.
LinkedBlockingQueue
LinkedBlockingQueue 是用链表实现的 无界 阻塞队列.
因为是链表实现的, 所以它可以动态扩容, 初始可以指定容量.
我们想一想对于链表来说, 我们每次put, 其实都需要新new一个节点, 而take则是取原有节点, 所以它读和写是不影响的.所以它有两把锁.
存放元素和获取元素是两个不同的锁对象, 这样就足以保证线程安全.所以它其实就实现了锁分离, 读写不会相互阻塞, put和take是可以同时进行的,在并发量高的时候性能会高一些
SynchronousQueue
SynchronousQueue是Java并发包中提供的一种特殊类型的阻塞队列。它是一种没有容量的队列,每个插入操作必须等待另一个线程的相应移除操作,反之亦然。
它完成线程间数据交换是同步的, 也就是当你put一个元素就会直接被阻塞, 只有这个元素被take之后,put线程才会被唤醒.
其实如果只有一个线程put,一个线程take,那这个还是很容易实现的
但是其实我们可能有很多个线程put,很多个线程take,又要完成同步的数据交换.
这个怎么实现呢?
首先如果很多个线程put, 那这些线程都会被阻塞, 那这些线程所带的元素应该放哪呢?
所以需要一个容器, 去存储元素,并不是很多人理解的SynchronousQueue是没有存储容器的
这个容器其实就是以Node节点构成的队列, 当put元素的时候, 把put线程中携带的元素封装成Node节点,同时该节点还存储了当前put线程,也就是绑定该线程.然后将该线程阻塞. 一直阻塞到该Nodo的节点被take之后, 再唤醒put线程.从而实现同步.
由于SynchronousQueue需要一对线程进行插入和移除操作,因此需要额外的线程协调来保证操作的成功。如果某个线程插入元素而没有其他线程移除,或者某个线程移除元素而没有其他线程插入,那么可能会导致线程阻塞,甚至死锁。
LinkedTransferQueue
如果理解了LinkedBlockingQueue和SynchronousQueue,那LinkedTransferQueue也很好理解了.
但是它有一个很大的特点, 结合了LinkedBlockingQueue和SynchronousQueue的特点.
你可以自己控制放元素是否需要被阻塞
比如使用put方法就不会阻塞,立即返回
而使用transfer方法就会阻塞线程,等待被消费者消费
而取元素基本和SynchronousQueue一样,都会阻塞直到有新的元素可以被取出.
PriorityBlockingQueue
PriorityBlockingQueue是一个可以实现优先级任务的并发阻塞队列,它继承自BlockingQueue接口,实现了一个基于优先级的无界阻塞队列。它的特点是当多个线程同时插入元素时,会按照元素的优先级进行排序,并且可以保证在获取元素时,总是返回优先级最高的元素。
那是怎样实现的呢?
它的底层数据结构是一个数组实现的平衡二叉树, 并且是一个最小堆,最小堆的特点是每个节点的值都小于或等于其子节点的值. 所以它的根节点一定是最小的值
而每次返回的优先级最高的元素也就是这个根节点, 当前的最小值.
当然, 每次取元素或者存元素都需要排序. 性能也会受到一定影响.
DelayQueue
DelayQueue允许将任务按照延迟时间进行排序,保证了任务按照预定的时间顺序执行。这对于需要在特定时间执行任务的场景非常有用,比如定时任务、延迟队列等。
它底层使用的是PriorityQueue, 和上面讲的PriorityBlockingQueue很类似, 只不过本身没有阻塞的功能, 阻塞功能由DelayQueue自己实现.
而PriorityQueue队列会根据元素的延迟时间自动排序。元素的延迟时间越短,它就越靠近队列头部,即优先级高. 当获取元素时, 会判断头部元素是否已经超过延迟时间, 超过则进行取出, 否则就进行阻塞.
LinkedBlockingDeque
LinkedBlockingDeque是一个基于链表的双向阻塞队列,它可以在队列的两端进行插入和删除操作.
它的实现基本和LinkedBlockingQueue类似,但是可以支持两边操作, 比如在两边同时进行插入操作.
但是也是由于这个不同点, 为了两边同时操作时的线程安全, 它存取是用的同一把锁, 这也是一个区别点.
今天的分享就到这里了,有问题可以在评论区留言,均会及时回复呀.
我是bling,未来不会太差,只要我们不要太懒就行, 咱们下期见.