Java 并发 --- 阻塞队列之LinkedBlockingQueue源码分析

前面我们分析了ArrayBlockingQueue,ArrayBlockingQueue是基于数组来实现的阻塞队列,通过可重入锁和Condition 实现了ArrayBlockingQueue的线程安全和条件阻塞,说到数组,一般就会有链表,今天我们一起来看看另一个阻塞队列—LinkedBlockingQueue

在看本文之前,最好要有 AbstractQueuedSynchronizer,ReentrantLock,Condition的知识,下面是我个人分析的博客,仅供参考:
Java 并发 —AbstractQueuedSynchronizer-共享模式与Condition
Java 并发 —ReentrantLock源码分析

LinkedBlockingQueue 介绍(jdk 1.8)

LinkedBlockingQueue 是一个用链表实现的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行操作。

继承体系

这里写图片描述

LinkedBlockingQueue 实现了BlockingQueue接口,该接口中定义了阻塞的方法接口,
LinkedBlockingQueue 继承了AbstractQueue,具有了队列的行为。
LinkedBlockingQueue 实现了Serializable接口,可以序列化。

数据结构

public class LinkedBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

    //队列存储节点
    static class Node<E> {
        E item;  //元素值
        //节点后继
        Node<E> next;
        Node(E x) { item = x; }
    }

    /** The capacity bound, or Integer.MAX_VALUE if none 队列容量  */
    private final int capacity;

    /** Current number of elements 队列含有的数据个数 */
    private final AtomicInteger count = new AtomicInteger();

    //队列头,默认不会序列化 (transient 修饰)
    transient Node<E> head;

   //队列尾 ,默认不会序列化(transient 修饰)
    private transient Node<E> last;

    //用于出队列的可重入锁,使用非公平锁
    private final ReentrantLock takeLock = new ReentrantLock();

    // 队列不为空的条件
    private final Condition notEmpty = takeLock.newCondition();

    //用于入队的可重入锁,使用非公平锁 
    private final ReentrantLock putLock = new ReentrantLock();

    // 队列不满的条件
    private final Condition notFull = putLock.newCondition();
    ...
 }

可重入锁是独占式的锁,如果用可重入锁来控制对 队列的操作访问,那么此队列将是线程安全的,阻塞操作,那么什么情况下该阻塞,什么情况下不阻塞,这个是由Condition来控制的。
notEmpty :表示的是队列不为空,符合这种条件那么可以进行出队操作,否则将会阻塞,直到队列不为空为止。

notFull: 表示是队列没有满,符合这种条件的可以进行入队操作,否则将会阻塞。

在ArrayBlockingQueue中,入队和出队都是用的同一个可重入锁,而LinkedBlockingQueue 对于出队和入队使用了不同的可重入锁来控制,ArrayBlockingQueue 入队和出队是不能同时并发的,而在LinkedBlockingQueue 中出队和出队是可以同时并发执行的(锁不一样)。
正是如此,对于count(队列中元素的个数)使用了原子类AtomicInteger,来保证对count的操作具有原子性。

构造方法

1、默认构造方法

    /**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}.
     */
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

在LinkedBlockingQueue 中可以不用指定队列大小,默认是用Integer.MAX_VALUE 来作为队列的大小。

2、指定队列大小

    /**
     * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
     *
     * @param capacity the capacity of this queue
     * @throws IllegalArgumentException if {@code capacity} is not greater
     *         than zero
     */
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        //初始化 队头和队尾
        last = head = new Node<E>(null);
    }

队头和队尾初始化为一个空节点(不存储元素值的节点)

3、通过集合初始化

    public LinkedBlockingQueue(Collection<? extends E> c) {
        //设置队列大小为Integer.MAX_VALUE,而不是集合的大小
        this(Integer.MAX_VALUE);
        //将集合中的元素入队,因此会用到入队锁
        final ReentrantLock putLock = this.putLock;
        //入队锁 加锁
        putLock.lock(); // Never contended, but necessary for visibility
        try {
            int n = 0;
            //遍历集合
            for (E e : c) {
                //不允许存储空值
                if (e == null)
                    throw new NullPointerException();
                if (n == capacity)
                    throw new IllegalStateException("Queue full");
                // 入队    
                enqueue(new Node<E>(e));
                ++n;
            }
            //设置队列实际大小
            count.set(n);
        } finally {
            //释放锁
            putLock.unlock();
        }
    }

通过集合初始化队列,队列的大小不是集合的大小,而是Integer.MAX_VALUE,获取入队锁,然后依次向队列中添加元素,在finally 中释放锁,这样能保证就是出现异常,也能正确的释放锁。

