JAVA多线程并发(三)-- 阻塞队列

一、引言

在Java并发编程中,阻塞队列(BlockingQueue)是一个非常重要的概念。它支持在队列为空时,获取元素的线程将会阻塞,直到有元素可获取当队列已满时,尝试添加元素的线程也将阻塞,直到队列有空余空间。这种机制为多线程间的数据交换提供了高效且安全的同步方式。

二、阻塞队列的主要特点

  1. 线程安全:多个线程同时访问阻塞队列时,不需要额外的同步措施。
  2. 支持阻塞:当队列为空时,从队列中获取元素的线程将会阻塞;当队列满时,尝试添加元素的线程也会阻塞。
  3. 支持超时等待:当队列为空时,可以尝试等待一定的时间,若时间内队列仍为空,则获取元素操作返回null或抛出异常。

三、阻塞队列的常用方法

1. 添加元素的方法

  • put(E e):将指定元素插入此队列中(如果立即可行且不会违反容量限制),在成功时返回true,如果当前没有可用的空间,则一直阻塞直到空间可用。
  • offer(E e):将指定元素插入此队列中(如果立即可行且不会违反容量限制),当成功时返回true,如果当前没有可用的空间,则返回false。
  • offer(E e, long timeout, TimeUnit unit):尝试将指定元素插入此队列中,等待指定的等待时间以便插入元素(如果有必要)。

2. 移除元素的方法

  • take():获取并移除此队列的头,在元素变得可用之前一直等待(如果有必要)。
  • poll():获取并移除此队列的头,在元素变得可用之前一直等待(如果有必要),但是如果没有可用的元素,则返回null。
  • poll(long timeout, TimeUnit unit):获取并移除此队列的头部,在指定的等待时间前等待可用的元素(如果有必要)。

3. 检查队列状态的方法

  • peek():检索,但是不移除此队列的头;如果此队列为空,则返回null。
  • size():返回此队列中的元素数量。
  • remainingCapacity():返回此队列的剩余容量。

四、阻塞队列的实现类

Java中提供了多种阻塞队列的实现类,如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等。

  • ArrayBlockingQueue:基于数组的阻塞队列,需要指定队列的大小。

  • LinkedBlockingQueue:基于链表的阻塞队列,如果不指定大小,则默认大小为Integer.MAX_VALUE。

  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列,默认情况下元素根据自然顺序排序,也可以自定义比较器。

  • SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等待一个相应的删除操作,反之亦然。

  • DelayQueue:使用优先级队列实现的无界阻塞队列。

  • LinkedTransferQueue:由链表结构组成的无界阻塞队列。

  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

在这里插入图片描述

1. ArrayBlockingQueue(公平、非公平)

  • 需要指定队列的大小
  • 用数组实现的有界阻塞队列。
  • 此队列按照先进先出(FIFO)的原则对元素进行排序
  • 默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素
  • 通常情况下为了保证公平性会降低吞吐量加粗样式。我们可以使用以下代码创建一个公平的阻塞队列
 ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);

2. LinkedBlockingQueue(两个独立锁提高并发)

  • 基于链表的阻塞队列,同ArrayListBlockingQueue类似,此队列按照先进先出(FIFO)的原则对元素进行排序。
  • LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能
  • LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE)

3. PriorityBlockingQueue(compareTo排序实现优先)

  • 支持优先级的无界队列
  • 默认情况下元素采取自然顺序升序排列
  • 可以自定义实现compareTo()方法来指定元素进行排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。
  • 需要注意的是不能保证同优先级元素的顺序

4. DelayQueue(缓存失效、定时任务 )

  • 支持延时获取元素无界阻塞队列

  • 队列使用PriorityQueue来实现

  • 队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。

  • 只有在延迟期满时才能从队列中提取元素。

  • 应用场景:
    (1)缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
    (2)定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的

5. SynchronousQueue(不存储数据、可用于传递数据)

  • 不存储元素的阻塞队列。
  • 每一个put操作必须等待一个take操作,否则不能继续添加元素。SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。
  • 队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另外一个线程使用,SynchronousQueue的吞吐量高于LinkedBlockingQueue 和 ArrayBlockingQueue

6 LinkedTransferQueue

  • 由链表结构组成的无界阻塞TransferQueue队列。
  • 相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法
    (1) transfer方法
    如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。
    (2) tryTransfer方法:
    用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回而transfer方法是必须等到消费者消费了才返回。 对于带有时间限制的tryTransfer(E e, long timeout, TimeUnit unit)方法,则是试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回false,如果在超时时间内消费了元素,则返回true。

