六、队列(Queue)

队列

一、队列概述

队列是一种遵循先进先出(FIFO,First-In-First-Out)原则的重要数据结构。这种特性使其广泛应用于任务调度、消息传递和缓冲处理等场景。典型应用包括操作系统的进程调度、打印机任务队列以及网络请求处理等,这些场景都需要队列来确保数据处理的有序性。

Java集合框架在java.util包中提供了完整的队列实现。Queue接口作为所有队列类的基础,不仅继承了Collection接口的基本功能,还针对队列操作进行了扩展。该接口定义了两类处理方式:一类在操作失败时抛出异常,另一类则返回特殊值(如nullfalse),以满足不同场景的需求。

1.1 队列的核心操作

Queue接口定义的核心操作如下表所示:

操作类型抛出异常的方法返回特殊值的方法说明
入队add(e)offer(e)将元素添加到队列尾部,成功返回true
出队remove()poll()移除并返回队列头部元素,队列为空时前者抛NoSuchElementException,后者返回null
查看队头element()peek()返回但不移除队列头部元素,队列为空时前者抛NoSuchElementException,后者返回null

从表中可以看出,add()offer()的主要区别在于当队列有界(即有容量限制)且已满时,add()会抛出IllegalStateException,而offer()则返回false。因此,在使用有界队列时,offer()方法更为常用,因为它可以通过返回值判断操作是否成功。

1.2 队列的分类

根据不同的特性,Java中的队列可以分为以下几类:

  1. 普通队列:遵循FIFO原则,如LinkedListArrayDeque(作为队列使用时)。
  2. 优先级队列:元素按照优先级排序,而非插入顺序,如PriorityQueue
  3. 双端队列:允许在队列的两端进行元素的插入和删除操作,如ArrayDequeLinkedBlockingDeque
  4. 阻塞队列:当队列满时,入队操作会阻塞;当队列空时,出队操作会阻塞,主要用于多线程环境,如LinkedBlockingQueueArrayBlockingQueue
  5. 并发队列:线程安全的队列,支持多线程并发操作,如ConcurrentLinkedQueueLinkedTransferQueue
  6. 延迟队列:元素只有在指定的延迟时间后才能被取出,如DelayQueue

接下来,我们将详细讲解Java中各个队列相关的类及其方法,包括它们的底层实现、特性、常用方法及使用场景。

二、Queue接口详解

Queue接口是Java队列体系的基础,它继承自Collection接口,因此具备Collection的所有方法(如size()isEmpty()contains()等),同时又定义了队列特有的操作方法。

2.1 Queue接口的定义

Queue接口的定义如下:

public interface Queue<E> extends Collection<E> {
    // 入队,失败抛异常
    boolean add(E e);
    
    // 入队,失败返回false
    boolean offer(E e);
    
    // 出队,失败抛异常
    E remove();
    
    // 出队,失败返回null
    E poll();
    
    // 查看队头,失败抛异常
    E element();
    
    // 查看队头,失败返回null
    E peek();
}

从定义可以看出,Queue接口是一个泛型接口,类型参数E表示队列中元素的类型。

2.2 Queue接口的方法详解

2.2.1 入队方法
  1. boolean add(E e)

    • 功能:将指定元素添加到队列的尾部。
    • 特点:如果队列有容量限制且已满,会抛出IllegalStateException;如果元素为null且队列不允许null元素,会抛出NullPointerException;如果元素的类不允许加入队列,会抛出ClassCastException;如果元素的某些属性不允许加入队列,会抛出IllegalArgumentException
    • 返回值:操作成功返回true(根据Collection.add()的规范)。
  2. boolean offer(E e)

    • 功能:将指定元素添加到队列的尾部。
    • 特点:与add()类似,但当队列满时返回false而非抛出异常,其他异常情况与add()一致。
    • 返回值:操作成功返回true,失败返回false
2.2.2 出队方法
  1. E remove()

    • 功能:移除并返回队列的头部元素。
    • 特点:如果队列为空,会抛出NoSuchElementException
    • 返回值:队列的头部元素。
  2. E poll()

    • 功能:移除并返回队列的头部元素。
    • 特点:如果队列为空,返回null
    • 返回值:队列的头部元素,队列为空时返回null
2.2.3 查看队头方法
  1. E element()

    • 功能:返回但不移除队列的头部元素。
    • 特点:如果队列为空,会抛出NoSuchElementException
    • 返回值:队列的头部元素。
  2. E peek()

    • 功能:返回但不移除队列的头部元素。
    • 特点:如果队列为空,返回null
    • 返回值:队列的头部元素,队列为空时返回null

2.3 Queue接口的子接口

Queue接口有多个子接口,它们在Queue的基础上扩展了更多特性:

  1. Deque:双端队列接口,允许在队列的两端进行元素的插入和删除操作。
  2. BlockingQueue:阻塞队列接口,定义了在多线程环境下的阻塞操作。
  3. TransferQueue:继承自BlockingQueue,增加了元素传递的功能,允许生产者直接将元素传递给消费者。

这些子接口将在后续章节中详细讲解。

三、普通队列实现类

3.1 LinkedList

LinkedList是Java集合框架中一个常用的类,它不仅实现了List接口,还实现了Deque接口(进而实现了Queue接口),因此可以作为队列使用。LinkedList底层基于双向链表实现,这使得它在插入和删除元素时具有较高的效率,但随机访问元素的效率较低。

3.1.1 LinkedList的特性
  • 数据结构:双向链表,每个节点包含前驱节点引用、后继节点引用和节点值。
  • 容量:无界,理论上可以无限添加元素(受限于内存)。
  • 线程安全性:非线程安全,多线程环境下需要外部同步。
  • 元素允许:允许null元素,允许重复元素。
  • 排序:不保证元素的排序,按照插入顺序维护元素。
3.1.2 LinkedList的构造方法

LinkedList提供了两个构造方法:

  1. public LinkedList():创建一个空的LinkedList。

    Queue<String> queue = new LinkedList<>();
    
  2. public LinkedList(Collection<? extends E> c):创建一个包含指定集合元素的LinkedList,元素的顺序与集合的迭代器返回顺序一致。

    List<String> list = Arrays.asList("a", "b", "c");
    Queue<String> queue = new LinkedList<>(list);
    
3.1.3 LinkedList作为队列的常用方法

除了实现Queue接口的add()offer()remove()poll()element()peek()方法外,LinkedList还继承了CollectionList接口的方法,以下是一些常用方法:

  1. int size():返回队列中的元素个数。

    Queue<String> queue = new LinkedList<>();
    queue.add("a");
    queue.add("b");
    System.out.println(queue.size()); // 输出:2
    
  2. boolean isEmpty():判断队列是否为空。

    Queue<String> queue = new LinkedList<>();
    System.out.println(queue.isEmpty()); // 输出:true
    queue.add("a");
    System.out.println(queue.isEmpty()); // 输出:false
    
  3. boolean contains(Object o):判断队列是否包含指定元素。

    Queue<String> queue = new LinkedList<>();
    queue.add("a");
    System.out.println(queue.contains("a")); // 输出:true
    System.out.println(queue.contains("b")); // 输出:false
    
  4. Iterator<E> iterator():返回队列的迭代器,用于遍历元素。

    Queue<String> queue = new LinkedList<>();
    queue.add("a");
    queue.add("b");
    Iterator<String> iterator = queue.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next()); // 依次输出:a、b
    }
    
  5. Object[] toArray():将队列中的元素转换为数组。

    Queue<String> queue = new LinkedList<>();
    queue.add("a");
    queue.add("b");
    Object[] array = queue.toArray();
    System.out.println(Arrays.toString(array)); // 输出:[a, b]
    
  6. boolean remove(Object o):移除队列中第一个出现的指定元素(如果存在)。

    Queue<String> queue = new LinkedList<>();
    queue.add("a");
    queue.add("b");
    queue.add("a");
    queue.remove("a");
    System.out.println(queue); // 输出:[b, a]
    
  7. boolean addAll(Collection<? extends E> c):将指定集合中的所有元素添加到队列尾部。

    Queue<String> queue = new LinkedList<>();
    queue.add("a");
    List<String> list = Arrays.asList("b", "c");
    queue.addAll(list);
    System.out.println(queue); // 输出:[a, b, c]
    
  8. void clear():清空队列中的所有元素。

    Queue<String> queue = new LinkedList<>();
    queue.add("a");
    queue.add("b");
    queue.clear();
    System.out.println(queue.isEmpty()); // 输出:true
    
3.1.4 LinkedList的底层实现原理

LinkedList的底层是一个双向链表,其节点定义如下(简化版):

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

每个节点包含三个部分:存储的元素(item)、前驱节点引用(prev)和后继节点引用(next)。LinkedList类中维护了两个引用:first指向链表的头节点,last指向链表的尾节点。

当调用add(E e)offer(E e)方法时,会在链表的尾部添加一个新节点:

public boolean add(E e) {
    linkLast(e);
    return true;
}

public boolean offer(E e) {
    return add(e); // LinkedList的offer()方法直接调用add(),因为它是无界的
}

void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

可以看出,LinkedListoffer()方法与add()方法实现相同,因为LinkedList是无界队列,不会出现添加失败的情况(除非内存不足,此时会抛出OutOfMemoryError)。

当调用remove()方法时,会移除并返回头节点的元素:

public E remove() {
    return removeFirst();
}

public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