入队

LinkedBlockingQueue提供了诸多方法,可以将元素加入队列尾部
1、add(E e)
将指定的元素插入到此队列的尾部,在成功时返回 true,如果此队列已满,则抛出 IllegalStateException

    public boolean add(E e) {
        //调用offer 方法 完成
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }

2、offer(E e)
将指定的元素插入到此队列的尾部在成功时返回 true,如果此队列已满,则返回 false。
在前面的add 中,内部调用了offer 方法,我们也可以直接调用offer 方法来完成入队操作。

    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);
                //原子增加 count的值,返回旧值(注意是返回旧值)
                c = count.getAndIncrement();
                //c+1 表示入队后的队列中元素的个数,如果此时没有队满,那么not Full条件满足,唤醒阻塞在notFull条件上的一个线程
                if (c + 1 < capacity)
                    notFull.signal();
            }
        } finally {
            //释放入队锁
            putLock.unlock();
        }
        //c 是旧值,如果c==0 表示原来队列为空,现在入队了一个元素,则不为空了,则notEmpty 条件满足,唤醒阻塞在notEmpty条件上的一个线程
        if (c == 0)
            signalNotEmpty();
        return c >= 0;
    }

从offer中可以看出来:LinkedBlockingQueue和ArrayBlockingQueue一样,存储的元素是不能为空的,ArrayList和LinkedList可以存储空元素。

    /**
     * Signals a waiting take. Called only from put/offer (which do not
     * otherwise ordinarily lock takeLock.)
     */
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        //出队锁 加锁
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

notEmpty 用于出队控制条件上,当队列不为空时,notEmpty 条件满足,则可以出队,因此需要获取出队锁,在入队的同时,可以出队,在出队的同时也可以出队,只要满足notEmpty 或者notFull 条件就可以了。

3、offer(E e, long timeout, TimeUnit unit)
将指定的元素插入此队列的尾部,如果该队列已满,则在到达指定的等待时间之前等待。

    /**
     * Inserts the specified element at the tail of this queue, waiting if
     * necessary up to the specified wait time for space to become available.
     *
     * @return {@code true} if successful, or {@code false} if
     *         the specified waiting time elapses before space is available
     * @throws InterruptedException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     */
    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;
                // 未超时,notFull 条件不满足,则在notFull条件上等待
                nanos = notFull.awaitNanos(nanos);
            }
            //出队
            enqueue(new Node<E>(e));
            //c 是入队前的旧值
            c = count.getAndIncrement();
            //notFull 条件满足
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        //notEmpty条件满足
        if (c == 0)
            signalNotEmpty();
        return true;
    }

这里在获取锁的时候和前面的不一样,lockInterruptibly 表示可以中断的,前面的加锁对中断不敏感,也就是说,在前面的获取锁的方式中,别的线程对当前线程中断,当前线程不会理会(会记录中断状态),而可中断的获取锁,其它线程中断该线程的时候,会抛出中断异常,举个例子:如果队列满了,但是超时时间还没有到,此时如果不想执行入队操作了,那么就可以中断当前线程(注意:中断不是立即取消,中断后也可能会执行入队操作,参考:Java 并发 —中断机制)。

4、put(E e)
将指定的元素插入此队列的尾部,如果该队列已满,则等待。

    /**
     * Inserts the specified element at the tail of this queue, waiting if
     * necessary for space to become available.
     *
     * @throws InterruptedException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     */
    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        //入队锁 可中断的加锁
        putLock.lockInterruptibly();
        try {
            //如果队满,则notFull 条件不满足,阻塞在该条件上,直到条件满足,或者发生中断
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }

当队列满时,notFull 条件不满足,因此会阻塞在该方法上(await()),这里用的while循环,因为当线程从notFull 条件阻塞中唤醒时,需要重新检查(可以此时被其它线程抢先了,导致还是不满足)。

出队

和入队一样,LinkedBlockingQueue 也提供了很多出队的方法。
1、poll()
获取并移除此队列的头,如果此队列为空,则返回 null

    public E poll() {
        final AtomicInteger count = this.count;
        //如多队列中没有元素,返回false
        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 为出队前的队列大小
                c = count.getAndDecrement();
                // 元素出队前,队列>1 那么出队后就>0,notEmpty条件满足
                if (c > 1)
                    notEmpty.signal();
            }
        } finally {
            //释放锁
            takeLock.unlock();
        }
        //出队前,队满,出队后则队列不满,notFull条件满足,唤醒阻塞在notFull条件上的一个线程
        if (c == capacity)
            signalNotFull();
        return x;
    }
    /**
     * Signals a waiting put. Called only from take/poll.
     */
    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        //入队锁 加锁
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }

