JUC学习之BlockingQueue和LinkedBlockingQueue

一、简介

BlockingQueue,指的是阻塞队列,所谓队列,就是先进先出的一种数据结构。在Java中,BlockingQueue是一个接口,并且继承与Queue<E>接口,类图大体如下:

目前已知的实现类有:

ArrayBlockingQueueDelayQueueLinkedBlockingDequeLinkedBlockingQueueLinkedTransferQueuePriorityBlockingQueueSynchronousQueue

以上几种队列主要区别在于底层实现的数据结构可能有些许区别,但是take出队与put入队操作的原理,是大体类似的。

队列还支持以下操作:在检索元素时等待队列变为非空,在存储元素时等待队列中的空间可用。BlockingQueue方法有四种形式:

  • 第一个抛出一个异常;
  • 第二个返回一个特殊的值(或null或假,这取决于操作);
  • 第三个当前线程无限期直到操作能成功;
  • 第四个只有一个给定的超时时间之前放弃;

这些方法总结如下表:

操作

Throws exception

抛出异常

Special value

返回特殊值

Blocks

无限阻塞

Times out

超时限制

Insert

add(e)

offer(e)

put(e)

offer(e, time, unit)

Remove

remove()

poll()

take()

poll(time, unit)

Examine

element()

peek()

not applicable

not applicable

BlockingQueue不接受空元素。实现在试图添加、放置或提供空值时抛出NullPointerException。null用作标记值,表示轮询操作失败。

BlockingQueue可能有容量限制。在任何给定的时间,它都可能有一个剩余容量,超过这个容量就不能在不阻塞的情况下放置其他元素。没有任何内在容量约束的BlockingQueue总是报告剩余容量为Integer.MAX_VALUE(整数类型最大值)。

BlockingQueue实现主要用于生产者-消费者队列,但还支持集合接口。因此,例如,可以使用remove(x)从队列中删除任意元素。然而,这样的操作通常执行得不是很有效,并且建议偶尔使用,例如当队列消息被取消时。此外,BlockingQueue实现是线程安全的。

二、常用API

返回值类型

方法名称以及描述

boolean

add(E e)

如果可以在不违反容量限制的情况下立即将指定的元素插入到此队列中,如果成功则返回true,如果当前没有空间可用则抛出IllegalStateException。

boolean

contains(Object o)

如果此队列包含指定的元素,则返回true。

int

drainTo(Collection<? super E> c)

从此队列中删除所有可用元素并将它们添加到给定集合中。

int

drainTo(Collection<? super E> c, int maxElements)

从该队列中最多删除给定数量的可用元素,并将它们添加到给定集合中。

boolean

offer(E e)

如果可以在不违反容量限制的情况下立即将指定的元素插入到此队列中,如果成功则返回true,如果当前没有可用空间则返回false。

boolean

offer(E e, long timeout, TimeUnit unit)

将指定的元素插入此队列,如果需要空间可用,则等待指定的等待时间。

E

poll(long timeout, TimeUnit unit)

检索并删除此队列的头,如果元素变得可用,则需要等待指定的等待时间。

void

put(E e)

将指定的元素插入此队列,在必要时等待空间可用。

int

remainingCapacity()

返回此队列(在没有内存或资源约束的情况下)在不阻塞或整数的情况下可以理想地接受的附加元素的数量。MAX_VALUE,如果没有内部限制。

boolean

remove(Object o)

如果指定元素存在,则从此队列中移除该元素的单个实例。

E

take()

检索并删除此队列的头,如有必要则等待,直到某个元素可用为止。

三、使用示例

下面我们通过一个简单的示例说明如何在多线程中使用LinkedBlockingQueue。LinkedBlockingQueue是BlockingQueue中常用的一个实现子类。

/**
 * LinkedBlockingQueue: 阻塞队列
 */
public class T08_TestLinkedBlockingQueue {
    /**
     * 创建阻塞队列
     * this(Integer.MAX_VALUE); 不指定长度的话默认为Integer.MAX_VALUE最大值
     */
    private static BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>();
    private static Random random = new Random();

