并发容器:手写一个阻塞队列

在这里插入图片描述

Java中提供的阻塞队列

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。

  1. 支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入的元素,直到队列不满
  2. 支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空

阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者从队列里取元素的线程。阻塞队列就是生产者用来存放元素,消费者用来获取元素的容器

插入和移除操作的4种处理方式

方法/处理方式抛出异常
Queue接口
返回特殊值
Queue接口
一直阻塞
BlockingQueue接口
超时退出
BlockingQueue接口
插入方法boolean add©boolean offer(e)void put(e)boolean offer(e, time, unit)
解释添加元素,添加成功返回true,如果队列满了,抛出IllegalStateException添加元素,添加成功返回true,如果队列满了,返回false添加元素,如果队列已经满了,则阻塞等待添加元素,添加成功返回true,如果队列已经满了,则阻塞等待,指定时间已经过去还没能添加成功元素,则返回false
移除方法E remove()E poll()E take()E poll(time, unit)
解释返回头结点,从队列中移除头节点,如果队列为空,抛出NoSuchElementException返回头结点,从队列中移除头节点,如果队列为空,返回null返回头结点,从队列中移除头节点,如果队列为空则阻塞等待返回头结点,从队列中移除头节点,队列中没元素会一直阻塞等待,指定时间已经过去还没能拿到头节点,则返回null
检查方法E element()E peek()不可用不可用
解释返回头结点,但是不从队列中移除头节点,如果队列为空,抛出NoSuchElementException返回头结点,但是不从队列中移除头节点,如果队列为空,返回null

用等待/通知协调生产者和消费者

举一个多生产者,多消费者的例子,队列的大小为3,即队列大小为3时,生产者就不再生产

public class Producer implements Runnable {

    private ArrayList list;

    private int capacity;

    public Producer(ArrayList queue,int capacity) {
        this.list = queue;
        this.capacity = capacity;
    }

    @Override
    public void run() {
        synchronized (list) {
            while (true) {
                while (list.size() == capacity) {
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Object object = list.add(new Object());
                System.out.println(Thread.currentThread().getName() + " 生产");
                list.notifyAll();
            }
        }
    }
}

注意消费者和生产者都是用的notifyAll()[通知所有阻塞的线程]方法,而不是notify()[通知一个阻塞的线程]方法,因为有可能出现“生产者”唤醒“生产者”,消费者“唤醒”消费者的情况,因此有可能造成死锁。

这里以1个消费者,3个生产者为例说一下,消费者1获得锁还没产品呢,阻塞,接着生产者1获得锁生产完了,然后生产者2获得锁后生产完了,再然后生产者3获得锁生产完了,最后生产者1获得锁了,然后阻塞了,现在好了生产者和消费者都阻塞了,造成了死锁。notifyAll()则不会造成死锁,接着上面的步骤,生产者3生产完了释放锁后,会通知所有阻塞的线程,因此消费者1肯定有机会拿到锁来进行消费

public class Consumer implements Runnable {

    private List list;

    public Consumer(List queue) {
        this.list = queue;
    }

    @Override
    public void run() {
        synchronized (list) {
            while (true) {
                while (list.size() == 0) {
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Object object = list.remove(0);
                System.out.println(Thread.currentThread().getName() + " 消费");
                list.notifyAll();
            }
        }
    }
}
public class Test {

    public static void main(String[] args) {

        ArrayList list = new ArrayList(3);
        for (int i = 0; i < 3; i++) {
            new Thread(new Producer(list, 3), "生产者" + i).start();
            new Thread(new Consumer(list), "消费者" + i).start();
        }
    }
}

一部分结果

生产者0 生产
生产者0 生产
生产者0 生产
消费者1 消费
消费者1 消费
消费者1 消费
生产者1 生产

把这个实例用阻塞队列来改写,先自己写一个阻塞队列,实现BlockingQueue接口,这里只展示了一部分重写的方法

public class MyBlockingQueue<E> implements BlockingQueue<E> {

    private int capacity;
    private List<E> list;

    public MyBlockingQueue(int capacity) {
        this.capacity = capacity;
        this.list = new ArrayList(capacity);
    }

    @Override
    public void put(E e) throws InterruptedException {
        synchronized (this) {
            if (list.size() == capacity) {
                this.wait();
            }
            list.add(e);
            this.notifyAll();
        }
    }