private E unlinkFirst(Node<E> f) {
    // 获取头节点的元素
    final E element = f.item;
    // 获取头节点的后继节点
    final Node<E> next = f.next;
    // 释放头节点的引用,帮助GC
    f.item = null;
    f.next = null; // 方便GC
    // 将first指向后继节点
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

当调用poll()方法时,与remove()类似,但队列为空时返回null

public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

element()peek()方法用于查看头节点元素,实现如下:

public E element() {
    return getFirst();
}

public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}

public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}
3.1.5 LinkedList的使用场景

LinkedList作为队列使用时,适用于以下场景:

  • 需要频繁进行插入和删除操作,而随机访问操作较少的场景。
  • 对队列的容量没有限制(或限制较小)的场景。
  • 单线程环境或可以通过外部同步保证线程安全的多线程环境。

例如,在实现一个简单的任务调度器时,可以使用LinkedList作为任务队列,不断将新任务加入队列尾部,调度线程从队列头部取出任务执行。

import java.util.LinkedList;
import java.util.Queue;

public class TaskScheduler {
    private Queue<Task> taskQueue = new LinkedList<>();

    // 添加任务到队列
    public void addTask(Task task) {
        taskQueue.offer(task);
    }

    // 执行队列中的任务
    public void executeTasks() {
        while (!taskQueue.isEmpty()) {
            Task task = taskQueue.poll();
            task.execute();
        }
    }

    public static void main(String[] args) {
        TaskScheduler scheduler = new TaskScheduler();
        scheduler.addTask(new Task("任务1"));
        scheduler.addTask(new Task("任务2"));
        scheduler.addTask(new Task("任务3"));
        scheduler.executeTasks();
    }

    static class Task {
        private String name;

        public Task(String name) {
            this.name = name;
        }

        public void execute() {
            System.out.println("执行任务:" + name);
        }
    }
}

运行上述代码,输出结果为:

执行任务:任务1
执行任务:任务2
执行任务:任务3

可以看到,任务按照添加的顺序依次执行,符合队列的FIFO特性。

3.2 ArrayDeque(作为队列使用)

ArrayDeque是Java中另一个实现了Deque接口的类,它底层基于动态数组实现,既可以作为双端队列使用,也可以作为普通队列使用。与LinkedList相比,ArrayDeque在元素的插入、删除和访问操作上通常具有更高的效率,因为数组的访问是连续的内存空间,缓存利用率更高。

3.2.1 ArrayDeque的特性
  • 数据结构:动态数组,当数组容量不足时会自动扩容。
  • 容量:无界,理论上可以无限添加元素(受限于内存)。
  • 线程安全性:非线程安全,多线程环境下需要外部同步。
  • 元素允许:不允许null元素,否则会抛出NullPointerException;允许重复元素。
  • 排序:不保证元素的排序,按照插入顺序维护元素(作为队列使用时)。
3.2.2 ArrayDeque的构造方法

ArrayDeque提供了三个构造方法:

  1. public ArrayDeque():创建一个初始容量为16的空ArrayDeque。

    Queue<String> queue = new ArrayDeque<>();
    
  2. public ArrayDeque(int numElements):创建一个初始容量至少为指定值的空ArrayDeque。注意,实际容量可能是大于numElements的最小的2的幂次方。

    Queue<String> queue = new ArrayDeque<>(10); // 初始容量为16(因为16是大于10的最小2的幂次方)
    
  3. public ArrayDeque(Collection<? extends E> c):创建一个包含指定集合元素的ArrayDeque,元素的顺序与集合的迭代器返回顺序一致。

    List<String> list = Arrays.asList("a", "b", "c");
    Queue<String> queue = new ArrayDeque<>(list);
    
3.2.3 ArrayDeque作为队列的常用方法

ArrayDeque实现了Queue接口的所有方法,同时也有一些自身特有的方法。以下是作为队列使用时的常用方法:

  1. boolean add(E e):将元素添加到队列尾部,由于ArrayDeque是无界的,此方法始终返回true,如果元素为null则抛出NullPointerException

    Queue<String> queue = new ArrayDeque<>();
    queue.add("a");
    queue.add("b");
    System.out.println(queue); // 输出:[a, b]
    
  2. boolean offer(E e):与add()方法功能相同,将元素添加到队列尾部,返回true,元素为null时抛出NullPointerException

    Queue<String> queue = new ArrayDeque<>();
    queue.offer("a");
    queue.offer("b");
    System.out.println(queue); // 输出:[a, b]
    
  3. E remove():移除并返回队列头部元素,队列为空时抛出NoSuchElementException

    Queue<String> queue = new ArrayDeque<>();
    queue.add("a");
    queue.add("b");
    System.out.println(queue.remove()); // 输出:a
    System.out.println(queue); // 输出:[b]
    
  4. E poll():移除并返回队列头部元素,队列为空时返回null

    Queue<String> queue = new ArrayDeque<>();
    queue.add("a");
    queue.add("b");
    System.out.println(queue.poll()); // 输出:a
    System.out.println(queue); // 输出:[b]
    
  5. E element():返回但不移除队列头部元素,队列为空时抛出NoSuchElementException

    Queue<String> queue = new ArrayDeque<>();
    queue.add("a");
    System.out.println(queue.element()); // 输出:a
    
  6. E peek():返回但不移除队列头部元素,队列为空时返回null

    Queue<String> queue = new ArrayDeque<>();
    queue.add("a");
    System.out.println(queue.peek()); // 输出:a
    
  7. 其他继承自Collection接口的方法如size()isEmpty()contains()等与LinkedList类似,此处不再赘述。

3.2.4 ArrayDeque的底层实现原理

ArrayDeque底层使用一个动态数组elements来存储元素,同时维护了两个索引:head(指向队列的头部元素)和tail(指向队列尾部元素的下一个位置)。数组的长度始终是2的幂次方,这是为了便于使用位运算来实现循环队列的效果。

当调用add(E e)offer(E e)方法时,会将元素添加到tail指向的位置,然后更新tail索引:

public boolean add(E e) {
    offer(e);
    return true;
}

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    // 计算新的tail索引,使用位运算实现循环
    if ( (tail = (tail + 1) & (elements.length - 1)) == head )
        doubleCapacity(); // 数组已满,扩容
    return true;
}

上述代码中,(tail + 1) & (elements.length - 1)是一种高效的取模运算,由于elements.length是2的幂次方,elements.length - 1的二进制表示全为1,因此与tail + 1进行与运算等价于对elements.length取模,实现了循环队列的效果。当tailhead相等时,表示数组已满,此时调用doubleCapacity()方法进行扩容。

doubleCapacity()方法的实现如下:

private void doubleCapacity() {
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p; // 头部元素到数组末尾的元素个数
    int newCapacity = n << 1; // 容量翻倍
    if (newCapacity < 0)
        throw new IllegalStateException("Deque too big");
    Object[] a = new Object[newCapacity];
    // 复制从head到数组末尾的元素
    System.arraycopy(elements, p, a, 0, r);
    // 复制从数组开始到head的元素
    System.arraycopy(elements, 0, a, r, p);
    elements = a;
    head = 0;
    tail = n;
}

扩容时,将原数组的容量翻倍(左移一位),然后将原数组中的元素复制到新数组中,调整head为0,tail为原数组的长度。

当调用poll()方法时,会移除并返回head指向的元素,然后更新head索引:

public E poll() {
    int h = head;
    @SuppressWarnings("unchecked")
    E result = (E) elements[h];
    // 如果队列为空,返回null
    if (result == null)
        return null;
    elements[h] = null; // 帮助GC
    // 更新head索引
    head = (h + 1) & (elements.length - 1);
    return result;
}

remove()方法的实现与poll()类似,但队列为空时会抛出异常:

public E remove() {
    E x = poll();
    if (x == null)
        throw new NoSuchElementException();
    return x;
}

peek()方法用于查看头部元素:

public E peek() {
    return (E) elements[head];
}

element()方法则在peek()返回null时抛出异常:

public E element() {
    E x = peek();
    if (x == null)
        throw new NoSuchElementException();
    return x;
}
3.2.5 ArrayDeque与LinkedList的对比
特性ArrayDequeLinkedList
数据结构动态数组双向链表
内存占用较低(连续内存)较高(每个节点有额外引用)
插入/删除效率高(数组操作,除非需要扩容)高(链表节点操作)
随机访问效率高(O(1))低(O(n))
是否允许null元素不允许允许
迭代器性能较高

从对比可以看出,ArrayDeque在大多数情况下性能优于LinkedList,尤其是在频繁的插入、删除和访问操作中。因此,在不需要存储null元素且需要作为队列或双端队列使用时,ArrayDeque是更好的选择。

3.2.6 ArrayDeque的使用场景

ArrayDeque作为队列使用时,适用于以下场景:

  • 需要高效的插入、删除和访问操作的场景。
  • 不需要存储null元素的场景。
  • 单线程环境或可以通过外部同步保证线程安全的多线程环境。

例如,在实现一个简单的缓存系统时,可以使用ArrayDeque作为缓存队列,当缓存满时移除最早加入的元素:

import java.util.ArrayDeque;
import java.util.Queue;

public class SimpleCache {
    private Queue<String> cacheQueue = new ArrayDeque<>(3); // 缓存容量为3
    private static final int MAX_CAPACITY = 3;

