阻塞队列

前序

当我们实现一个生产者-消费者模式时,我们需要一个存储资源的容器。JDK为我们实现了这样一个容器:阻塞队列BlockingQueue,我们只需要实现存,取操作而不必担心多线程环境下的线程安全问题。

接口BlockingQueue是Java util.concurrent(JUC)包下重要的数据结构,区别于普通的队列,BlockingQueue提供了线程安全的队列访问方式,并发包下很多高级同步类的实现都是基于BlockingQueue实现的。

 

 BlockingQueue的操作方法

阻塞队列提供了四组不同的方法用于插入、移除、检查元素:

方法\处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
检查方法 element() peek()

抛出异常:如果试图的操作无法立即执行,抛异常。当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException异常 。

返回特殊值:如果试图的操作无法立即执行,返回一个特殊值,通常是true / false。

一直阻塞:如果试图的操作无法立即执行,则一直阻塞或者响应中断。

超时退出:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功,通常是 true / false。

不能往阻塞队列中插入null,会抛出空指针异常。

 

BlockingQueue的实现类

ArrayBlockingQueue

数组结构组成的有界阻塞队列。内部结构是数组,故具有数组的特性。

public ArrayBlockingQueue(int capacity, boolean fair){
    //..省略代码
}
//ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);

可以初始化队列大小, 一旦初始化不能改变。构造方法中的fair表示控制对象的内部锁是否采用公平锁,默认是非公平锁

LinkedBlockingQueue

链表结构组成的有界阻塞队列。内部结构是链表,具有链表的特性。默认队列的大小是Integer.MAX_VALUE,也可以指定大小。此队列按照先进先出的原则对元素进行排序。

DelayQueue

该队列的元素只有在其指定的延迟时间到了,才能够从队列中获取到该元素 。放入其中的元素必须实现 java.util.concurrent.Delayed 接口。 

DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。 

SynchronousQueue

它是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作, 否则不能继续添加元素。队列本身并不存储任何元素

非常适合传递性场景。SynchronousQueue的吞吐量高于 LinkedBlockingQueue和ArrayBlockingQueue。

它支持公平访问队列。默认情况下线程非公平性访问队列。

PriorityBlockingQueue

一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序 升序排列,也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化 PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。需要注意的该队列不能保证同优先级元素的顺序。

public class Test {
    static PriorityBlockingQueue queue = new PriorityBlockingQueue<>();
    static class User implements Comparable {
        public User(int age,String name) {
            this.age = age;
            this.name = name;
        }

        int age;
        String name;

        @Override
        public int compareTo(User o) {
            return this.age > o.age ? -1 : 1;
        }
    }
    public static void main(String[] args)  throws InterruptedException{
       queue.add(new User(1, "w1"));
       queue.add(new User(66, "w66"));
       queue.add(new User(55, "w55"));
       queue.add(new User(77, "w77"));
       queue.add(new User(3, "w3"));
       queue.add(new User(9, "w9"));
        for (User user : queue) {
            System.out.println(queue.take().name);
        }
/*输出:
    w77
    w66
    w55
    w9
    w3
    w1
*/
    }
}

由于队列是无界的,因此PriorityBlockingQueue不会阻塞数据生产者,只会在没有可消费的数据时,阻塞消费者。

需要注意的是,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。对于使用默认大小的LinkedBlockingQueue也是一样的。

 

阻塞队列ArrayBlockingQueue原理

下面来看一下ArrayBlockingQueue 在JDK 1.8 的源码。

构造方法的参数

相关参数中除了有初始化队列的大小和是否是公平锁之外,还有 对同一个内部锁lock初始化的两个监视器notEmpty和notFull。

//数据元素数组
final Object[] items;
//下一个待取出元素索引
int takeIndex;
//下一个待添加元素索引
int putIndex;
//元素个数
int count;
//内部锁
final ReentrantLock lock;
//消费者监视器
private final Condition notEmpty;
//生产者监视器
private final Condition notFull;  

