java中常用的阻塞队列与非阻塞队列详解
目录
- 队列概述以及常用方法
- 1 非阻塞队列
- 1 ConcurrentLinkedQueue(线程安全)
- 2 ConcurrentLinkedDeque(线程安全)
- 3 PriorityQueue(线程不安全)
- 2 阻塞队列(线程安全)
- 1. ArrayblockingQueue
- 2. LinkedBlockingQueue
- 3. SynchronousQueue
- 4. LinkedTransferQueue
- 5. LinkedBlockingDeque
- 6. PriorityBlockingQueue
- 7. DelayQueue
- 总结
队列概述以及常用方法
队列是一个有序列表,可以用数组或者链表实现,且遵循先进先出的原则,在此基础上还分为阻塞、非阻塞、优先队列等。
Java中只要是队列都实现了Queue
接口,队列接口定义的方法如下:
方法名 | 描述 | 注意 |
---|---|---|
add(E) | 增加一个元索 | 添加一个元素,添加成功返回true;如果队列空间已满,则抛出异常 |
offer(E) | 增加一个元索 | 添加一个元素,添加成功返回true;如果队列空间已满,返回false |
remove() | 返回并删除队首元素 | 如果队列为空,则抛出异常 |
poll() | 返回并删除队首元素 | 如果队列为空,则返回null |
element() | 返回队首元素,但不移除 | 队列为空则抛出异常 |
peek() | 返回队首元素,但不移除 | 如果队列为空,则返回null |
1 非阻塞队列
1 ConcurrentLinkedQueue(线程安全)
单向链表结构的无界并发队列, 非阻塞队列,由CAS实现线程安全
- 是否有界:无界
- 是否线程安全:是
- 性能:高
2 ConcurrentLinkedDeque(线程安全)
双向链表结构的无界并发队列, 非阻塞队列,由CAS实现线程安全,相较于ConcurrentLinkedQueue,ConcurrentLinkedDeque在某些场景下提供了更加灵活的操作,比如可以从两端进行元素的插入和删除操作。
- 是否有界:无界
- 是否线程安全:是
- 性能:略低于ConcurrentLinkedQueue
3 PriorityQueue(线程不安全)
PriorityQueue是Java中的一个基于优先级堆的队列,基于数组实现,它可以按照元素的优先级进行排序,并且在插入和删除元素时保持有序状态。具体来说,PriorityQueue中的元素必须实现Comparable接口或者通过构造函数提供一个Comparator对象,以便进行元素的比较和排序。
- 是否有界:无界
- 是否线程安全:否(如需线程安全可以使用PriorityBlockingQueue)
- 性能:比普通队列略低(插入和获取操作都需要进行堆调整和排序操作)
PriorityQueue的实现基于数组,它可以自动扩容,但是它的容量大小是有限制的。在插入和删除元素时,PriorityQueue会根据元素的优先级重新调整堆的结构,保证堆顶元素一定是优先级最高的元素。
2 阻塞队列(线程安全)
阻塞队列BlockingQueue
继承了 Queue
接口,是队列的一种,阻塞队列是线程安全
的,阻塞队列在队列基础上加了两个重要的接口put() take()
:
方法\处理方式 | 抛出异常 | 返回true或false | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
检查方法 | element() | peek() | 不可用 | 不可用 |
注意:
在ThreadPoolExecutor中就使用的阻塞队列来实现线程的不结束,以达到线程复用。
常见阻塞队列,juc中常用的阻塞队列如下:
1. ArrayBlockingQueue
基于数组结构实现的一个有界阻塞队列,在创建ArrayBlockingQueue对象时必须制定容量大小。并且可以指定公平性与非公平性,默认情况下为非公平的,即不保证等待时间最长的队列最优先能够访问队列。
它使用了同步机制来保证多个线程对队列的访问不会发生竞态条件。具体来说,ArrayBlockingQueue使用ReentrantLock
和Condition
来保证线程安全和队列的阻塞和唤醒。
- 是否有界:有
- 是否线程安全:是
- 性能:高
2. LinkedBlockingQueue
基于链表结构实现的一个有界阻塞队列,在创建LinkepythondBlockingQueue对象时如果不指定容量大小,则默认大小为Integer.MAX_VALUE
- 是否有界:无
- 是否线程安全:是
- 性能:性能低于ArrayBlockingQueue
LinkedBlockingQueue的实现使用了两个锁,一个用于生产者线程的访问,另一个用于消费者线程的访问。这样可以避免在高并发场景下产生竞态条件,保证了线程安全性。
LinkedBlockingQueue的性能比ArrayBlockingQueue略低,因为它使用链表而不是数组来存储元素。
但是,由于LinkedBlockingQueue是无界队列,因此它在需要动态调整容量时更加灵活,可以根据实际情况自动扩容或缩容。
总的来说,LinkedBlockingQueue是一个非常实用的数据结构,在生产者-消费者模型、线程池等多线程场景中被广泛使用。
3. SynchronousQueue
SynchronousQueue内部并不存储任何元素,它只是在等待其他线程将元素插入或删除队列时才会阻塞,每一个put操作必须等待一个take操作,否则不能继续添加元素
- 是否有界:有
- 是否线程安全:是
- 性能:-
具体来说,当一个线程调用SynchronousQueue的put()
方法时,如果没有其他线程在等待从队列中取出元素,那么当前线程将会阻塞,直到有其他线程调用take()
方法来取出这个元素。
同样地,当一个线程调用take()方法时,如果没有其他线程在等待向队列中插入元素,那么当前线程也会阻塞,直到有其他线程调用put()方法来插入元素。
SynchronousQueue可以用于一些特殊的场景,例如在生产者和消费者之间进行高效的交互。在这种情况下,生产者线程将元素直接交给消费者线程处理,而不需要通过中间缓存。另外,SynchronousQueue还可以用于实现一些并发算法和数据结构,例如公平的交换器(Fair Exchanger)。
需要注意的是,SynchronousQueue不允许null元素,如果试图插入null元素,将会抛出NullPointerException。
4. LinkedTransferQueue
基于链表结构实现的一个无界阻塞队列,是 SynchronousQueue
和 LinkedBlockingQueue
的合体,它使用了CAS(Compare and Swap)操作来保证并发安全性,与其他阻塞队列不同,LinkedTransferQueue内部使用了多个节点来实现队列,每个节点包含一个元素以及指向下一个节点的指针,这种方式可以避免在并发场景下使用锁导致的性能瓶颈,性能比 LinkedBlockingQueue 更高(没有锁操作),比 SynchronousQueue能存储更多的元素
- 是否有界:无
- 是否线程安全:是
- 性能:高
TransferQueue接口相较普通的阻塞队列,增加了这么几个方法:
public interface TransferQueue<E> extends BlockingQueue<E> { // 如果可能,立即将元素转移给等待的消费者。 // 更确切地说,如果存在消费者已经等待接收它(在 take 或 timed poll(long,TimeUnit)poll)中,则立即传送指定的元素,否则返回 false。 boolean tryTransfer(E e); // 将元素转移给消费者,如果需要的话等待。 // 更准确地说,如果存在一个消费者已经等待接收它(在 take 或timed poll(long,TimeUnit)poll)中,则立即传送指定的元素,否则等待直到元素由消费者接收。 void transfer(E e) throws InterruptedException; // 上面方法的基础上设置超时时间 boolean tryTransfer(E e, long timeout, TimeUnit unit) throws InterruptedException; // 如果至少有一位消费者在等待,则返回 true boolean hasWaitingConsumer(); // 返回等待消费者人数的估计值 int getWaitingConsumerCount(); }
但是在使用LinkedTransferQueue时,需要注意一些细节:
- LinkedTransferQueue不支持null元素,如果插入null元素将会抛出NullPointerException异常。
- 在使用transfer()方法时,如果队列已满或为空,调用transfer()方法的线程将会被阻塞,直到另一个线程将元素插入或取走。因此,在使用transfer()方法时需要注意线程的阻塞问题,避免出现死锁或线程饥饿的情况。
- LinkedTransferQueue的迭代器只能用于遍历当前队列中的元素,无法保证在迭代期间队列中的元素不被修改或删除。
5. LinkedBlockingDeque
基于双向链表结构实现的一个双向阻塞队列,它可以同时从队列的两端插入和删除元素,因此支持队列
和栈
的操作。
- 是否有界:无界,但是可以设置队列上限
- 是否线程安全:是
- 性能:高
使用LinkedBlockingDeque时需要注意以下几点:
- LipythonnkedBlockingDeque不支持null元素,如果插入null元素将会抛出NullPointerException异常。
- LinkedBlockingDeque的阻塞特性可能会导致线程饥饿,即某些线程一直无法获得访问队列的机会。为避免这种情况,可以在创建LinkedBlockinGPlPlrfRgDeque时指定容量,或者使用
putFirst()
、putLast()
、takeFirst()
、takeLast()
等非阻塞操作。
6. PriorityBlockingQueue
基于优先级的阻塞队列,它使用数组实现,并且具有无界限的容量,它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。
PriorityBlockingQueue的注意事项如下:
- PriorityBlockingQueue允许插入null元素,但是不允许插入不可比较的元素,如果插入不可比较的元素,将会抛出ClassCastException异常。
- PriorityBlockingQueue的迭代顺序并不是按照元素的优先级顺序排列的。虽然PriorityBlockingQueue能够保证每次取出的元素都是优先级最高的元素,但是在迭代PriorityBlockingQueue时,它的顺序是无法保证的。
- PriorityBlockingQueue在插入元素时,会根据元素的优先级对元素进行排序。因此,如果队列中的元素的优先级发生了变化,需要调用队列中的元素的offer、add、put方法重新排序。
- PriorityBlockingQueue不支持remove(Object)操作,因为它无法快速地找到并删除指定元素。如果需要删除指定元素,可以先将PriorityBlockingQueue中的元素复制到另一个集合中,然后再从新集合中删除指定元素,最后将新集合中的元素重新添加到PriorityBlockingQueue中。
- PriorityBlockingQueue在迭代、插入和删除元素时,都使用了ReentrantLock来保证线程安全。因此,在使用PriorityBlockingQueue时需要注意避免死锁和饥饿等问题。
简单示例:
import java.util.concurrent.PriorityBlockingQueue; public class Task implements Comparable<Task> { private String name; private int priority; public Task(String name, int priority) { this.name = name; this.priority = priority; } public String getName() { return name; } public int getPriority() { return priority; } @Override public int compareTo(Task o) { return Integer.compare(o.priority, this.priority); } } public class PriorityBlockingQueueExample { public static void main(String[] args) { // 创建一个PriorityBlockingQueue对象 PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<Task>(); // 添加任务到队列中 queue.offer(new Task("Task1", 3)); queue.offer(new Task("Task2", 2)); queue.offer(new Task("Task3", 1)); // 取出队列中的任务 while (!queue.isEmpty()) { Task task = queue.poll(); System.out.println("Execute task " + task.getName() + " with priority " + task.getPriority()); } } }
输出结果:
Execute task Task1 with priority 3Execute task Task2 with priority 2Execute task Task3 with priority 1
7. DelayQueue
是一种延时阻塞队列,它可以存储实现了Delayed
接口的元素,其中每个元素都有一个过期时间,即当元素的过期时间到达时,元素会被取出
- 是否有界:无界的,但是在实际应用中,我们可以通过指定初始容量来控制队列的大小,以避免内存溢出等问题。
- 是否线程安全:是
- 性能:性能要比普通的阻塞队列略低(插入和获取操作都需要进行堆调整和排序操作)
DelayQueue内部使用PriorityQueue
实现,元素会按照过期时间排序,即过期时间最短的元素排在队列的头部。DelayQueue的插入操作是非阻塞的,但是取出操作是阻塞的,即当队列中没有过期元素时,取出操作会一直被阻塞,直到队列中有过期元素被插入。
使用DelayQueue时需要注意以下几点:
- DelayQueue的元素必须实现
Delayed
接口,并重写getDelay()
和compareTo()
方法,其中getDelay()方法返回元素的过期时间距当前时间的剩余时间(单位为毫秒),compareTo()方法用于比较元素的过期时间。 - DelayQueue内部使用
ReentrantLock
和Condition
实现线程同步和阻塞操作,因此是线程安全的。 - DelayQueue的取出操作可能会被阻塞,如果需要在队列为空时立即返回,可以使用poll()方法而不是take()方法。
- DelayQueue在理论上是无界的,因为它使用PriorityQueue来存储元素,PriorityQueue的长度是没有限制的。但是,如果你想控制DelayQueue的大小,你可以在创建DelayQueue实例时指定一个初始容量。
DelayQueue<DelayedTask> queue = new DelayQueue<DelayedTask>(new ArrayList<DelayedTask>(capacity));
下面是一个简单的使用DelayQueue的示例代码,在取出元素时,队列会自动按照元素的过期时间进行排序,并等待元素过期后再取出。
注意,在实际使用中,需要根据具体的业务场景来设置元素的过期时间和比较方式:
import java.util.concurrent.DelayQueue; import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; public class DelayQueueDemo { public static void main(String[] args) throws InterruptedException { DelayQueue<DelayElement> queue = new DelayQueue<>(); // 添加元素到队列 queue.add(new DelayElement("A", 3000)); queue.add(new DelayElement("B", 2000)); queue.add(new DelayElement("C", 1000)); // 取出元素 System.out.println(queue.take()); System.out.println(queue.take()); System.out.println(queue.take()); } } class DelayElement implements Delayed { private String name; private long expireTime; public DelayElement(String name, long delayTime) { this.name = name; this.expireTime = System.currentTimeMillis() + delayTime; } @Override public long getDelay(TimeUnit unit) { return expireTime - System.currepythonntTimeMillis(); } @Override public int compareTo(Delayed o) { return Long.compare(this.expireTime, ((DelayElement) o).expireTime); } @Override public String toString() { return "DelayElement{" + "name='" + name + '\'' + '}'; } }
总结
以上为个人经验,希android望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。
精彩评论