数据结构和算法之【阻塞队列】上篇

目录

前言

引出阻塞队列

普通队列的线程安全问题

测试案例

运行结果

第一次运行,结果如下

第二次运行,结果如下

第三次运行,结果如下

队列空时执行取出元素

实现阻塞队列

定义接口

用单锁实现阻塞队列

用上述相同的测试案例来测试自实现的阻塞队列的线程安全问题

生产者-消费者模型的体现

测试代码

测试结果


前言

前面的文章中,提到了队列,如自己实现一个队列和Java中提供了队列的实现LinkedList,而本文(分为上篇和下篇)中主要介绍的也是队列,但它是一种特殊的队列-----阻塞队列

引出阻塞队列

在使用我们自己实现的队列或者Java中提供好的LinkedList时,可能会出现一些问题,如在多线程环境下可能会出现数据不一致问题等,再或者想实现当队列空时取出元素不立即返回或抛异常,而是等待执行时间或一直等到队列有元素时取出,阻塞队列就能解决这些问题

普通队列的线程安全问题

测试案例

java 复制代码
package algorithm.queue;

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.stream.IntStream;

public class JavaQueueTest {
    public static void main(String[] args) throws InterruptedException {
        // 队列:多个线程共享
        Queue<Integer> queue = new LinkedList<>();
        // 用于控制所有线程执行完
        CountDownLatch countDownLatch = new CountDownLatch(10);
        // 这里有10个线程,每个线程分别往队列中添加100个元素
        IntStream.range(0, 10).forEach(no -> new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                queue.offer(i);
            }
            System.out.println(Thread.currentThread().getName() + "-执行结束...");
            countDownLatch.countDown();
        }, "线程" + no).start());
        countDownLatch.await();
        // 正常来讲,这里队列的大小应该是10*100=1000
        // 但实际结果却是小于1000,且每次运行的结果不一样
        // 存在线程安全问题
        System.out.println(queue.size());
    }
}

运行结果

第一次运行,结果如下
第二次运行,结果如下
第三次运行,结果如下

队列空时执行取出元素

java 复制代码
package algorithm.queue;

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

public class JavaQueueTest {
    public static void main(String[] args) {
        // Java中提供的队列
        Queue<Object> queue = new LinkedList<>();
        // 此时队列为空
        System.out.println(queue.isEmpty());
        // 执行取出元素
        Object ele = queue.poll();
        // 立即返回null
        System.out.println(ele);
    }
}

实现阻塞队列

定义接口

老样子,第一步还是先把接口定义出来,后续有多种实现方式都可以对该接口进行实现,可以提高其可扩展性

java 复制代码
package algorithm.queue;

public interface BlockingQueue {
    /**
     * 添加元素,若队列已满,则等待其他元素被移除
     */
    void put(Object e) throws InterruptedException;

    /**
     * 移除元素,若队列已空,则等待其他元素被添加到队列中
     */
    Object take() throws InterruptedException;

    /**
     * 元素个数
     */
    int size();
}

用单锁实现阻塞队列

java 复制代码
package algorithm.queue;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 基于链表并使用一把锁的方式来实现阻塞队列
 */
public class OneLockListBlockingQueue implements BlockingQueue {
    // 虚拟头节点和虚拟尾节点
    private Node dummyHead;
    private Node dummyTail;

    // 元素个数
    private final AtomicInteger size = new AtomicInteger(0);

    // 默认最大容量
    private static final int MAX_CAPACITY = Integer.MAX_VALUE - 8;

    private final int maxCapacity;

    // 定义一把锁
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();


    // 未指定队列最大容量,使用默认值
    public OneLockListBlockingQueue() {
        maxCapacity = MAX_CAPACITY;
        initHeadAndTail();
    }

    // 初始化虚拟头尾节点的关系
    private void initHeadAndTail() {
        dummyHead = new Node(null);
        dummyTail = new Node(null);
        dummyHead.next = dummyTail;
        dummyTail.prev = dummyHead;
    }

    // 指定了队列的最大容量
    public OneLockListBlockingQueue(int maxCapacity) {
        this.maxCapacity = maxCapacity;
        initHeadAndTail();
    }