public ArrayBlockingQueue(int capacity, boolean fair) {
    //..省略其他代码
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

put操作

生产者线程尝试使用put方法放入资源:

(1)所有执行put操作的线程竞争lock锁,拿到lock锁的线程执行下一步骤,没有拿到锁的线程自旋竞争锁。

(2)判断阻塞队列是否满了:若已满,则调用await方法阻塞该线程,并标记为notFull(生产者)线程,同时释放lock锁,等待被消费者线程唤醒。

(3)若没有满,则调用enqueue方法将资源put进阻塞队列。

(4)唤醒一个标记为notEmpty(消费者)的线程。

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    // 1.自旋拿锁
    lock.lockInterruptibly();
    try {
        // 2.判断队列是否满了
        while (count == items.length)
            // 2.1如果满了,阻塞该线程,并标记为notFull线程,
            // 等待notFull的唤醒,唤醒之后继续执行while循环。
            notFull.await();
        // 3.如果没有满,则进入队列
        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++;
    // 4 唤醒一个等待的线程
    notEmpty.signal();
}

take操作

消费者线程尝试使用take操作从队列中取资源:

(1)所有执行take操作的线程竞争lock锁,拿到lock锁的线程执行下一步骤,没有拿到锁的线程自旋竞争锁。

(2)判断阻塞队列是否为空,如果是空,则调用await方法阻塞这个线程,并标记为notEmpty(消费者)线程,同时释放lock锁,等待被生产者线程唤醒。

(3)如果没有空,则调用dequeue方法。

(4)唤醒一个标记为notFull(生产者)的线程。

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
// 1.自旋拿锁
    lock.lockInterruptibly();
    try {
        // 2.判断队列是否为空:若空了,则阻塞该线程。
        while (count == 0)
            notEmpty.await();
        //3.若没有空,则调用dequeue方法.
        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)
        itrs.elementDequeued();
    notFull.signal();
    return x;
}

注意事项

在put操作和take操作中的第2点里,判断队列是否为空/满都是用while来执行语句。这是因为被唤醒的线程依然需要拿到锁才能继续执行,否则自旋拿锁。拿到锁需要判断队列是否可用,如果是用if语句,那么被唤醒且拿到锁的线程是不会再次检查队列是否可用,而是直接执行下一步骤,此时如果队列不可用会导致出错。

 

使用示例

生产者-消费者模式

我们规定队列最多放入3个资源:

public class Test {
    private int queueSize = 3;
    ArrayBlockingQueue queue = new ArrayBlockingQueue<>(queueSize);

    class Consumer extends Thread {
        @Override
        public void run() {
            consume();
        }
        private void consume() {
            while(true) {
                try {
                    queue.take();
                    System.out.println("取出了一个元素,队列剩余" + queue.size() + "元素");
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    class Producer extends Thread {
        @Override
        public void run() {
            produce();
        }
        private void produce() {
            while(true) {
                try {
                    queue.put(1);
                    System.out.println("生产了一个元素,队列还可以放入:"+(queueSize-queue.size()) + "个元素");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args)  throws InterruptedException{
        Test test = new Test();
        test.new Producer().start();
        test.new Consumer().start();
    }
}

输出结果如下。结果可能是有问题的,这是因为System.out.println语句没有锁,线程1执行完put/take操作后立即失去CPU时间片,然后转到线程2执行get/take操作,执行完后又回到线程1的输出语句并输出,此时队列的大小已被线程2修改,因此输出的size不是当时线程1执行完put/take操作后队列的size。不过我们可以确保的是队列的size不会超过3。

阻塞队列_第1张图片

 

线程池中使用阻塞队列

Java的线程池使用了阻塞队列来实现的,详情见此处。

 


参考资料

http://concurrent.redspider.group/

 

 

你可能感兴趣的:(多线程)