文章目录
队列概述以及常用方法
队列是一个有序列表,可以用数组或者链表实现,且遵循先进先出的原则,在此基础上还分为阻塞、非阻塞、优先队列等。
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
基于链表结构实现的一个有界阻塞队列,在创建LinkedBlockingQueue对象时如果不指定容量大小,则默认大小为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时需要注意以下几点:
-
LinkedBlockingDeque不支持null元素,如果插入null元素将会抛出NullPointerException异常。
-
LinkedBlockingDeque的阻塞特性可能会导致线程饥饿,即某些线程一直无法获得访问队列的机会。为避免这种情况,可以在创建LinkedBlockingDeque时指定容量,或者使用
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 3
Execute task Task2 with priority 2
Execute 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.currentTimeMillis();
}
@Override
public int compareTo(Delayed o) {
return Long.compare(this.expireTime, ((DelayElement) o).expireTime);
}
@Override
public String toString() {
return "DelayElement{" +
"name='" + name + '\'' +
'}';
}
}