Java 入门指南:Java 并发编程 —— 并发容器 TransferQueue、LinkedTransferQueue、SynchronousQueue

BlockingQueue

BlockingQueue 是Java并发包(java.util.concurrent)中提供的一个阻塞队列接口,它继承自 Queue 接口。

BlockingQueue 中的元素采用 FIFO 的原则,支持多线程环境并发访问,提供了阻塞读取和写入的操作,当前线程在队列满或空的情况下会被阻塞,直到被唤醒或超时。

常用的实现类有:

  • ArrayBlockingQueue:并发容器 ArrayBlockingQueue 详解
  • LinkedBlockingQueue:并发容器 LinkedBlockingQueue 详解
  • PriorityBlockingQueue:并发容器 PriorityBlockingQueue 详解
  • SynchronousQueue
  • LinkedBlockingDeque:并发容器 LinkedBlockingDeque 详解
    等类,它们的实现方式各有不同。
适用场景

BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。

Java 入门指南:Java 并发编程 —— 并发容器 TransferQueue、LinkedTransferQueue、SynchronousQueue_第1张图片

一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点

如果该阻塞队列到达了其临界点,生产者线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中,直到消费者线程从队列中拿走一个对象。

消费者线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。

常用方法
  1. put(E e):将元素 e 插入到队列中,如果队列已满,则会阻塞当前线程,直到队列有空闲空间

  2. offer(E e):将元素 e 插入到队列中,如果队列已满,则返回 false。

  3. offer(E element, long timeout, TimeUnit unit) 方法是 BlockingQueue:在指定的时间内将元素添加到队列中。

    • timeout:超时时间,表示在指定的时间内等待队列空间可用。如果超过指定的时间仍然无法将元素添加到队列中,将返回 false。

    • unit:超时时间的单位。

  4. take():移除并返回队列头部的元素,如果队列为空,则会阻塞当前线程,直到队列有元素

  5. poll():移除并返回队列头部的元素,如果队列为空,则返回 null

  6. poll(long timeout, TimeUnit unit):在指定的时间内从队列中检索并移除元素。返回移除的元素。如果超过指定的时间仍然没有可用的元素,将返回 null。

  7. peek():返回队列头部的元素,但不会移除。如果队列为空,则返回null

  8. size():返回队列中元素的数量

  9. isEmpty():判断队列是否为空,为空返回 true,否则返回 false

  10. isFull():判断队列是否已满,已满返回 true,否则返回 false

  11. clear():清空队列中的所有元素

行为 抛异常 返回特定值 阻塞 超时
插入 add(o) offer(o) put(o) offer(o, timeout, timeunit)
移除 remove() poll() take() poll(timeout, timeunit)
检查 element() peek()
  • 抛异常: 如果试图的操作无法立即执行,抛一个异常。

  • 特定值: 如果试图的操作无法立即执行,返回 true / false / null)。

  • 阻塞: 如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。

  • 超时: 如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是 true / false)。

若向 BlockingQueue 中插入 null,将会抛出 NullPointerException

死锁问题

需要注意的是,在使用 BlockingQueue 时要注意防止死锁的问题:

  • 在队列满之后调用 offer() 方法插入元素会返回false,此时不能直接调用put()方法,因为在插入之前还需要获取其它资源,如果在获取资源时一直阻塞在这里,就会发生死锁。

  • 为了防止死锁的问题,建议使用 offer(E e, long timeout, TimeUnit unit)poll(long timeout, TimeUnit unit) 带有超时时间的方法。

TransferQueue

TransferQueue 是 JUC 中的一个接口,它继承了 BlockingQueue 接口,在 BlockingQueue 的基础上扩展了一个 transfer() 方法,提供了一种更强大的交互方式。

BlockingQueue 不同,TransferQueue 提供了 "一对一"和 “多对一” 两种传输模式:

  • 一对一模式:指等待的线程必须与指定线程直接配对,相当于是生产者把元素需要传递给对应的消费者如果没有对应的消费者,则生产者线程会被阻塞

  • 多对一模式:指等待的线程可以与任何线程交互,即生产者线程不需与特定的消费者线程配对,线程池中的任意线程都可以接收到元素