    @Override
    public E take() throws InterruptedException {
        synchronized (this) {
            while (list.size() == 0) {
                this.wait();
            }
            E value = list.remove(0);
            this.notifyAll();
            return value;
        }
    }

}
public class Producer implements Runnable {

    private BlockingQueue queue;

    public Producer(BlockingQueue queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                queue.put(new Object());
                System.out.println(Thread.currentThread().getName() + " 生产");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Consumer implements Runnable {

    private BlockingQueue queue;

    public Consumer(BlockingQueue queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Object object = queue.take();
                System.out.println(Thread.currentThread().getName() + " 消费");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Test {

    public static void main(String[] args) {
        BlockingQueue queue = new MyBlockingQueue(3);
        for (int i = 0; i < 3; i++) {
            new Thread(new Producer(queue), "生产者" + i).start();
            new Thread(new Consumer(queue), "消费者" + i).start();
        }
    }
}

这里将BlockingQueue的实现改为ArrayBlockingQueue,程序运行结果一样,和我们之前的例子比较,BlockingQueue其实就是不用我们自己写阻塞和唤醒的部分,直接看一下ArrayBlockingQueue的源码,其实和我自己实现的差不多,只不过是并发这部分源码用的是ReentLock,而我用的是synchronized

ArrayBlockingQueue

在这里插入图片描述

设置notEmpty和notFull2个Condition用来实现线程通信

// 设置同步队列的大小和锁是否公平
public ArrayBlockingQueue(int capacity, boolean fair) {
	if (capacity <= 0)
		throw new IllegalArgumentException();
	this.items = new Object[capacity];
	lock = new ReentrantLock(fair);
	notEmpty = lock.newCondition();
	notFull =  lock.newCondition();
}

往阻塞队列中放元素,当放满时,将线程阻塞到notFull这个等待队列。
可以正常放入阻塞队列中时,则唤醒notEmpty中的线程,让其消费元素

public void put(E e) throws InterruptedException {
	checkNotNull(e);
	final ReentrantLock lock = this.lock;
	lock.lockInterruptibly();
	try {
		while (count == items.length)
			notFull.await();
		enqueue(e);
	} finally {
		lock.unlock();
	}
}
private void enqueue(E x) {
	// assert lock.getHoldCount() == 1;
	// assert items[putIndex] == null;
	final Object[] items = this.items;
	items[putIndex] = x;
	// 循环数组实现
	if (++putIndex == items.length)
		putIndex = 0;
	count++;
	notEmpty.signal();
}

从阻塞队列中获取元素时,如果为空,则将线程阻塞到notEmpty这个等待队列。不为空,从阻塞队列取到元素时,还需要唤醒notFull中的线程,让其往阻塞队列中放元素

public E take() throws InterruptedException {
	final ReentrantLock lock = this.lock;
	lock.lockInterruptibly();
	try {
		while (count == 0)
			notEmpty.await();
		return dequeue();
	} finally {
		lock.unlock();
	}
}
private E dequeue() {
	// assert lock.getHoldCount() == 1;
	// assert items[takeIndex] != null;
	final Object[] items = this.items;
	@SuppressWarnings("unchecked")
	E x = (E) items[takeIndex];
	items[takeIndex] = null;
	// 放到数组的最后一个了,下一次从头开始放
	if (++takeIndex == items.length)
		takeIndex = 0;
	count--;
	if (itrs != null)
		//更新iterators,不再分析
		itrs.elementDequeued();
	notFull.signal();
	return x;
}

最后说一下LZ的理解,个人感觉用ArrayBlockingQueue实现生产者和消费者,比我上面用synchronized的方式应该快很多,毕竟ArrayBlockingQueue只会是生成者通知消费者,或者消费者通知生产者,而synchronized不是,会造成很多不必要的锁竞争,当然并没有实验,有时间可以试试

参考博客

[1]https://blog.csdn.net/luohuacanyue/article/details/16359777
[2]https://www.cnblogs.com/chentingk/p/6497107.html
[3]https://www.cnblogs.com/Ming8006/p/7243858.html
实现阻塞队列
[4]https://blog.csdn.net/truelove12358/article/details/59597169
[5]https://blog.csdn.net/z83986976/article/details/52017259

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java识堂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值