    public static void main(String[] args) {
        //一个生产者线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    //如果满了,就会等待
                    blockingQueue.put("A" + i);
                    System.out.println(Thread.currentThread().getName() + "---put---" + ("A" + i));
                    TimeUnit.MILLISECONDS.sleep(random.nextInt(1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "生产者线程").start();

        //五个消费者线程
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                for (; ; ) {
                    try {
                        //如果空了,就会等待
                        System.out.println(Thread.currentThread().getName() + "---take---" + blockingQueue.take());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "消费者" + i).start();
        }
    }
}

运行结果:

生产者线程---put---A0
消费者0---take---A0
生产者线程---put---A1
消费者0---take---A1
消费者1---take---A2
生产者线程---put---A2
消费者2---take---A3
生产者线程---put---A3
消费者3---take---A4
生产者线程---put---A4
消费者4---take---A5
生产者线程---put---A5
生产者线程---put---A6
消费者0---take---A6
消费者1---take---A7
生产者线程---put---A7
消费者2---take---A8
生产者线程---put---A8
生产者线程---put---A9
消费者3---take---A9

并且我们发现程序其实还是一直阻塞着,一般我们使用的时候建议指定容量创建阻塞队列,这里为了演示方便并没有指定初始容量,默认为Integer.MAX_VALUE.

四、LinkedBlockingQueue源码阅读

LinkedBlockingQueue是一个有界阻塞队列,底层使用链表实现,既然是队列,那么同样就是“先进先出”,队头的元素是插入时间最长的,队尾的元素是最新插入的,新的元素将会被插入到队列的尾部。就像一根管道一样,先进入的必然就是先出去的。

构造方法:LinkedBlockingQueue提供了两个构造方法,一个可以指定初始容量,一个不指定初始容量。

/**
 * 如果没有指定初始容量,那么默认是Integer.MAX_VALUE
 */
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

/**
 * 指定初始容量
 */
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);
}

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();

其中一把是入队使用的,一把是出队列使用的,由此可知,同一时刻,只能有一个线程执行入队,其余执行入队的线程将会被阻塞;同时,可以有另一个线程执行出队,其余执行出队的线程将会被阻塞。注意,虽然入队和出队两个操作同时均只能有一个线程操作,但是可以一个入队线程和一个出队线程共同执行,也就意味着可能同时有两个线程在操作队列。LinkedBlockingQueue里面还维持了一个AtomicInterger原子整数型数值,用来表示当前队列中的元素个数,这样就确保了两个线程之间操作队列是线程安全的。

LinkedBlockingQueue源码中属性介绍:

/** 容量界限,或整数。如果没有指定默认为MAX_VALUE */
private final int capacity;

/** 记录当前元素个数,使用AtomicInteger 原子变量保存 */
private final AtomicInteger count = new AtomicInteger();

/**
 * 链表的头
 * Invariant: head.item == null
 */
transient Node<E> head;

/**
 * 链表的尾.
 * Invariant: last.next == null
 */
private transient Node<E> last;

/** 由take、poll等出队列持有的锁 */
private final ReentrantLock takeLock = new ReentrantLock();

/** 取元素的条件对象 */
private final Condition notEmpty = takeLock.newCondition();

/** 由put, offer等入队列持有的锁 */
private final ReentrantLock putLock = new ReentrantLock();

/** 添加元素的条件对象 */
private final Condition notFull = putLock.newCondition();

下面我们看一下其中比较重要的几个方法:

【a】put(E e)方法

/**
 * 将指定的元素插入到此队列的末尾,如果需要等待则等待空间可用
 */
public void put(E e) throws InterruptedException {
    //如果入队的元素为空,直接报空指针异常
    if (e == null) throw new NullPointerException();
    int c = -1;
    //根据入队的元素创建一个Node链表节点
    Node<E> node = new Node<E>(e);
    //入队需要的锁
    final ReentrantLock putLock = this.putLock;
    //当前队列元素的个数
    final AtomicInteger count = this.count;
    //入队锁可响应中断
    putLock.lockInterruptibly();
    try {
       //如果队列已经满了,那么此线程将会阻塞等待
       //注意这里使用while循环判断
        while (count.get() == capacity) {
            notFull.await();
        }
        //将当前元素添加到链表尾部
        enqueue(node);
        //获取插入之前队列的元素个数
        c = count.getAndIncrement();
        //如果还能插入元素,那么唤醒阻塞等待中的入队线程
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        //手动释放锁
        putLock.unlock();
    }
    if (c == 0)
        //唤醒出队线程
        signalNotEmpty();
}

//enqueue()方法源码
/**
 * 链接队列末端的节点
 */
private void enqueue(Node<E> node) {
    //将当前元素添加到链表尾部
    last = last.next = node;
}

//signalNotEmpty()方法源码
/**
 * 唤醒出队线程
 */
private void signalNotEmpty() {
    //获取出队需要的锁
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        //通知
        notEmpty.signal();
    } finally {
        //释放锁
        takeLock.unlock();
    }
}