扩展方法

TransferQueue 接口扩展了以下方法:

  • transfer(E e): 将指定的元素发送给一个消费者线程,如果没有等待的消费者线程,则当前线程被阻塞,直到有消费者线程接收该元素。

  • tryTransfer(E e): 将指定的元素发送给一个消费者线程,如果没有等待的消费者线程,则立即返回 false,否则返回 true。

  • tryTransfer(E e, long timeout, TimeUnit unit): 将指定的元素发送给一个消费者线程,如果没有等待的消费者线程则在指定时间内等待,如果等待超时则返回 false,否则返回 true。

  • hasWaitingConsumer(): 判断是否有消费者线程在等待接收元素。

  • getWaitingConsumerCount(): 获取等待接收元素的消费者线程数量。

需要注意的是,由于其阻塞特性,TransferQueue 在并发环境下使用时需要注意线程安全性。

LinkedTransferQueue

LinkedTransferQueue 是一个无界的、基于链表的阻塞队列,于 JDK 7 被引入。

与其他并发队列不同,LinkedTransferQueue 通过 transfer() 方法将元素直接发送给消费者线程,如果没有消费者线程等待接收元素,那么生产者线程将会阻塞(一对一传输)。

内部原理

LinkedTransferQueue 基于链表实现,其内部节点分为数据节点和请求节点。数据节点用于存储实际的数据元素,而请求节点则用于表示消费者的取数请求。

  • 消费者线程尝试从队列中取数据时,如果队列为空,则消费者线程会生成一个请求节点并放入队列中等待。

  • 生产者线程在添加数据时,会检查队列头部是否有等待的消费者请求节点,如果有,则直接将元素传递给该消费者并唤醒其线程。

特点
  • 无界队列LinkedTransferQueue无界的,它允许任意数量的元素加入队列,因此无需担心队列溢出的问题。然而,在实际应用中,由于它内部设计的高效性,通常不会导致内存无限增长。

  • 直接传递LinkedTransferQueue 提供了一种机制,使得生产者可以直接将元素传输给等待的消费者,而无需通过中间缓冲区。当调用transfer(E e)方法时,如果有一个消费者正在等待接收元素,那么元素会立即从生产者转移到消费者,并且两个线程之间的交换无需锁或其他同步机制。

  • 高性能低延迟LinkedTransferQueue 通过原子操作和 CAS(Compare-And-Swap)算法保证了高度的并发性能和较低的线程上下文切换开销。此外,它还使用了一些优化技术(如自旋锁)来减少无效通知(如虚假唤醒),从而提高效率。

  • 混合支持阻塞和非阻塞操作:除了基本的插入(offer)、移除(poll)和检查(peek)等操作外,LinkedTransferQueue 还提供了 tryTransfer(E e) 等额外的方法,这些方法支持无等待的元素传输,即尝试立即将元素传输给一个等待的消费者,如果成功则返回true,否则返回false。

应用场景
  • 生产者-消费者模型LinkedTransferQueue 最常见的应用场景就是生产者-消费者模型。生产者向队列中添加数据,而消费者则从队列中获取数据。由于 LinkedTransferQueue 支持直接传递和高效的并发性能,因此特别适用于需要高吞吐量和低延迟的数据传输场景。

  • 多线程数据传输:多个线程可以同时向 LinkedTransferQueue 中添加数据,同时也可以同时从队列中获取数据。由于 LinkedTransferQueue 的线程安全性,因此可以保证数据传输的正确性和可靠性。

  • 消息队列LinkedTransferQueue 还可以用于实现消息队列。生产者向队列中添加消息,而消费者则从队列中获取消息。由于 LinkedTransferQueue 的高效性,因此可以实现高吞吐量的消息传输。

  • 线程池工作队列:在 Java 线程池中,LinkedTransferQueue 可以作为工作队列使用。线程池中的线程从队列中获取任务并执行,由于 LinkedTransferQueue 的线程安全性和高效性,因此可以保证任务的正确性和可靠性,并提高线程池的整体性能。

