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

目录

前言

引出阻塞队列

普通队列的线程安全问题

测试案例

运行结果

第一次运行,结果如下

第二次运行,结果如下

第三次运行,结果如下

队列空时执行取出元素

实现阻塞队列

定义接口

用单锁实现阻塞队列

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

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

测试代码

测试结果


前言

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

测试结果

相关推荐
墨^O^2 小时前
并发控制策略与分布式数据重排:锁机制、Redis 分片与 Spark Shuffle 简析
java·开发语言·c++·学习·spark
zb200641202 小时前
MySQL——表操作及查询
java
人道领域2 小时前
LeetCode【刷题日记】:滑动窗口算法详解:从暴力法到最优解
java·算法·leetcode
迷藏4942 小时前
# 发散创新:用Locust实现高并发场景下的精准压力测试实战在现代微服务架构中,**系统稳定性与性能瓶颈的识别能力直接决定了产品上线后
java·python·微服务·架构·压力测试
zs宝来了2 小时前
Redis 数据结构底层实现:intset、ziplist、skiplist 深度剖析
数据结构·redis·源码解析·skiplist·ziplist·intset
这辈子谁会真的心疼你2 小时前
怎么修改视频的拍摄信息?详细的修改过程
java·服务器·音视频
小碗羊肉2 小时前
【从零开始学Java | 第二十四篇】泛型的继承和通配符
java·开发语言·新手入门
愤豆2 小时前
15-Java语言核心-并发编程-并发容器详解
java·开发语言
liangblog2 小时前
Spring Boot中手动实例化 `JdbcTemplate` 并指定 数据源
java·spring boot·后端