7. LinkedBlockingDeque

  • 由链表结构组成的双向阻塞队列
  • 所谓双向队列是指可以从队列的两端插入和移出元素。双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque多了addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast等方法,以First单词结尾的方法,表示插入,获取(peek)或移除双端队列的第一个元素。以Last单词结尾的方法,表示插入,获取或移除双端队列的最后一个元素。另外插入方法add等同于addLast,移除方法remove等效于removeFirst
  • 在初始化LinkedBlockingDeque时可以设置容量防止其过渡膨胀。另外双向阻塞队列可以运用在“工作窃取”模式中。

五、使用场景

阻塞队列在Java并发编程中扮演着重要的角色,它们提供了一种机制,使线程在队列为空时等待数据的到来,或者在队列满时等待队列有空间可插入数据。以下是几种常见的阻塞队列使用场景:

1. 生产者-消费者模型:

  • 在生产者-消费者模型中,生产者负责将数据或任务放入队列,而消费者则从队列中取出数据或任务进行处理。
  • 阻塞队列在这里起到了缓冲的作用,避免了生产者和消费者之间的直接竞争。生产者生产完数据之后不用等待消费者处理,直接放入阻塞队列;消费者则从阻塞队列中取出数据,无需直接找生产者。
  • 这种模式确保了生产者和消费者之间的协调,避免了潜在的竞争资源和数据不一致性问题。

2. 线程池:

  • 线程池中的任务队列通常是一个阻塞队列。当任务数超过线程池的容量时,新提交的任务会被放入任务队列中等待执行。
  • 线程池中的工作线程从任务队列中取出任务进行处理,如果队列为空,则工作线程会被阻塞,直到队列中有新的任务被提交。
  • 通过这种方式,阻塞队列帮助线程池实现了对任务的缓冲和调度,提高了系统的吞吐量。
  • Java中的ExecutorService和ThreadPoolExecutor类使用阻塞队列来存放待执行的任务

3. 数据交换与传输:

在多线程间的数据交换场景中,阻塞队列可以作为数据传输的通道。一个线程将数据放入阻塞队列,另一个线程从队列中取出数据进行处理。
这种方式避免了多线程间的直接通信和数据竞争,提高了数据交换的安全性和效率。

4. 同步通信:

  • 阻塞队列可以用于实现多个线程之间的同步通信。生产者线程可以将数据放入队列,消费者线程可以从队列中获取数据,从而实现数据的传递和交换。
  • 通过这种方式,阻塞队列提供了一种可靠的方式来处理并发访问和线程间的同步,避免了手动同步和锁机制的复杂性。

5. 控制并发度:
通过调整阻塞队列的容量和阻塞策略,可以控制并发线程的数量,避免资源过度竞争和线程的空转。

六、应用场景示例

简单生产者-消费者模型的示例代码:

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

public class ProducerConsumerExample {
    // 阻塞队列,容量为10
    private static final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);

    // 生产者线程
    static class Producer implements Runnable {
        @Override
        public void run() {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    int product = produce();
                    queue.put(product);
                    System.out.println("Produced " + product + ", queue size: " + queue.size());
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        private int produce() {
            return (int) (Math.random() * 100);
        }
    }

    // 消费者线程
    static class Consumer implements Runnable {
        @Override
        public void run() {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    int consumed = queue.take();
                    System.out.println("Consumed " + consumed + ", queue size: " + queue.size());
                    Thread.sleep((int) (Math.random() * 100));
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 3; i++) {
            executorService.submit(new Producer());
        }
        for (int i = 0; i < 2; i++) {
            executorService.submit(new Consumer());
        }

        // 为了确保示例程序能够运行,这里使用一个简单的延迟来模拟程序运行时间
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        executorService.shutdownNow();
    }
}

在这个示例中:

  • ArrayBlockingQueue被用作阻塞队列,其容量被设置为10。
  • Producer类实现了Runnable接口,代表生产者线程。它不断地生产产品并将其放入队列中。
  • Consumer类也实现了Runnable接口,代表消费者线程。它不断地从队列中取出产品并“消费”它。
  • main方法创建了一个固定大小的线程池,提交了3个生产者和2个消费者任务。
  • 为了模拟程序运行时间,main方法中使用了Thread.sleep,之后立即关闭了线程池。
  • 23
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值