【b】E take()方法

take()方法:用于获取到队列中队头的元素,在队列为空时会阻塞,一直等待到队列有元素为止。

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等待队列中的线程
            notEmpty.signal();
    } finally {
        //释放锁
        takeLock.unlock();
    }
    if (c == capacity)
        //如果队列中的元素从满到非满,通知入队线程 
        signalNotFull();
    return x;
}

/**
 * 从队列头部移除一个节点
 */
private E dequeue() {
    Node<E> h = head;
    Node<E> first = h.next;
    h.next = h; // help GC
    head = first;
    E x = first.item;
    first.item = null;
    return x;
}

private void signalNotFull() {
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        notFull.signal();
    } finally {
        putLock.unlock();
    }
}

【c】remove(Object o)方法

remove()方法:用于删除队列中一个元素,如果队列中不含有该元素,那么返回false;有的话则删除并返回true.

/**
 * 删除队列中一个元素
 */
public boolean remove(Object o) {
    //如果待删除的元素为空,直接返回false
    if (o == null) return false;
    //获取put和take两把锁
    fullyLock();
    try {
        //从头部开始遍历
        for (Node<E> trail = head, p = trail.next;
             p != null;
             trail = p, p = p.next) {
                 //挨个进行比较
            if (o.equals(p.item)) {
                //满足条件的话从链表中删除该元素,然后返回true
                unlink(p, trail);
                return true;
            }
        }
        //元素未找到,返回false
        return false;
    } finally {
        //释放持有的两把锁
        fullyUnlock();
    }
}

/**
 * 获取两把锁,以防止puts和takes.
 */
void fullyLock() {
    putLock.lock();
    takeLock.lock();
}

/**
 * 将内部节点p与前辈节点断开连接.
 */
void unlink(Node<E> p, Node<E> trail) {
    p.item = null;
    trail.next = p.next;
    if (last == p)
        last = trail;
    if (count.getAndDecrement() == capacity)
        notFull.signal();
}

/**
 * 释放锁,以允许puts和 takes.
 */
void fullyUnlock() {
    takeLock.unlock();
    putLock.unlock();
}

【d】offer(E e)和offer(E e, long timeout, TimeUnit unit)

offer():将指定的元素插入到此队列的末尾,插入成功返回true,插入失败返回false.实现原理跟前面介绍的put很相似,这里大概说一下即可。

/**
 * 将指定的元素插入到此队列的末尾,如有必要,将等待指定的等待时间,直到空间可用为止。
 */
public boolean offer(E e, long timeout, TimeUnit unit)
    throws InterruptedException {

    if (e == null) throw new NullPointerException();
    //获取超时时间
    long nanos = unit.toNanos(timeout);
    int c = -1;
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
        while (count.get() == capacity) {
            //如果超过指定的时间还没能成功入队,直接返回false
            if (nanos <= 0)
                return false;
            nanos = notFull.awaitNanos(nanos);
        }
        enqueue(new Node<E>(e));
        c = count.getAndIncrement();
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
        //成功入队返回true
    return true;
}

public boolean offer(E e) {
    if (e == null) throw new NullPointerException();
    final AtomicInteger count = this.count;
    if (count.get() == capacity)
        return false;
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        if (count.get() < capacity) {
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        }
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
    return c >= 0;
}

 【e】poll()和poll(long timeout, TimeUnit unit)

poll():用于获取到队列中队头的元素,实现基本上与take()一模一样,可以参考前面的take()说明.

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    E x = null;
    int c = -1;
    long nanos = unit.toNanos(timeout);
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        while (count.get() == 0) {
            if (nanos <= 0)
                return null;
            nanos = notEmpty.awaitNanos(nanos);
        }
        x = dequeue();
        c = count.getAndDecrement();
        if (c > 1)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}

public E poll() {
    final AtomicInteger count = this.count;
    if (count.get() == 0)
        return null;
    E x = null;
    int c = -1;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        if (count.get() > 0) {
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        }
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}

五、总结

LinkedBlockingQueue是一个有界阻塞队列,底层使用链表实现,内部持有两把锁,一把是入队操作使用的,另外一把是出队使用的,同一时刻,只能有一个线程在操作入队或者出队,但是一个入队线程跟一个出队线程可以同时存在,创建LinkedBlockingQueue不指定初始容量的时候默认取整数最大值,相当于容量无限。

以上就是关于LinkedBlockingQueue的一些介绍, 仅仅是笔者学习中的一些总结,如果不对之处,还请指出来,一起学习,一起进步。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页
实付 19.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值