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

目录

前言

引出阻塞队列

普通队列的线程安全问题

测试案例

运行结果

第一次运行,结果如下

第二次运行,结果如下

第三次运行,结果如下

队列空时执行取出元素

实现阻塞队列

定义接口

用单锁实现阻塞队列

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

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

测试代码

测试结果


前言

前面的文章中,提到了队列,如自己实现一个队列和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();
    }
}

测试结果

相关推荐
咖啡八杯3 小时前
GoF设计模式——策略模式
java·后端·spring·设计模式
用户1285261160211 小时前
我把祖传Java项目重构后,接口响应从3s砍到了200ms,只改了这几行代码
java
Linsk11 小时前
组件 = 模板 + 业务逻辑
java·前端·vue.js
星沉远浦12 小时前
用Gemini高效解决Java代码报错难以定位的问题
java
用户2986985301415 小时前
Word 文档字符级格式化:Java 实现方案详解
java·后端
笨鸟飞不快16 小时前
从单个服务到集群:一次完整的性能排查复盘
java·前端
荣码16 小时前
用Streamlit给AI应用套个界面,10行代码出Web页面
java·python
SamDeepThinking16 小时前
Java微服务练习方式
java·后端·微服务
朦胧之1 天前
AI 编程-老项目改造篇
java·前端·后端
程序猿大帅1 天前
别再只当调包侠了:用 Spring AI 落地 Function Calling,我被大模型硬生生砸出了三个大坑
java