    // 添加元素到缓存
    public void addToCache(String item) {
        if (cacheQueue.size() >= MAX_CAPACITY) {
            // 缓存满,移除最早加入的元素
            cacheQueue.poll();
        }
        cacheQueue.offer(item);
    }

    // 打印缓存中的元素
    public void printCache() {
        System.out.println("缓存中的元素:" + cacheQueue);
    }

    public static void main(String[] args) {
        SimpleCache cache = new SimpleCache();
        cache.addToCache("元素1");
        cache.printCache(); // 输出:缓存中的元素:[元素1]
        cache.addToCache("元素2");
        cache.printCache(); // 输出:缓存中的元素:[元素1, 元素2]
        cache.addToCache("元素3");
        cache.printCache(); // 输出:缓存中的元素:[元素1, 元素2, 元素3]
        cache.addToCache("元素4");
        cache.printCache(); // 输出:缓存中的元素:[元素2, 元素3, 元素4]
    }
}

运行上述代码,输出结果符合预期,展示了ArrayDeque作为队列在缓存场景中的应用。

四、优先级队列(PriorityQueue)

PriorityQueue是Java中实现优先级队列的类,它基于优先级堆(通常是最小堆)实现,元素按照优先级进行排序,而非插入顺序。在PriorityQueue中,每次出队的元素都是当前队列中优先级最高的元素。

4.1 PriorityQueue的特性

  • 数据结构:优先级堆(数组实现的二叉堆)。
  • 容量:无界,默认初始容量为11,当元素数量超过容量时会自动扩容。
  • 线程安全性:非线程安全,多线程环境下需要外部同步(或使用PriorityBlockingQueue)。
  • 元素允许:不允许null元素;如果元素没有实现Comparable接口且未指定比较器,会抛出ClassCastException;允许重复元素,但排序时会根据优先级处理。
  • 排序方式:默认按照元素的自然顺序(实现Comparable接口)排序,也可以通过构造方法指定Comparator来定义排序规则。

4.2 PriorityQueue的构造方法

PriorityQueue提供了多个构造方法:

  1. public PriorityQueue():创建一个初始容量为11的空优先级队列,按照元素的自然顺序排序。

    PriorityQueue<Integer> pq = new PriorityQueue<>();
    
  2. public PriorityQueue(int initialCapacity):创建一个指定初始容量的空优先级队列,按照元素的自然顺序排序。initialCapacity必须大于0,否则抛出IllegalArgumentException

    PriorityQueue<Integer> pq = new PriorityQueue<>(20);
    
  3. public PriorityQueue(Comparator<? super E> comparator):创建一个初始容量为11的空优先级队列,按照指定的比较器排序。

    // 创建一个按照降序排序的优先级队列
    PriorityQueue<Integer> pq = new PriorityQueue<>(Collections.reverseOrder());
    
  4. public PriorityQueue(int initialCapacity, Comparator<? super E> comparator):创建一个指定初始容量和比较器的空优先级队列。

    PriorityQueue<Integer> pq = new PriorityQueue<>(20, Collections.reverseOrder());
    
  5. public PriorityQueue(Collection<? extends E> c):创建一个包含指定集合元素的优先级队列。如果集合是SortedSet或另一个PriorityQueue,则按照其排序规则排序;否则,按照元素的自然顺序排序。

    List<Integer> list = Arrays.asList(3, 1, 2);
    PriorityQueue<Integer> pq = new PriorityQueue<>(list);
    
  6. public PriorityQueue(PriorityQueue<? extends E> c):创建一个包含指定PriorityQueue元素的优先级队列,使用与指定队列相同的排序规则。

    PriorityQueue<Integer> pq1 = new PriorityQueue<>();
    pq1.add(3);
    pq1.add(1);
    pq1.add(2);
    PriorityQueue<Integer> pq2 = new PriorityQueue<>(pq1);
    
  7. public PriorityQueue(SortedSet<? extends E> c):创建一个包含指定SortedSet元素的优先级队列,使用与指定集合相同的排序规则。

    SortedSet<Integer> set = new TreeSet<>();
    set.add(3);
    set.add(1);
    set.add(2);
    PriorityQueue<Integer> pq = new PriorityQueue<>(set);
    

4.3 PriorityQueue的常用方法

PriorityQueue实现了Queue接口的所有方法,同时还有一些与排序和堆操作相关的方法。以下是常用方法的详细说明:

4.3.1 入队方法
  1. boolean add(E e):将元素添加到优先级队列中,成功返回true。如果元素为null,抛出NullPointerException;如果元素无法比较(未实现Comparable且无比较器),抛出ClassCastException

    PriorityQueue<Integer> pq = new PriorityQueue<>();
    pq.add(3);
    pq.add(1);
    pq.add(2);
    System.out.println(pq); // 输出:[1, 3, 2](内部存储结构,并非排序后的完整列表)
    
  2. boolean offer(E e):与add()方法功能相同,将元素添加到优先级队列中,返回true。由于PriorityQueue是无界的,此方法始终返回true

    PriorityQueue<Integer> pq = new PriorityQueue<>();
    pq.offer(3);
    pq.offer(1);
    pq.offer(2);
    
4.3.2 出队方法
  1. E remove():移除并返回队列中优先级最高的元素(队头元素),队列为空时抛出NoSuchElementException

    PriorityQueue<Integer> pq = new PriorityQueue<>();
    pq.add(3);
    pq.add(1);
    pq.add(2);
    System.out.println(pq.remove()); // 输出:1
    System.out.println(pq); // 输出:[2, 3]
    
  2. E poll():移除并返回队列中优先级最高的元素,队列为空时返回null

    PriorityQueue<Integer> pq = new PriorityQueue<>();
    pq.add(3);
    pq.add(1);
    pq.add(2);
    System.out.println(pq.poll()); // 输出:1
    System.out.println(pq); // 输出:[2, 3]
    
4.3.3 查看队头方法
  1. E element():返回但不移除队列中优先级最高的元素,队列为空时抛出NoSuchElementException

    PriorityQueue<Integer> pq = new PriorityQueue<>();
    pq.add(3);
    pq.add(1);
    pq.add(2);
    System.out.println(pq.element()); // 输出:1
    
  2. E peek():返回但不移除队列中优先级最高的元素,队列为空时返回null

    PriorityQueue<Integer> pq = new PriorityQueue<>();
    pq.add(3);
    pq.add(1);
    pq.add(2);
    System.out.println(pq.peek()); // 输出:1
    
4.3.4 其他常用方法
  1. int size():返回队列中的元素个数。

    PriorityQueue<Integer> pq = new PriorityQueue<>();
    pq.add(1);
    pq.add(2);
    System.out.println(pq.size()); // 输出:2
    
  2. boolean isEmpty():判断队列是否为空。

    PriorityQueue<Integer> pq = new PriorityQueue<>();
    System.out.println(pq.isEmpty()); // 输出:true
    
  3. boolean contains(Object o):判断队列是否包含指定元素。

    PriorityQueue<Integer> pq = new PriorityQueue<>();
    pq.add(1);
    System.out.println(pq.contains(1)); // 输出:true
    System.out.println(pq.contains(2)); // 输出:false
    
  4. Iterator<E> iterator():返回队列的迭代器,用于遍历元素。注意,迭代器返回的元素顺序不一定是优先级顺序。

    PriorityQueue<Integer> pq = new PriorityQueue<>();
    pq.add(3);
    pq.add(1);
    pq.add(2);
    Iterator<Integer> iterator = pq.iterator();
    while (iterator.hasNext()) {
        System.out.print(iterator.next() + " "); // 可能输出:1 3 2(顺序不固定)
    }
    
  5. Object[] toArray():将队列中的元素转换为数组,数组中的元素顺序不一定是优先级顺序。

    PriorityQueue<Integer> pq = new PriorityQueue<>();
    pq.add(3);
    pq.add(1);
    pq.add(2);
    Object[] array = pq.toArray();
    System.out.println(Arrays.toString(array)); // 可能输出:[1, 3, 2]
    
  6. boolean remove(Object o):移除队列中第一个出现的指定元素(如果存在),返回true

    PriorityQueue<Integer> pq = new PriorityQueue<>();
    pq.add(3);
    pq.add(1);
    pq.add(2);
    pq.remove(3);
    System.out.println(pq); // 输出:[1, 2]
    
  7. void clear():清空队列中的所有元素。

    PriorityQueue<Integer> pq = new PriorityQueue<>();
    pq.add(1);
    pq.add(2);
    pq.clear();
    System.out.println(pq.isEmpty()); // 输出:true
    

4.4 PriorityQueue的底层实现原理

PriorityQueue底层基于二叉堆实现,二叉堆是一种完全二叉树,它可以分为最大堆和最小堆。PriorityQueue默认是最小堆,即每个父节点的值小于或等于其子节点的值,根节点(堆顶)是整个堆中最小的元素。

二叉堆通常使用数组来存储,对于数组中的索引为i的元素:

  • 其父节点的索引为(i - 1) >>> 1(无符号右移一位,等价于(i - 1) / 2)。
  • 其左子节点的索引为2 * i + 1
  • 其右子节点的索引为2 * i + 2

PriorityQueue的核心字段包括:

  • transient Object[] queue:存储元素的数组。
  • int size:队列中的元素个数。
  • private final Comparator<? super E> comparator:用于比较元素的比较器,null表示使用元素的自然顺序。
  • transient int modCount:用于快速失败机制的修改计数。
4.4.1 入队操作(offer()方法)