构造方法
  1. 创建一个空的 LinkedTransferQueue
LinkedTransferQueue()
  1. 创建一个包含给定集合中所有元素的 LinkedTransferQueue,元素存储顺序为集合迭代器返回元素的顺序。如果给定集合为空,则创建一个空的 LinkedTransferQueue
LinkedTransferQueue(Collection<? extends E> collection)
模式转换
  1. 一对一(生产者)模式(Transfer Mode):

    • 生产者线程使用 transfer(E e) 方法将元素放入队列中。如果队列中没有等待的消费者线程,则生产者线程会被阻塞,直到有消费者线程从队列中获取该元素。

    • transfer(E e) 方法是一种阻塞方法,它会等待直到元素被消费者线程接收或者队列关闭。

  2. 多对一(消费者)模式(Try Mode):

    • 消费者线程使用 tryTransfer(E e) 方法尝试从队列中获取元素。如果队列中没有元素可以立即获取,那么该方法会立即返回 false,而不会阻塞线程。

    • tryTransfer(E e) 方法是一种非阻塞方法,它无需等待,直接返回获取结果。

LinkedTransferQueue 使用示例

LinkedTransferQueue 是一种高性能的阻塞队列,它支持在生产者和消费者之间直接传递对象。与普通的阻塞队列相比,LinkedTransferQueue 具有更高的性能,因为它减少了对象的复制次数,并且支持更细粒度的并发控制。

import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TimeUnit;

public class LinkedTransferQueueExample {

    public static void main(String[] args) {
        // 创建一个 LinkedTransferQueue 实例
        LinkedTransferQueue<String> transferQueue = new LinkedTransferQueue<>();

        // 创建生产者线程
        Thread producer = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println("生产者生产了数据: " + i);
                    transferQueue.transfer("Data " + i); // 将数据放入队列
                    TimeUnit.MILLISECONDS.sleep(200); // 模拟生产延迟
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // 设置线程中断状态
                    System.err.println("生产者线程被中断");
                }
            }
        });

        // 创建消费者线程
        Thread consumer = new Thread(() -> {
            while (true) {
                try {
                    String data = transferQueue.take(); // 从队列中取出数据
                    System.out.println("消费者消费了数据: " + data);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // 设置线程中断状态
                    System.err.println("消费者线程被中断");
                    break; // 终止循环
                }
            }
        });

        // 启动生产者线程和消费者线程
        producer.start();
        consumer.start();

        // 等待生产者线程结束
        try {
            producer.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // 设置线程中断状态
            System.err.println("主线程被中断");
        }

        // 中断消费者线程
        consumer.interrupt();
    }
}
SynchronousQueue

SynchronousQueue(同步队列)是 JUC 中的一个没有任何容量的阻塞队列,只支持 一对一 模式。

SynchronousQueue每个插入操作必须等待另一个线程的对应移除操作,反之亦然。也就是说,它是一种没有存储元素的阻塞队列,插入和移除操作是成对出现的

由于 SynchronousQueue 是一个同步队列,它的操作是阻塞的,即插入和移除操作都必须等待对方的配对操作。这使得 SynchronousQueue适用于线程之间的数据交换,尤其是在生产者-消费者模型中的数据传递。

实现原理

SynchronousQueue 的内部实现依赖于两种数据结构:

  • 一种是先入先出的队列(TransferQueue),用于公平模式

  • 另一种是后入先出的栈(TransferStack),用于非公平模式。

这两种数据结构都继承自一个抽象类 Transferer,该类定义了一个 transfer 方法,用于执行 puttake 操作。在 transfer 方法中,通过自旋和 LockSupport 的 park/unpark 方法来实现线程的阻塞和唤醒,从而实现了线程间的直接通信。

