阻塞队列入门介绍

文章目录

  • 一、阻塞队列是什么?
  • 二、基本的实现方式
    • Java里提供的阻塞队列
    • ArrayBlockingQueue
    • 阻塞队列对两种附加操作提供了四种处理方式:
    • LinkedBlockingQueue
    • CachedThreadPool
  • 三、 阻塞队列的实现原理


一、阻塞队列是什么?

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列,这两个附加的操作支持阻塞的插入和移除方法
1),支持阻塞的插入方法,当队列为满时,对队列的插入操作会被阻塞,直到队列被取出元素。
2),支持阻塞的移除方法,在队列为空时,获取元素的线程回被阻塞,直到队列被插入元素。
阻塞队列常用于生产者消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素,消费者用来获取元素的容器。

二、基本的实现方式

Java里提供的阻塞队列

Java中提供了七种阻塞队列

1. ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列
2. LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。

3. PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
4. DelayQueue:一个使用优先级队列实现的无界阻塞队列。
5. SynchronousQueue:一个不存储元素的阻塞队列。
6. LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
7. LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

我的讨论思路是,先详细介绍较为简单的ArrayBlockingQueue,在在此基础上对比引出LinkedBlockingQueue,最后简要介绍SynchronousQueue。

ArrayBlockingQueue

ArrayBlockingQueue的底层时一个用数组实现的有界阻塞队列,此队列按照先进先出的原则对元素进行排序。默认情况下不保证线程公平的访问队列(为了保证公平性会降低吞吐量)这个公平性是通过内部的ReenterLock来构造的。

//fair参数为true代表 创建的是公平锁,为false创建的是非公平的锁
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();
}

//不指定是否公平的话创建的是非公平锁
public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
}

公平性:
1,公平性是指访问阻塞队列的线程可以按照阻塞的先来后到顺序访问队列,即先阻塞的线程(当队列变成可被访问的时候)先访问队列。(相当于排队,公平)
2,非公平性是指当队列可用时,被阻塞的所有线程有同样的机会访问队列,(相当于没有排队,不公平)

阻塞队列对两种附加操作提供了四种处理方式:

此处代码以ArrayBlockingQueue源码为例

ArrayBlockingQueue中只有一把锁,
/** Main lock guarding all access */
    final ReentrantLock lock;

    /** Condition for waiting takes */
    private final Condition notEmpty;

    /** Condition for waiting puts */
    private final Condition notFull;

阻塞队列入门介绍_第1张图片

  • 抛出异常:当队列满时,如果再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。

  • 返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回null。

public boolean offer(E e) {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	//通过的是if判断来进行,会此次判断返回不同的值(插入成功true,失败false)
            if (count == items.length)
            	//当队列满时返回false
                return false;
            else {
                enqueue(e);
                //当队列中有空余位置时返回true
                return true;
            }
        } finally {
            lock.unlock();
        }
    }

public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	//同样的只执行一次判断,而不会阻塞
        	//根据三目运算符的判断为true表示队列为空,没有元素可以被弹出,返回null,反之dequeue()弹出元素
            return (count == 0) ? null : dequeue();
        } finally {
            lock.unlock();
        }
    }
  • 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。
public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
        	//此处通过while循环来不停的轮询判断count标志位和当前列表长度的关系(因为即使当前条件上的锁被唤醒,也可能先被别的线程抢先put等操作了,又导致count == items.length),
        	//每次判断成功就会陷入阻塞,阻塞在notFull条件上。
            while (count == items.length)
                notFull.await();
            //enqueue() 会执行notEmpty.singal()操作唤醒阻塞在notEmpty条件上的线程
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
        //此处通过while循环来不停的轮询判断当前队列是否为空(因为即使当前条件上的锁被唤醒,也可能先被别的线程抢先take等操作了,又导致count == 0),
        	//每次判断成功就会陷入阻塞,阻塞在notEmpty条件上。
            while (count == 0)
                notEmpty.await();
            //dequeue() 会执行notFull.singal()操作唤醒阻塞在notFull条件上的线程
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
  • 超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。

不过总的来说,并没有因为返回值的不同和不同的处理方式来创建不同的对队列操作的方法,在对队列进行入队操作的时候都是使用enqueue(),在出队的时候都是使用dequeue()。

	/**
     * Inserts element at current put position, advances, and signals.
     * Call only when holding lock.
     */
    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条件上阻塞的线程
        notEmpty.signal();
    }

    /**
     * Extracts element at current take position, advances, and signals.
     * Call only when holding lock.
     */
    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)
        	//使用迭代器来删除元素
            itrs.elementDequeued();
		//每次执行完出队操作就唤醒notFull条件上阻塞的线程
        notFull.signal();
        return x;
    }

LinkedBlockingQueue

FixedThreadPool和SingleThreadPool的工作队列使用的阻塞队列

LinkedBlockingQueue是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。内部使用的是不公平的锁。

而在LinkedBlockingQueue中我们可以看到他有两把锁,可以分别锁住读操作、写操作
	/** Lock held by take, poll, etc */
    private final ReentrantLock takeLock = new ReentrantLock();

    /** Wait queue for waiting takes */
    private final Condition notEmpty = takeLock.newCondition();

    /** Lock held by put, offer, etc */
    private final ReentrantLock putLock = new ReentrantLock();

    /** Wait queue for waiting puts */
    private final Condition notFull = putLock.newCondition();

以put和take操作为例,可以看到使用的是不同的锁来进行阻塞操作的,所以它的出队和入队操作是可以同时执行的,相较于ArrayBlockingQueue(只有一把锁,读写都需要使用同一个锁)LinkedBlockingQueue具有更好的并发性。

public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // Note: convention in all put/take/etc is to preset local var
        // holding count negative to indicate failure unless set.
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            /*
             * Note that count is used in wait guard even though it is
             * not protected by lock. This works because count can
             * only decrease at this point (all other puts are shut
             * out by lock), and we (or some other waiting put) are
             * signalled if it ever changes from capacity. Similarly
             * for all other uses of count in other wait guards.
             */
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }

    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

CachedThreadPool

(3)CachedThreadPool的工作队列使用的阻塞队列:SynchronousQueue

1,SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作, 否则不能继续添加元素。
2,它支持公平访问队列。默认情况下线程采用非公平性策略访问队列。
3, SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合传递性场景。
4,SynchronousQueue的吞吐量高于 LinkedBlockingQueue和ArrayBlockingQueue。

三、 阻塞队列的实现原理

简要介绍一下,阻塞队列使用通知模式实现。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。

阻塞线程是通过Condition来实现的,通过ReenterLock的newCondition()获取一个ConditionObject(AQS容器中的一个内部类)的类的对象

final ConditionObject newCondition() {
            return new ConditionObject();
        }

ConditionObject的await()方法的话有使用到LockSupport.park(this)内部又调用UNSAFE类来阻塞线程。

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

LockSupport的park()方法

public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }

UNSAFE.park()是一个本地方法,会阻塞当前线程。只有当以下4种情况中的一种发生时,该方法才会返回。

  1. 与park对应的unpark执行会已经执行时,“已经执行”是指unpark先执行,然后再执行park的情况。
  2. 当前线程被中断时。
  3. 等待完time参数指定的毫秒数时。
  4. 异常情况发生时,这个异常情况没有任何原因。

你可能感兴趣的:(笔记,java,阻塞队列,并发,并发编程)