2、poll(long timeout, TimeUnit unit)
获取并移除此队列的头部,在指定的等待时间前等待。

    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;
                //notEmpty 条件上阻塞    
                nanos = notEmpty.awaitNanos(nanos);
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

在poll()方法上增加超时等待,如果队列为空,则等待指定的时间,如果在超时发生前,队列不空,则可以出队,否则发生超时,返回false。
因为可以超时等待,因此使用的可以中断的上锁,同时抛出中断异常,当在超时未发生时,如果不想再继续等待,那么就可以中断该线程,让其从阻塞等待中返回(中断并不一定就可以马上取消任务)。
3、take() :
获取并移除此队列的头部,在元素变得可用之前一直等待

    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        // 出队锁,可中断加锁
        takeLock.lockInterruptibly();
        try {
           // 如果队空,则notEmpty 条件不满足,阻塞在notEmpty条件上,直到队列不空,或者发生中断。
            while (count.get() == 0) {
                notEmpty.await();
            }
            //出队
            x = dequeue();
            c = count.getAndDecrement();
            //notEmpty 条件满足
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        //notFull 条件满足
        if (c == capacity)
            signalNotFull();
        return x;
    }

4、peek()
调用此方法,可以返回队头元素,但是元素并不出队。

    public E peek() {
        //队列为空,返回false
        if (count.get() == 0)
            return null;
        final ReentrantLock takeLock = this.takeLock;
        //虽然不会出队,但是会访问队头元素,因此还是需要加锁
        takeLock.lock();
        try {
            //head 是一个空节点(不存储具体元素值的节点),因此head.next 才是第一个元素节点
            Node<E> first = head.next;
            if (first == null)  // 队空,返回false 
                return null;
            else
                return first.item;  //返回元素组
        } finally {
            takeLock.unlock();
        }
    }

在前面我们虽然判断了队列是否为空,但是仍然可能出现队列为空,为什么呢,因为我们判断队列空的地方,并没有获取出队锁,那么如果队列此时只有一个元素,那么队列不为空,在出队锁加锁前,如果其他线程抢先了一步进行了出队,那么此时队列变为空了,当前线程是感应不到的。

序列化

在LinkedBlockingQueue 我们看到其部分属性被声明为了transient,被transient 修饰的属性默认不会被序列化, 在 Java集合之ArrayList 中我们具体阐释过序列化,可以参考了解一下。
因为该阻塞队列是基于链表实现的,物理上是不连续的,因此不能通过ArrayBlockingQueue 那么来默认序列化,需要我们自己”手动”序列化。

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        //出队锁,入队锁都 加锁
        fullyLock();
        try {
            // Write out any hidden stuff, plus capacity
            //先进行默认序列化工作
            s.defaultWriteObject();

            // Write out all elements in the proper order.
            //将队列中的元素,依次进行序列化
            for (Node<E> p = head.next; p != null; p = p.next)
                s.writeObject(p.item);

            // Use trailing null as sentinel
            //标记队列序列化结束,可以结合反序列化来看
            s.writeObject(null);
        } finally {
            fullyUnlock();
        }
    }

writeObject 就是序列化过程,这个并不复制,就是把队列中的数据依次序列化即可,反序列化就是一个逆过程。

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in capacity, and any hidden stuff
        //进行默认反序列化
        s.defaultReadObject();

        //初始化队列大小为0
        count.set(0);
        last = head = new Node<E>(null);

        // Read in all elements and place in queue
        for (;;) {
            //不断读取元素,添加到队列中,知道读到null 值。
            E item = (E)s.readObject();
            if (item == null)
                break;
            add(item);
        }
    }
}

总结

  1. LinkedBlockingQueue 底层是基于链表实现的队列,容量指定后,不会改变,可以不指定容量,默认容量为Integer.MAX_VALUE。
  2. LinkedBlockingQueue 线程安全,和Vector不一样,Vector 中用的synchronized
    关键字进行线程同步,LinkedBlockingQueue 中通过ReentrantLock来完成的。
  3. LinkedBlockingQueue和ArrayBlockingQueue 不一样,前者对于出队和入队使用了不同的可重入锁来完成,后者使用了一个可重入锁来完成,因此前者的并发高于后者,后者比前者更线程安全,LinkedBlockingQueue 并不完全线程安全,因为入队和出队是不同的锁,因此二者可以并发执行。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值