特点
  • 不存储元素:与其他阻塞队列不同,SynchronousQueue 内部不存储任何元素。它仅仅作为线程间传递对象的媒介。

  • 直接传递SynchronousQueue 实现了线程间的直接对象传递,即一个线程通过put操作放入对象后,必须等待另一个线程通过take操作取走对象,然后put操作才会返回。

  • 公平性选择SynchronousQueue 提供了两种构造方法,一种是默认的非公平模式,另一种是公平模式。在公平模式下,等待时间较长的线程会优先得到插入或移除元素的机会。

  • 高效性:由于 SynchronousQueue 不存储元素,因此它避免了在内存中存储元素所需的空间和时间开销,使得线程间的通信更加高效。

应用场景
  • 生产者-消费者模型:在生产者-消费者模型中,SynchronousQueue 可以作为生产者和消费者之间的数据传递通道。生产者将数据放入 SynchronousQueue,消费者从队列中取出数据并处理。

  • 线程池任务执行:在 ThreadPoolExecutor 中,使用 SynchronousQueue 作为工作队列可以让提交的任务立即被工作者线程接管处理,避免了任务在队列中的堆积,从而提高了系统的响应速度。

  • 事件驱动架构:在事件驱动的系统中,事件可以被立即传递给事件处理器,无需中间缓存,保证了低延迟和高效处理。

  • 数据管道:在需要将数据从一个线程直接传递到另一个线程的场景中,如在并行处理流水线中,SynchronousQueue 可以作为连接不同处理阶段的桥梁。

构造方法
  1. 创建一个不带任何元素的 SynchronousQueue 对象,默认使用非公平策略。
SynchronousQueue()
  1. 创建一个带有公平性参数的 SynchronousQueue 对象。

    如果 fairness 参数为 true,则通常使用公平的排序策略,即等待时间最长的线程被优先处理

    如果 fairness 参数为 false,则使用非公平的排序策略,不考虑线程等待时间的先后顺序,而是尽可能地让新到达的线程获取共享资源。

SynchronousQueue(boolean fairness)

需要注意的是,SynchronousQueue 的构造方法不支持指定容量或初始元素集合。因为 SynchronousQueue 中每个插入操作必须等待另一个线程对应的删除操作,反之亦然。所以其大小不可预知。

SynchronousQueue 使用示例

SynchronousQueue 是一个特殊的阻塞队列,它实际上不存储任何元素。它的特点是,当一个线程尝试将元素插入队列时,必须有另一个线程准备从队列中取走这个元素,否则插入操作会一直阻塞。同样地,当一个线程尝试从队列中取走元素时,必须有一个线程准备插入元素,否则取走操作也会阻塞。

import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

public class SynchronousQueueExample {

    public static void main(String[] args) {
        // 创建一个 SynchronousQueue 实例
        SynchronousQueue<String> syncQueue = new SynchronousQueue<>();

        // 创建生产者线程
        Thread producer = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println("生产者生产了数据: " + i);
                    syncQueue.put("Data " + i); // 将数据放入队列
                    TimeUnit.MILLISECONDS.sleep(200); // 模拟生产延迟
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // 设置线程中断状态
                    System.err.println("生产者线程被中断");
                }
            }
        });

        // 创建消费者线程
        Thread consumer = new Thread(() -> {
            while (true) {
                try {
                    String data = syncQueue.take(); // 从队列中取出数据
                    System.out.println("消费者消费了数据: " + data);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // 设置线程中断状态
                    System.err.println("消费者线程被中断");
                    break; // 终止循环
                }
            }
        });

        // 启动生产者线程和消费者线程
        producer.start();
        consumer.start();

        // 等待生产者线程结束
        try {
            producer.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // 设置线程中断状态
            System.err.println("主线程被中断");
        }

        // 中断消费者线程
        consumer.interrupt();
    }
}

你可能感兴趣的:(Java,java,开发语言,团队开发,个人开发,java-ee,intellij-idea)