    @Override
    public void put(Object e) throws InterruptedException {
        if (e == null) {
            throw new NullPointerException();
        }
        try {
            lock.lock();
            while (size.get() == maxCapacity) {
                // 阻塞等待
                condition.await();
            }
            // 从尾部添加节点元素
            Node newNode = new Node(e);
            Node prev = dummyTail.prev;
            dummyTail.prev = newNode;
            newNode.prev = prev;
            newNode.next = dummyTail;
            prev.next = newNode;
            // 元素个数加一
            size.getAndIncrement();
            // 唤醒其他等待线程
            condition.signalAll();
        } finally {
            lock.unlock();
        }

    }

    @Override
    public Object take() throws InterruptedException {
        try {
            lock.lock();
            while (size.get() == 0) {
                // 阻塞等待
                condition.await();
            }
            // 从头部删除元素
            Node removeNode = dummyHead.next;
            Node next = dummyHead.next.next;
            dummyHead.next = next;
            next.prev = dummyHead;
            removeNode.prev = null;
            removeNode.next = null;
            // 元素个数减一
            size.getAndDecrement();
            // 唤醒其他等待线程
            condition.signalAll();
            return removeNode.val;
        } finally {
            lock.unlock();
        }
    }

    @Override
    public int size() {
        return size.get();
    }

    // 定义数据节点,这里使用双向链表
    private static class Node {
        Object val;
        Node prev;
        Node next;

        public Node(Object val) {
            this.val = val;
        }

        public Node(Object val, Node prev, Node next) {
            this.val = val;
            this.prev = prev;
            this.next = next;
        }
    }
}

用上述相同的测试案例来测试自实现的阻塞队列的线程安全问题

java 复制代码
package algorithm.queue;

import java.util.concurrent.CountDownLatch;
import java.util.stream.IntStream;

/**
 * 测试自实现的单锁实现的阻塞队列
 */
public class OneLockListBlockingQueueTest {
    public static void main(String[] args) throws InterruptedException {
        // 阻塞队列:多个线程共享
        BlockingQueue queue = new OneLockListBlockingQueue();
        // 用于控制所有线程执行完
        CountDownLatch countDownLatch = new CountDownLatch(10);
        // 这里有10个线程,每个线程分别往队列中添加100个元素
        IntStream.range(0, 10).forEach(no -> new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    queue.put(i);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName() + "-执行结束...");
            countDownLatch.countDown();
        }, "线程" + no).start());
        countDownLatch.await();
        // 无论执行多少次,结果都是1000
        System.out.println(queue.size());
    }
}

生产者-消费者模型的体现

可以使用自定义的阻塞队列实现生产一个消费一个的效果

测试代码

java 复制代码
package algorithm.queue;

public class ProducerConsumerTest {
    public static void main(String[] args) {
        // 阻塞队列
        BlockingQueue queue = new OneLockListBlockingQueue(1);
        // 生产者
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    // 生产者生产速度:100ms一次
                    Thread.sleep(100);
                    queue.put(i);
                    System.out.println(Thread.currentThread().getName() + "-" + i);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "producer").start();
        // 消费者
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    // 消费者消费速度:500ms一次
                    Thread.sleep(500);
                    Object val = queue.take();
                    System.out.println(Thread.currentThread().getName() + "-" + val);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "consumer").start();
    }
}

测试结果

相关推荐
敲敲千反田4 分钟前
CMS和G1
java·开发语言·jvm
花千树-0108 分钟前
MCP HTTP 传输详解:比 SSE 简单,但有一个意外的坑
java·agent·sse·function call·ai agent·mcp·harness
花千树-01010 分钟前
三个 Agent 并行调研:用 concurrent 节点构建并发-汇聚式旅游规划助手
java·langchain·agent·function call·multi agent·mcp·harness
2501_9130613411 分钟前
网络原理之HTTP
java·网络·面试
yaaakaaang11 分钟前
二十、状态模式
java·状态模式
一只大袋鼠17 分钟前
MyBatis 进阶实战(四): 连接池、动态 SQL、多表关联(一对多 / 多对一 / 多对多)
java·开发语言·数据库·sql·mysql·mybatis
电商API&Tina22 分钟前
【1688API接口】1688 开放平台 API 接入心得
java·开发语言·数据库·python·sql·json
剑挑星河月24 分钟前
73.矩阵置零
数据结构·算法·leetcode·矩阵
Rabitebla24 分钟前
【C++】手撕日期类——运算符重载完全指南(含易错点+底层逻辑分析)
java·c语言·开发语言·数据结构·c++·算法·链表
callJJ25 分钟前
SpringBoot 自动配置原理详解——从“约定优于配置“到源码全程追踪
java·spring boot·后端·spring