当调用offer(E e)方法添加元素时,会执行以下步骤:

  1. 检查元素是否为null,如果是则抛出NullPointerException
  2. 检查是否需要扩容(如果size等于数组长度)。
  3. 将元素添加到数组的末尾(索引为size的位置)。
  4. 调用siftUp()方法调整堆结构,以维持堆的特性。
  5. 增加size的值。

offer()方法的实现如下:

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1); // 扩容
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e); // 调整堆
    return true;
}

siftUp()方法用于将新添加的元素向上调整,以维持堆的特性:

private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}

// 使用元素的自然顺序(Comparable)调整
private void siftUpComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1; // 父节点索引
        Object e = queue[parent];
        if (key.compareTo((E) e) >= 0)
            break; // 新元素大于等于父节点,停止调整
        queue[k] = e; // 将父节点下移
        k = parent; // 继续向上调整
    }
    queue[k] = key;
}

// 使用指定的比较器调整
private void siftUpUsingComparator(int k, E x) {
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (comparator.compare(x, (E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = x;
}

siftUp()方法的过程是:将新元素与父节点比较,如果新元素小于父节点(对于最小堆),则交换它们的位置,然后继续与新的父节点比较,直到新元素大于等于父节点或到达根节点。

4.4.2 出队操作(poll()方法)

当调用poll()方法移除元素时,会执行以下步骤:

  1. 如果队列为空,返回null
  2. 获取根节点元素(优先级最高的元素)作为返回值。
  3. 将数组末尾的元素移到根节点位置。
  4. 调用siftDown()方法调整堆结构,以维持堆的特性。
  5. 减少size的值,并将原末尾元素的位置设为null(帮助GC)。

poll()方法的实现如下:

public E poll() {
    if (size == 0)
        return null;
    int s = --size;
    modCount++;
    E result = (E) queue[0]; // 根节点元素
    E x = (E) queue[s];
    queue[s] = null;
    if (s != 0)
        siftDown(0, x); // 调整堆
    return result;
}

siftDown()方法用于将根节点的元素向下调整,以维持堆的特性:

private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}

// 使用元素的自然顺序调整
private void siftDownComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>) x;
    int half = size >>> 1; // 非叶子节点的最大索引
    while (k < half) {
        int child = (k << 1) + 1; // 左子节点索引
        Object c = queue[child];
        int right = child + 1; // 右子节点索引
        if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
            c = queue[child = right]; // 选择左右子节点中较小的一个
        if (key.compareTo((E) c) <= 0)
            break; // 父节点小于等于子节点,停止调整
        queue[k] = c; // 将子节点上移
        k = child; // 继续向下调整
    }
    queue[k] = key;
}

// 使用指定的比较器调整
private void siftDownUsingComparator(int k, E x) {
    int half = size >>> 1;
    while (k < half) {
        int child = (k << 1) + 1;
        Object c = queue[child];
        int right = child + 1;
        if (right < size && comparator.compare((E) c, (E) queue[right]) > 0)
            c = queue[child = right];
        if (comparator.compare(x, (E) c) <= 0)
            break;
        queue[k] = c;
        k = child;
    }
    queue[k] = x;
}

siftDown()方法的过程是:将当前节点与左右子节点中较小的一个比较,如果当前节点大于该子节点,则交换它们的位置,然后继续与新的子节点比较,直到当前节点小于等于子节点或到达叶子节点。

4.4.3 扩容机制

当队列中的元素个数达到数组容量时,会调用grow()方法进行扩容:

private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    // 如果旧容量小于64,则容量翻倍+2;否则,容量增加50%
    int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                     (oldCapacity + 2) :
                                     (oldCapacity >> 1));
    // 检查是否超过最大容量
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 复制元素到新数组
    queue = Arrays.copyOf(queue, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // 溢出
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

扩容规则是:如果当前容量小于64,则新容量为旧容量的两倍加2;否则,新容量为旧容量的1.5倍。如果新容量超过了最大数组容量(Integer.MAX_VALUE - 8),则使用Integer.MAX_VALUE

4.5 优先级的定义方式

PriorityQueue中元素的优先级可以通过两种方式定义:

  1. 自然顺序:元素实现Comparable接口,并重写compareTo()方法。

    class Student implements Comparable<Student> {
        private String name;
        private int score;
    
        public Student(String name, int score) {
            this.name = name;
            this.score = score;
        }
    
        // 按照分数升序排序(分数低的优先级高)
        @Override
        public int compareTo(Student o) {
            return Integer.compare(this.score, o.score);
        }
    
        @Override
        public String toString() {
            return name + "(" + score + ")";
        }
    }
    
    public class PriorityQueueExample {
        public static void main(String[] args) {
            PriorityQueue<Student> pq = new PriorityQueue<>();
            pq.add(new Student("张三", 80));
            pq.add(new Student("李四", 60));
            pq.add(new Student("王五", 90));
    
            while (!pq.isEmpty()) {
                System.out.println(pq.poll()); // 依次输出:李四(60)、张三(80)、王五(90)
            }
        }
    }
    
  2. 指定比较器:通过构造方法传入Comparator对象,定义排序规则。

    class Student {
        private String name;
        private int score;
    
        public Student(String name, int score) {
            this.name = name;
            this.score = score;
        }
    
        public int getScore() {
            return score;
        }
    
        @Override
        public String toString() {
            return name + "(" + score + ")";
        }
    }
    
    public class PriorityQueueExample {
        public static void main(String[] args) {
            // 按照分数降序排序(分数高的优先级高)
            PriorityQueue<Student> pq = new PriorityQueue<>(
                Comparator.comparingInt(Student::getScore).reversed()
            );
            pq.add(new Student("张三", 80));
            pq.add(new Student("李四", 60));
            pq.add(new Student("王五", 90));
    
            while (!pq.isEmpty()) {
                System.out.println(pq.poll()); // 依次输出:王五(90)、张三(80)、李四(60)
            }
        }
    }
    

4.6 PriorityQueue的使用场景

PriorityQueue适用于需要按照优先级处理元素的场景,例如:

  1. 任务调度:根据任务的优先级(如紧急程度)处理任务,优先级高的任务先执行。
  2. 事件驱动模拟:在模拟系统中,按照事件发生的时间顺序处理事件,时间早的事件先处理。
  3. Top K问题:从大量元素中找出前K个最大或最小的元素,使用PriorityQueue可以高效实现。

以下是一个使用PriorityQueue解决Top K问题的示例:

import java.util.*;

public class TopKExample {
    // 找出数组中前K个最大的元素
    public static List<Integer> topK(int[] nums, int k) {
        // 使用最小堆,堆的大小保持为k
        PriorityQueue<Integer> pq = new PriorityQueue<>(k);
        for (int num : nums) {
            if (pq.size() < k) {
                pq.offer(num);
            } else if (num > pq.peek()) {
                // 如果当前元素大于堆顶元素,替换堆顶元素
                pq.poll();
                pq.offer(num);
            }
        }
        // 将堆中的元素转换为列表并反转(得到从大到小的顺序)
        List<Integer> result = new ArrayList<>(pq);
        Collections.reverse(result);
        return result;
    }

    public static void main(String[] args) {
        int[] nums = {3, 1, 4, 1, 5, 9, 2, 6};
        int k = 3;
        List<Integer> topK = topK(nums, k);
        System.out.println("前" + k + "个最大的元素:" + topK); // 输出:前3个最大的元素:[9, 6, 5]
    }
}

上述代码中,使用一个容量为k的最小堆来存储前k个最大的元素。对于每个元素,如果堆的大小小于k,则直接加入堆中;否则,如果当前元素大于堆顶元素(堆中最小的元素),则替换堆顶元素。最后,堆中的元素就是前k个最大的元素,反转后得到从大到小的顺序。

五、双端队列(Deque)及其实现类

5.1 Deque接口详解

Deque(Double-Ended Queue,双端队列)接口继承自Queue接口,它允许在队列的两端进行元素的插入和删除操作。因此,Deque既可以作为普通队列(FIFO)使用,也可以作为栈(LIFO,后进先出)使用。

5.1.1 Deque接口的定义

Deque接口的定义如下:

public interface Deque<E> extends Queue<E> {
    // 向队头添加元素,失败抛异常
    void addFirst(E e);
    
    // 向队尾添加元素,失败抛异常
    void addLast(E e);
    
    // 向队头添加元素,失败返回false
    boolean offerFirst(E e);
    
    // 向队尾添加元素,失败返回false
    boolean offerLast(E e);
    
    // 移除并返回队头元素,失败抛异常
    E removeFirst();
    
    // 移除并返回队尾元素,失败抛异常
    E removeLast();
    
    // 移除并返回队头元素,失败返回null
    E pollFirst();
    
    // 移除并返回队尾元素,失败返回null
    E pollLast();
    
    // 返回但不移除队头元素,失败抛异常
    E getFirst();
    
    // 返回但不移除队尾元素,失败抛异常
    E getLast();
    
    // 返回但不移除队头元素,失败返回null
    E peekFirst();
    
    // 返回但不移除队尾元素,失败返回null
    E peekLast();
    
    // 移除第一次出现的指定元素
    boolean removeFirstOccurrence(Object o);
    
    // 移除最后一次出现的指定元素
    boolean removeLastOccurrence(Object o);
    
    // 以下方法与Queue接口的方法等价
    boolean add(E e); // 等价于addLast(e)
    boolean offer(E e); // 等价于offerLast(e)
    E remove(); // 等价于removeFirst()
    E poll(); // 等价于pollFirst()
    E element(); // 等价于getFirst()
    E peek(); // 等价于peekFirst()
    
    // 以下方法与栈相关
    void push(E e); // 等价于addFirst(e)
    E pop(); // 等价于removeFirst()
    
    // 其他继承自Collection的方法
    // ...
}

从定义可以看出,Deque接口在Queue接口的基础上,增加了一系列用于操作队列两端的方法,以及与栈相关的push()pop()方法。

5.1.2 Deque接口的方法分类

Deque接口的方法可以分为以下几类:

  1. 向两端添加元素

    • addFirst(E e):向队头添加元素,失败抛异常。
    • addLast(E e):向队尾添加元素,失败抛异常(与add(E e)等价)。
    • offerFirst(E e):向队头添加元素,失败返回false
    • offerLast(E e):向队尾添加元素,失败返回false(与offer(E e)等价)。
  2. 从两端移除元素

    • removeFirst():移除并返回队头元素,失败抛异常(与remove()等价)。
    • removeLast():移除并返回队尾元素,失败抛异常。
    • pollFirst():移除并返回队头元素,失败返回null(与poll()等价)。
    • pollLast():移除并返回队尾元素,失败返回null
  3. 查看两端元素

    • getFirst():返回但不移除队头元素,失败抛异常(与element()等价)。
    • getLast():返回但不移除队尾元素,失败抛异常。
    • peekFirst():返回但不移除队头元素,失败返回null(与peek()等价)。
    • peekLast():返回但不移除队尾元素,失败返回null
  4. 移除指定元素

    • removeFirstOccurrence(Object o):移除第一次出现的指定元素。
    • removeLastOccurrence(Object o):移除最后一次出现的指定元素。
  5. 栈操作

    • push(E e):向栈顶(队头)添加元素,等价于addFirst(e)
    • pop(E e):移除并返回栈顶(队头)元素,等价于removeFirst()

5.2 ArrayDeque(双端队列实现)

前面已经介绍过ArrayDeque可以作为普通队列使用,实际上它是Deque接口的一个主要实现类,也是双端队列的推荐实现。

5.2.1 ArrayDeque作为双端队列的常用方法

除了作为普通队列使用的方法外,ArrayDeque作为双端队列使用时,还常用以下方法:

  1. void addFirst(E e):向队头添加元素,元素为null时抛出NullPointerException

    Deque<String> deque = new ArrayDeque<>();
    deque.addFirst("a");
    deque.addFirst("b");
    System.out.println(deque); // 输出:[b, a]
    
  2. void addLast(E e):向队尾添加元素,与add(E e)等价。

    Deque<String> deque = new ArrayDeque<>();
    deque.addLast("a");
    deque.addLast("b");
    System.out.println(deque); // 输出:[a, b]
    
  3. boolean offerFirst(E e):向队头添加元素,返回true,元素为null时抛出NullPointerException

    Deque<String> deque = new ArrayDeque<>();
    deque.offerFirst("a");
    deque.offerFirst("b");
    System.out.println(deque); // 输出:[b, a]
    
  4. boolean offerLast(E e):向队尾添加元素,返回true,与offer(E e)等价。

    Deque<String> deque = new ArrayDeque<>();
    deque.offerLast("a");
    deque.offerLast("b");
    System.out.println(deque); // 输出:[a, b]
    
  5. E removeFirst():移除并返回队头元素,队列为空时抛出NoSuchElementException

    Deque<String> deque = new ArrayDeque<>();
    deque.add("a");
    deque.add("b");
    System.out.println(deque.removeFirst()); // 输出:a
    
  6. E removeLast():移除并返回队尾元素,队列为空时抛出NoSuchElementException

    Deque<String> deque = new ArrayDeque<>();
    deque.add("a");
    deque.add("b");
    System.out.println(deque.removeLast()); // 输出:b
    
  7. E pollFirst():移除并返回队头元素,队列为空时返回null

    Deque<String> deque = new ArrayDeque<>();
    deque.add("a");
    deque.add("b");
    System.out.println(deque.pollFirst()); // 输出:a
    
  8. E pollLast():移除并返回队尾元素,队列为空时返回null

    Deque<String> deque = new ArrayDeque<>();
    deque.add("a");
    deque.add("b");
    System.out.println(deque.pollLast()); // 输出:b
    
  9. E getFirst():返回但不移除队头元素,队列为空时抛出NoSuchElementException

    Deque<String> deque = new ArrayDeque<>();
    deque.add("a");
    System.out.println(deque.getFirst()); // 输出:a
    
  10. E getLast():返回但不移除队尾元素,队列为空时抛出NoSuchElementException

    Deque<String> deque = new ArrayDeque<>();
    deque.add("a");
    deque.add("b");
    System.out.println(deque.getLast()); // 输出:b
    
  11. E peekFirst():返回但不移除队头元素,队列为空时返回null

    Deque<String> deque = new ArrayDeque<>();
    deque.add("a");
    System.out.println(deque.peekFirst()); // 输出:a
    
  12. E peekLast():返回但不移除队尾元素,队列为空时返回null

    Deque<String> deque = new ArrayDeque<>();
    deque.add("a");
    deque.add("b");
    System.out.println(deque.peekLast()); // 输出:b
    
  13. boolean removeFirstOccurrence(Object o):移除第一次出现的指定元素。

    Deque<String> deque = new ArrayDeque<>();
    deque.add("a");
    deque.add("b");
    deque.add("a");
    deque.removeFirstOccurrence("a");
    System.out.println(deque); // 输出:[b, a]
    
  14. boolean removeLastOccurrence(Object o):移除最后一次出现的指定元素。

    Deque<String> deque = new ArrayDeque<>();
    deque.add("a");
    deque.add("b");
    deque.add("a");
    deque.removeLastOccurrence("a");
    System.out.println(deque); // 输出:[a, b]
    
  15. void push(E e):向栈顶(队头)添加元素,等价于addFirst(e)

    Deque<String> stack = new ArrayDeque<>();
    stack.push("a");
    stack.push("b");
    System.out.println(stack); // 输出:[b, a]
    
  16. E pop():移除并返回栈顶(队头)元素,等价于removeFirst()

    Deque<String> stack = new ArrayDeque<>();
    stack.push("a");
    stack.push("b");
    System.out.println(stack.pop()); // 输出:b
    
5.2.2 ArrayDeque作为栈的使用

由于Deque接口提供了push()pop()方法,ArrayDeque可以方便地作为栈使用,且性能通常优于java.util.Stack类(Stack是遗留类,继承自Vector,性能较差)。

以下是使用ArrayDeque作为栈的示例:

import java.util.ArrayDeque;
import java.util.Deque;

public class StackExample {
    public static void main(String[] args) {
        Deque<Integer> stack = new ArrayDeque<>();
        
        // 入栈
        stack.push(1);
        stack.push(2);
        stack.push(3);
        System.out.println("栈中的元素:" + stack); // 输出:栈中的元素:[3, 2, 1]
        
        // 查看栈顶元素
        System.out.println("栈顶元素:" + stack.peek()); // 输出:栈顶元素:3
        
        // 出栈
        System.out.println("出栈元素:" + stack.pop()); // 输出:出栈元素:3
        System.out.println("栈中的元素:" + stack); // 输出:栈中的元素:[2, 1]
        
        // 再次出栈
        System.out.println("出栈元素:" + stack.pop()); // 输出:出栈元素:2
        System.out.println("栈中的元素:" + stack); // 输出:栈中的元素:[1]
    }
}

5.3 LinkedList(双端队列实现)

LinkedList也实现了Deque接口,因此也可以作为双端队列使用。其作为双端队列的方法与ArrayDeque类似,但底层实现不同(链表 vs 数组)。

5.3.1 LinkedList与ArrayDeque的对比(作为双端队列)
特性ArrayDequeLinkedList
数据结构动态数组双向链表
内存占用较低较高(每个节点有额外引用)
两端插入/删除效率高(数组操作,除非需要扩容)高(链表节点操作)
随机访问效率高(O(1))低(O(n))
是否允许null元素不允许允许
栈操作性能较高

在大多数情况下,ArrayDeque作为双端队列或栈使用时性能优于LinkedList,因此推荐优先使用ArrayDeque

六、阻塞队列(BlockingQueue)及其实现类

阻塞队列(BlockingQueue)是Java并发包(java.util.concurrent)中的重要组件,它继承自Queue接口,专门用于多线程环境。阻塞队列的特点是:当队列满时,入队操作会阻塞;当队列空时,出队操作会阻塞,直到队列有空间或有元素可用。

6.1 BlockingQueue接口详解

BlockingQueue接口扩展了Queue接口,增加了支持阻塞的入队和出队方法。

6.1.1 BlockingQueue接口的核心方法

BlockingQueue的核心方法可以分为以下几类:

  1. 阻塞式入队

    • void put(E e):将元素添加到队列尾部,如果队列满则阻塞,直到队列有空间。如果元素为null,抛出NullPointerException;如果线程被中断,抛出InterruptedException
  2. 超时阻塞式入队

    • boolean offer(E e, long timeout, TimeUnit unit):将元素添加到队列尾部,如果队列满则等待指定的时间,如果在指定时间内队列有空间则添加成功并返回true,否则返回false。可能抛出NullPointerExceptionInterruptedException
  3. 阻塞式出队

    • E take():移除并返回队列头部元素,如果队列为空则阻塞,直到队列有元素。如果线程被中断,抛出InterruptedException
  4. 超时阻塞式出队

    • E poll(long timeout, TimeUnit unit):移除并返回队列头部元素,如果队列为空则等待指定的时间,如果在指定时间内队列有元素则返回该元素,否则返回null。可能抛出InterruptedException
  5. 其他方法

    • int remainingCapacity():返回队列剩余的容量(即还能添加多少元素)。
    • boolean remove(Object o):移除队列中第一个出现的指定元素,返回true如果元素被移除。
    • boolean contains(Object o):判断队列是否包含指定元素。
    • int drainTo(Collection<? super E> c):将队列中的所有元素移到指定集合中,返回移动的元素个数。
    • int drainTo(Collection<? super E> c, int maxElements):将队列中的最多maxElements个元素移到指定集合中,返回移动的元素个数。

BlockingQueue不允许添加null元素,否则会抛出NullPointerException。此外,BlockingQueue通常用于生产者-消费者模式,生产者线程向队列中添加元素,消费者线程从队列中取出元素。

6.1.2 BlockingQueue的实现类

BlockingQueue有多个实现类,各有不同的特性:

  1. ArrayBlockingQueue:基于数组实现的有界阻塞队列。
  2. LinkedBlockingQueue:基于链表实现的可选有界阻塞队列(默认无界)。
  3. PriorityBlockingQueue:基于优先级堆实现的无界阻塞优先级队列。
  4. DelayQueue:基于优先级堆实现的无界阻塞延迟队列,元素只有在延迟时间到期后才能被取出。
  5. SynchronousQueue:不存储元素的阻塞队列,每个入队操作必须等待一个出队操作,反之亦然。
  6. LinkedTransferQueue:基于链表实现的无界阻塞队列,支持元素的直接传递。
  7. LinkedBlockingDeque:基于链表实现的双向阻塞队列。

接下来,我们将详细讲解这些实现类。

6.2 ArrayBlockingQueue

ArrayBlockingQueue是基于数组实现的有界阻塞队列,它的容量在创建时确定,之后不能更改。ArrayBlockingQueue内部使用重入锁(ReentrantLock)和条件变量(Condition)来保证线程安全。

6.2.1 ArrayBlockingQueue的特性
  • 数据结构:数组,容量固定。
  • 容量:有界,创建时指定,不可更改。
  • 线程安全性:线程安全,内部使用单一锁保证。
  • 元素排序:按照FIFO原则排序。
  • 公平性:可以指定是否为公平队列。公平队列保证线程按照等待顺序获取锁,而非公平队列则不保证,后者吞吐量通常更高。
6.2.2 ArrayBlockingQueue的构造方法
  1. public ArrayBlockingQueue(int capacity):创建一个指定容量的ArrayBlockingQueue,默认是非公平的。

    BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
    
  2. public ArrayBlockingQueue(int capacity, boolean fair):创建一个指定容量和公平性的ArrayBlockingQueuefairtrue表示公平队列,false表示非公平队列。

    BlockingQueue<String> queue = new ArrayBlockingQueue<>(10, true); // 公平队列
    
  3. public ArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c):创建一个指定容量、公平性,并包含指定集合元素的ArrayBlockingQueue。集合中的元素会按照迭代器顺序添加到队列中,如果集合元素个数超过容量,会抛出IllegalArgumentException

    List<String> list = Arrays.asList("a", "b", "c");
    BlockingQueue<String> queue = new ArrayBlockingQueue<>(10, false, list);
    
6.2.3 ArrayBlockingQueue的常用方法

除了BlockingQueue接口定义的方法外,ArrayBlockingQueue还继承了QueueCollection接口的方法,以下是常用方法的示例:

  1. void put(E e)

    BlockingQueue<String> queue = new ArrayBlockingQueue<>(2);
    try {
        queue.put("a");
        queue.put("b");
        // 队列已满,以下代码会阻塞
        // queue.put("c");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
  2. boolean offer(E e, long timeout, TimeUnit unit)

    BlockingQueue<String> queue = new ArrayBlockingQueue<>(2);
    try {
        queue.offer("a");
        queue.offer("b");
        // 队列已满,等待1秒
        boolean result = queue.offer("c", 1, TimeUnit.SECONDS);
        System.out.println(result); // 输出:false(1秒内队列仍满)
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
  3. E take()

    BlockingQueue<String> queue = new ArrayBlockingQueue<>(2);
    new Thread(() -> {
        try {
            Thread.sleep(1000); // 休眠1秒后添加元素
            queue.put("a");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
    
    try {
        // 队列为空,会阻塞直到有元素
        String element = queue.take();
        System.out.println(element); // 输出:a
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
  4. E poll(long timeout, TimeUnit unit)

    BlockingQueue<String> queue = new ArrayBlockingQueue<>(2);
    try {
        // 队列为空,等待1秒
        String element = queue.poll(1, TimeUnit.SECONDS);
        System.out.println(element); // 输出:null(1秒内队列仍空)
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
  5. int remainingCapacity()

    BlockingQueue<String> queue = new ArrayBlockingQueue<>(2);
    queue.offer("a");
    System.out.println(queue.remainingCapacity()); // 输出:1
    
6.2.4 ArrayBlockingQueue的底层实现原理

ArrayBlockingQueue的核心字段包括:

  • final Object[] items:存储元素的数组。
  • int takeIndex:下一个出队元素的索引。
  • int putIndex:下一个入队元素的索引。
  • int count:队列中的元素个数。
  • final ReentrantLock lock:保证线程安全的重入锁。
  • private final Condition notEmpty:当队列空时,等待的条件变量(供消费者使用)。
  • private final Condition notFull:当队列满时,等待的条件变量(供生产者使用)。

put(E e)方法的实现如下:

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly(); // 可中断地获取锁
    try {
        while (count == items.length)
            notFull.await(); // 队列满,等待
        enqueue(e); // 入队
    } finally {
        lock.unlock();
    }
}

private void enqueue(E x) {
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
        putIndex = 0; // 循环数组
    count++;
    notEmpty.signal(); // 唤醒等待的消费者线程
}

take()方法的实现如下:

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await(); // 队列空,等待
        return dequeue(); // 出队
    } finally {
        lock.unlock();
    }
}

private E dequeue() {
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        takeIndex = 0; // 循环数组
    count--;
    notFull.signal(); // 唤醒等待的生产者线程
    return x;
}

从实现可以看出,ArrayBlockingQueue使用单一锁来控制所有访问,因此生产者和消费者线程会竞争同一把锁,在高并发场景下可能会有一定的性能瓶颈。

6.2.5 ArrayBlockingQueue的使用场景

ArrayBlockingQueue适用于以下场景:

  • 已知队列最大容量的多线程场景。
  • 生产者和消费者之间需要同步的场景,如线程池中的任务队列(部分线程池实现使用了类似的原理)。

以下是一个使用ArrayBlockingQueue实现生产者-消费者模式的示例:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class ProducerConsumer {
    private static final int QUEUE_CAPACITY = 5;
    private static final int NUM_ITEMS = 10;

    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(QUEUE_CAPACITY);

        // 生产者线程
        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < NUM_ITEMS; i++) {
                    System.out.println("生产者生产:" + i);
                    queue.put(i);
                    Thread.sleep(100); // 模拟生产耗时
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            try {
                for (int i = 0; i < NUM_ITEMS; i++) {
                    int item = queue.take();
                    System.out.println("消费者消费:" + item);
                    Thread.sleep(200); // 模拟消费耗时
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        producer.start();
        consumer.start();
    }
}

在这个示例中,生产者线程不断生产元素并放入队列,消费者线程不断从队列中取出元素消费。由于消费者消费速度慢于生产者生产速度,当队列满时,生产者会阻塞等待,直到消费者消费了元素腾出空间。

6.3 LinkedBlockingQueue

LinkedBlockingQueue是基于链表实现的阻塞队列,它可以是有界的也可以是无界的(默认无界)。与ArrayBlockingQueue相比,LinkedBlockingQueue在高并发场景下通常具有更高的吞吐量,因为它使用两把锁(分别控制入队和出队),生产者和消费者可以并行操作。

6.3.1 LinkedBlockingQueue的特性
  • 数据结构:单向链表(节点包含元素和下一个节点的引用)。
  • 容量:可选有界,默认值为Integer.MAX_VALUE(无界)。
  • 线程安全性:线程安全,内部使用两把锁(takeLockputLock)分别控制入队和出队操作。
  • 元素排序:按照FIFO原则排序。
  • 内存占用:每个元素需要额外的节点对象,内存占用高于ArrayBlockingQueue
6.3.2 LinkedBlockingQueue的构造方法
  1. public LinkedBlockingQueue():创建一个默认容量为Integer.MAX_VALUE的无界LinkedBlockingQueue

    BlockingQueue<String> queue = new LinkedBlockingQueue<>();
    
  2. public LinkedBlockingQueue(int capacity):创建一个指定容量的有界LinkedBlockingQueuecapacity必须大于0,否则抛出IllegalArgumentException

    BlockingQueue<String> queue = new LinkedBlockingQueue<>(10);
    
  3. public LinkedBlockingQueue(Collection<? extends E> c):创建一个包含指定集合元素的LinkedBlockingQueue,容量为集合元素个数与Integer.MAX_VALUE中的较小值。

    List<String> list = Arrays.asList("a", "b", "c");
    BlockingQueue<String> queue = new LinkedBlockingQueue<>(list);
    
6.3.3 LinkedBlockingQueue的常用方法

LinkedBlockingQueue的常用方法与ArrayBlockingQueue类似,包括put()offer()take()poll()等,以下是一些示例:

  1. void put(E e)

    BlockingQueue<String> queue = new LinkedBlockingQueue<>(2);
    try {
        queue.put("a");
        queue.put("b");
        // 队列已满,以下代码会阻塞
        // queue.put("c");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
  2. E take()

    BlockingQueue<String> queue = new LinkedBlockingQueue<>(2);
    new Thread(() -> {
        try {
            Thread.sleep(1000);
            queue.put("a");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
    
    try {
        String element = queue.take();
        System.out.println(element); // 输出:a
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
6.3.4 LinkedBlockingQueue的底层实现原理

LinkedBlockingQueue的核心字段包括:

  • private final int capacity:队列的容量。
  • private final AtomicInteger count:队列中的元素个数(使用原子类保证线程安全)。
  • transient Node<E> head:头节点(头节点不存储元素,其下一个节点是第一个元素)。
  • transient Node<E> last:尾节点。
  • private final ReentrantLock takeLock:控制出队操作的锁。
  • private final Condition notEmpty:与takeLock关联的条件变量,用于等待元素。
  • private final ReentrantLock putLock:控制入队操作的锁。
  • private final Condition notFull:与putLock关联的条件变量,用于等待空间。

节点的定义如下:

static class Node<E> {
    E item;
    Node<E> next;
    Node(E x) { item = x; }
}

put(E e)方法的实现如下:

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 {
        // 队列满,等待
        while (count.get() == capacity) {
            notFull.await();
        }
        enqueue(node); // 入队
        c = count.getAndIncrement();
        // 如果还有空间,唤醒其他生产者
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        putLock.unlock();
    }
    // 如果队列从空变为非空,唤醒消费者
    if (c == 0)
        signalNotEmpty();
}

private void enqueue(Node<E> node) {
    last = last.next = node;
}

private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
}

take()方法的实现如下:

public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        // 队列空,等待
        while (count.get() == 0) {
            notEmpty.await();
        }
        x = dequeue(); // 出队
        c = count.getAndDecrement();
        // 如果还有元素,唤醒其他消费者
        if (c > 1)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    // 如果队列从满变为非满,唤醒生产者
    if (c == capacity)
        signalNotFull();
    return x;
}

private E dequeue() {
    Node<E> h = head;
    Node<E> first = h.next;
    h.next = h; // 帮助GC
    head = first;
    E x = first.item;
    first.item = null;
    return x;
}

private void signalNotFull() {
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        notFull.signal();
    } finally {
        putLock.unlock();
    }
}

由于LinkedBlockingQueue使用两把锁分离了入队和出队操作,生产者和消费者可以同时操作队列,因此在高并发场景下通常比ArrayBlockingQueue具有更高的吞吐量。

6.3.5 LinkedBlockingQueue的使用场景

LinkedBlockingQueue适用于以下场景:

  • 队列容量较大或不确定的多线程场景。
  • 生产者和消费者并发程度较高的场景,如日志收集、消息传递等。

LinkedBlockingQueue是Java线程池(如ThreadPoolExecutor)中常用的任务队列之一。

6.4 PriorityBlockingQueue

PriorityBlockingQueue是基于优先级堆实现的无界阻塞优先级队列,它继承自AbstractQueue并实现了BlockingQueue接口。与PriorityQueue类似,PriorityBlockingQueue中的元素按照优先级排序,但它是线程安全的,适用于多线程环境。

6.4.1 PriorityBlockingQueue的特性
  • 数据结构:优先级堆(数组实现的二叉堆)。
  • 容量:无界,初始容量为11,当元素数量超过容量时会自动扩容。
  • 线程安全性:线程安全,内部使用重入锁(ReentrantLock)保证。
  • 元素排序:默认按照元素的自然顺序排序,也可以通过构造方法指定Comparator
  • 阻塞特性:当队列为空时,出队操作(take())会阻塞,入队操作永远不会阻塞(因为是无界队列)。
6.4.2 PriorityBlockingQueue的构造方法
  1. public PriorityBlockingQueue():创建一个初始容量为11,按照元素自然顺序排序的PriorityBlockingQueue

    BlockingQueue<Integer> queue = new PriorityBlockingQueue<>();
    
  2. public PriorityBlockingQueue(int initialCapacity):创建一个指定初始容量,按照元素自然顺序排序的PriorityBlockingQueueinitialCapacity必须大于0,否则抛出IllegalArgumentException

    BlockingQueue<Integer> queue = new PriorityBlockingQueue<>(20);
    
  3. public PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator):创建一个指定初始容量和比较器的PriorityBlockingQueue

    BlockingQueue<Integer> queue = new PriorityBlockingQueue<>(20, Collections.reverseOrder());
    
  4. public PriorityBlockingQueue(Collection<? extends E> c):创建一个包含指定集合元素的PriorityBlockingQueue。如果集合是SortedSetPriorityQueue,则按照其排序规则排序;否则,按照元素的自然顺序排序。

    List<Integer> list = Arrays.asList(3, 1, 2);
    BlockingQueue<Integer> queue = new PriorityBlockingQueue<>(list);
    
6.4.3 PriorityBlockingQueue的常用方法

PriorityBlockingQueue的常用方法包括put()offer()take()poll()等,其中入队方法不会阻塞(因为是无界队列),出队方法在队列为空时会阻塞。

  1. void put(E e):由于队列无界,此方法不会阻塞,等价于offer(e)

    BlockingQueue<Integer> queue = new PriorityBlockingQueue<>();
    try {
        queue.put(3);
        queue.put(1);
        queue.put(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
  2. E take():移除并返回优先级最高的元素,队列为空时阻塞。

    BlockingQueue<Integer> queue = new PriorityBlockingQueue<>();
    new Thread(() -> {
        try {
            Thread.sleep(1000);
            queue.put(3);
            queue.put(1);
            queue.put(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
    
    try {
        System.out.println(queue.take()); // 输出:1(优先级最高)
        System.out.println(queue.take()); // 输出:2
        System.out.println(queue.take()); // 输出:3
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
6.4.4 PriorityBlockingQueue的底层实现原理

PriorityBlockingQueue的核心字段包括:

  • private transient Object[] queue:存储元素的数组(堆)。
  • private transient int size:元素个数。
  • private transient Comparator<? super E> comparator:比较器。
  • private final ReentrantLock lock:重入锁。
  • private final Condition notEmpty:条件变量,用于等待元素。
  • private transient volatile int allocationSpinLock:用于扩容时的自旋锁。

put(E e)方法的实现如下(实际上调用了offer(e)):

public void put(E e) {
    offer(e); // never need to block
}

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    lock.lock();
    int n, cap;
    Object[] array;
    // 如果元素个数达到当前容量,扩容
    while ((n = size) >= (cap = (array = queue).length))
        tryGrow(array, cap);
    try {
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
            siftUpComparable(n, e, array);
        else
            siftUpUsingComparator(n, e, array, cmp);
        size = n + 1;
        notEmpty.signal();
    } finally {
        lock.unlock();
    }
    return true;
}

take()方法的实现如下:

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    E result;
    try {
        while ( (result = dequeue()) == null )
            notEmpty.await();
    } finally {
        lock.unlock();
    }
    return result;
}

private E dequeue() {
    int n = size - 1;
    if (n < 0)
        return null;
    else {
        Object[] array = queue;
        E result = (E) array[0];
        E x = (E) array[n];
        array[n] = null;
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
            siftDownComparable(0, x, array, n);
        else
            siftDownUsingComparator(0, x, array, n, cmp);
        size = n;
        return result;
    }
}

PriorityBlockingQueue的扩容机制与PriorityQueue类似,但使用了自旋锁来保证扩容的线程安全:

private void tryGrow(Object[] array, int oldCap) {
    lock.unlock(); // 释放主锁,允许其他线程操作
    Object[] newArray = null;
    if (allocationSpinLock == 0 &&
        UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset, 0, 1)) {
        try {
            int newCap = oldCap + ((oldCap < 64) ?
                                   (oldCap + 2) : // grow faster if small
                                   (oldCap >> 1));
            if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow
                int minCap = oldCap + 1;
                if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                    throw new OutOfMemoryError();
                newCap = MAX_ARRAY_SIZE;
            }
            if (newCap > oldCap && queue == array)
                newArray = new Object[newCap];
        } finally {
            allocationSpinLock = 0;
        }
    }
    // 如果其他线程正在扩容,当前线程让出CPU
    if (newArray == null) // back off if another thread is allocating
        Thread.yield();
    lock.lock();
    if (newArray != null && queue == array) {
        queue = newArray;
        System.arraycopy(array, 0, newArray, 0, oldCap);
    }
}
6.4.5 PriorityBlockingQueue的使用场景

PriorityBlockingQueue适用于多线程环境下需要按照优先级处理元素的场景,例如:

  • 多线程任务调度,优先级高的任务先执行。
  • 多线程环境下的Top K问题处理。

以下是一个使用PriorityBlockingQueue实现多线程任务调度的示例:

import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;

class Task implements Comparable<Task> {
    private String name;
    private int priority; // 优先级,值越小优先级越高

    public Task(String name, int priority) {
        this.name = name;
        this.priority = priority;
    }

    public void execute() {
        System.out.println("执行任务:" + name + ",优先级:" + priority);
    }

    @Override
    public int compareTo(Task o) {
        return Integer.compare(this.priority, o.priority);
    }
}

public class PriorityTaskScheduler {
    private PriorityBlockingQueue<Task> taskQueue = new PriorityBlockingQueue<>();
    private volatile boolean isRunning = true;

    public void start() {
        new Thread(() -> {
            while (isRunning) {
                try {
                    Task task = taskQueue.take(); // 阻塞等待任务
                    task.execute();
                    TimeUnit.MILLISECONDS.sleep(500); // 模拟执行耗时
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    isRunning = false;
                }
            }
        }).start();
    }

    public void addTask(Task task) {
        taskQueue.offer(task);
    }

    public void stop() {
        isRunning = false;
    }

    public static void main(String[] args) throws InterruptedException {
        PriorityTaskScheduler scheduler = new PriorityTaskScheduler();
        scheduler.start();

        // 添加任务
        scheduler.addTask(new Task("任务1", 3));
        scheduler.addTask(new Task("任务2", 1));
        scheduler.addTask(new Task("任务3", 2));

        // 等待所有任务执行完成
        TimeUnit.SECONDS.sleep(3);
        scheduler.stop();
    }
}

运行上述代码,输出结果为:

执行任务:任务2,优先级:1
执行任务:任务3,优先级:2
执行任务:任务1,优先级:3

可以看到,任务按照优先级从高到低的顺序执行,符合PriorityBlockingQueue的特性。

(由于篇幅限制,其他阻塞队列实现类的详细讲解在此省略,如需了解可进一步扩展)

七、并发队列(ConcurrentQueue)及其实现类

并发队列是指支持多线程并发操作的队列,它们通常通过非阻塞算法(如CAS操作)来保证线程安全,避免了锁竞争带来的性能开销,因此在高并发场景下具有更好的性能。

7.1 ConcurrentQueue接口

ConcurrentQueue接口是Java并发包中所有并发队列的父接口,它继承自Queue接口,定义了并发队列的基本特性,但没有添加新的方法:

public interface ConcurrentQueue<E> extends Queue<E> {
}

ConcurrentQueue的主要实现类有ConcurrentLinkedQueueConcurrentLinkedDeque

7.2 ConcurrentLinkedQueue

ConcurrentLinkedQueue是基于链表实现的无界并发队列,它使用CAS(Compare-And-Swap)非阻塞算法来保证线程安全,适用于高并发场景。

7.2.1 ConcurrentLinkedQueue的特性
  • 数据结构:单向链表(节点包含元素、下一个节点的引用等)。
  • 容量:无界,理论上可以无限添加元素(受限于内存)。
  • 线程安全性:线程安全,使用CAS操作保证,无锁机制。
  • 元素排序:按照FIFO原则排序。
  • 元素允许:不允许null元素。
  • 性能:高并发场景下性能优异,避免了锁竞争。
7.2.2 ConcurrentLinkedQueue的构造方法
  1. public ConcurrentLinkedQueue():创建一个空的ConcurrentLinkedQueue

    ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
    
  2. public ConcurrentLinkedQueue(Collection<? extends E> c):创建一个包含指定集合元素的ConcurrentLinkedQueue,元素顺序与集合的迭代器返回顺序一致。

    List<String> list = Arrays.asList("a", "b", "c");
    ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>(list);
    
7.2.3 ConcurrentLinkedQueue的常用方法

ConcurrentLinkedQueue实现了Queue接口的所有方法,常用方法包括:

  1. boolean add(E e):向队尾添加元素,由于是无界队列,始终返回true,元素为null时抛出NullPointerException

    ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
    queue.add("a");
    queue.add("b");
    
  2. boolean offer(E e):与add()方法功能相同,向队尾添加元素,返回true

    ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
    queue.offer("a");
    queue.offer("b");
    
  3. E poll():移除并返回队头元素,队列为空时返回null

    ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
    queue.add("a");
    System.out.println(queue.poll()); // 输出:a
    
  4. E peek():返回但不移除队头元素,队列为空时返回null

    ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
    queue.add("a");
    System.out.println(queue.peek()); // 输出:a
    
  5. boolean isEmpty():判断队列是否为空。

    ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
    System.out.println(queue.isEmpty()); // 输出:true
    
  6. int size():返回队列中的元素个数。注意,在并发环境下,size()方法的结果可能不准确,因为在计算过程中队列可能被其他线程修改。

    ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
    queue.add("a");
    queue.add("b");
    System.out.println(queue.size()); // 输出:2
    
7.2.4 ConcurrentLinkedQueue的底层实现原理

ConcurrentLinkedQueue的核心字段包括:

  • private transient volatile Node<E> head:头节点。
  • private transient volatile Node<E> tail:尾节点。

节点的定义如下(简化版):

private static class Node<E> {
    volatile E item;
    volatile Node<E> next;

    Node(E item) {
        UNSAFE.putObject(this, itemOffset, item);
    }

    boolean casItem(E cmp, E val) {
        return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
    }

    void lazySetNext(Node<E> val) {
        UNSAFE.putOrderedObject(this, nextOffset, val);
    }

    boolean casNext(Node<E> cmp, Node<E> val) {
        return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
    }

    // 静态字段偏移量,用于UNSAFE操作
    private static final sun.misc.Unsafe UNSAFE;
    private static final long itemOffset;
    private static final long nextOffset;

    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> k = Node.class;
            itemOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("item"));
            nextOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("next"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

ConcurrentLinkedQueue的入队(offer())和出队(poll())操作均通过CAS实现,避免了锁的使用。

offer(E e)方法的实现逻辑如下:

  1. 检查元素是否为null,如果是则抛出异常。
  2. 创建新节点。
  3. 从尾节点开始,通过CAS操作将新节点添加到队列尾部,直到成功。
  4. 必要时更新尾节点(尾节点可能滞后于实际的最后一个节点)。

poll()方法的实现逻辑如下:

  1. 从头部开始,循环查找第一个有元素的节点。
  2. 通过CAS操作将该节点的元素设为null(标记为删除)。
  3. 必要时更新头节点(头节点可能是已删除的节点)。
  4. 返回被删除节点的元素。

由于使用了CAS操作,ConcurrentLinkedQueue在多线程环境下可以高效地并发操作,不会出现锁竞争导致的线程阻塞。

7.2.5 ConcurrentLinkedQueue的使用场景

ConcurrentLinkedQueue适用于高并发场景下的队列操作,例如:

  • 多线程日志收集,多个线程同时向队列中添加日志,一个线程负责处理日志。
  • 多生产者-多消费者模式,多个生产者向队列中添加任务,多个消费者从队列中取出任务执行。

以下是一个多生产者-多消费者的示例:

import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;

public class ConcurrentQueueExample {
    private static ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
    private static final int PRODUCER_COUNT = 2;
    private static final int CONSUMER_COUNT = 2;
    private static final int ITEM_COUNT = 5;

    public static void main(String[] args) throws InterruptedException {
        // 启动生产者线程
        for (int i = 0; i < PRODUCER_COUNT; i++) {
            final int producerId = i;
            new Thread(() -> {
                for (int j = 0; j < ITEM_COUNT; j++) {
                    int item = producerId * ITEM_COUNT + j;
                    queue.offer(item);
                    System.out.println("生产者" + producerId + "生产:" + item);
                    try {
                        TimeUnit.MILLISECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

        // 启动消费者线程
        for (int i = 0; i < CONSUMER_COUNT; i++) {
            final int consumerId = i;
            new Thread(() -> {
                int count = 0;
                while (count < ITEM_COUNT * PRODUCER_COUNT / CONSUMER_COUNT) {
                    Integer item = queue.poll();
                    if (item != null) {
                        System.out.println("消费者" + consumerId + "消费:" + item);
                        count++;
                        try {
                            TimeUnit.MILLISECONDS.sleep(150);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }

        // 等待所有线程完成
        TimeUnit.SECONDS.sleep(5);
    }
}

在这个示例中,2个生产者线程各生产5个元素,2个消费者线程共同消费这些元素。由于使用了ConcurrentLinkedQueue,多线程可以安全地并发操作队列。

八、队列的选择与使用建议

在实际开发中,选择合适的队列实现类对于程序的性能和正确性至关重要。以下是一些选择队列的建议:

  1. 单线程环境

    • 如果需要普通队列(FIFO),优先选择ArrayDeque,它的性能优于LinkedList
    • 如果需要双端队列或栈,优先选择ArrayDeque
    • 如果需要优先级队列,选择PriorityQueue
  2. 多线程环境

    • 如果需要阻塞队列(生产者-消费者模式):
      • 队列容量固定:选择ArrayBlockingQueue
      • 队列容量较大或不确定:选择LinkedBlockingQueue
      • 需要按照优先级处理元素:选择PriorityBlockingQueue
      • 需要元素延迟处理:选择DelayQueue
      • 需要同步移交元素:选择SynchronousQueue
    • 如果不需要阻塞,只需要线程安全的并发队列:选择ConcurrentLinkedQueue(普通队列)或ConcurrentLinkedDeque(双端队列)。
  3. 特殊需求

    • 需要延迟执行任务:使用DelayQueue
    • 需要高效的栈操作:使用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值