Java集合之LinkedBlockingQueue源码分析

问题

(1)LinkedBlockingQueue的实现方式?

(2)LinkedBlockingQueue是有界的还是无界的队列?

(3)LinkedBlockingQueue相比ArrayBlockingQueue有什么改进?

(4)LinkedBlockingQueue 内部是如何使用两个独占锁 ReentrantLock 以及对应的条件变量保证多线程先入队出队操作的线程安全?为什么不使用一把锁,使用两把为何能提高并发度?

简介

LinkedBlockingQueue是java并发包下一个以单链表实现的可选容量的阻塞队列(容量限制是可选的,如果在初始化时没有指定容量,那么默认使用int的最大值作为队列容量),它是线程安全的,里面的代码写的很漂亮,生产者消费者模式在这个类中用的酣畅淋漓,其作者是大名鼎鼎的 Doug Lea,掌握这个类是比较重要的。里面很多实现基于锁,可以好好学习一下。

首先看一下LinkedBlockingQueue 的类图结构,如下图所示:

LinkedBlockingQueue有别于一般的队列,在于该队列至少有一个节点,头节点不含有元素。结构图如下:

可以发现head.item=null,last.next=null。

原理

如类图所示:LinkedBlockingQueue是使用单向链表实现,有两个Node分别来存放首尾节点,并且里面有个初始值为0 的原子变量count,它用来记录队列元素个数。LinkedBlockingQueue中维持两把锁,一把锁用于入队,一把锁用于出队,这也就意味着,同一时刻,只能有一个线程执行入队,其余执行入队的线程将会被阻塞;同时,可以有另一个线程执行出队,其余执行出队的线程将会被阻塞。换句话说,虽然入队和出队两个操作同时均只能有一个线程操作,但是可以一个入队线程和一个出队线程共同执行,也就意味着可能同时有两个线程在操作队列,说白了,这其实就是一个生产者 -  消费者模型。那么为了维持线程安全,LinkedBlockingQueue使用一个AtomicInterger类型的变量表示当前队列中含有的元素个数,所以可以确保两个线程之间操作底层队列是线程安全的。另外notEmpty 和 notFull 是信号量,内部分别有一个条件队列用来存放进队和出队的时候被阻塞的线程。

使用场景

我查了一些资料,感觉和 MQ 有点联系,就是说我们可以使用这个东西进行解耦,或者负载均衡,比如说,有很多任务需要提交,我们可以把任务提交给 Queue,消费者负责处理消息,这个可以根据消费者的能力决定任务的执行效率,不会一下字任务过来而导致崩溃,讲道理,可以适合多生产者,多消费者模式,如果有这个,我们可以很好的进行解耦,负载均衡

源码分析

主要属性

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

    /** Current number of elements */
    private final AtomicInteger count = new AtomicInteger();

    /**
     * Head of linked list.
     * 不变性条件: head.item == null
     */
    transient Node<E> head;

    /**
     * Tail of linked list.
     * 不变性条件: last.next == null
     */
    private transient Node<E> last;

    /** 进队锁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();

    /**

(1)capacity,有容量,可以理解为LinkedBlockingQueue是有界队列

(2)head, last,链表头、链表尾指针(只有head节点为公有,其余全部为private

(3)takeLock,notEmpty,take锁及其对应的条件

(4)putLock, notFull,put锁及其对应的条件

(5)入队、出队使用两个不同的锁控制,锁分离,提高效率

内部类

/**
     * Linked list node class.
     */
    static class Node<E> {
        E item;

        /**
         * One of:
         * - the real successor Node
         * - this Node, meaning the successor is head.next
         * - null, meaning there is no successor (this is the last node)
         */
        Node<E> next;

        Node(E x) { item = x; }
    }

典型的单链表结构。

主要构造方法

/**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}.如果没传容量,就使用最大int值初始化其容量
     */
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

    /**
     * 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;
        // 初始化head和last指针为空值节点
        last = head = new Node<E>(null);  //last和head在队列为空时都存在,所以队列中至少有一个节点
    }

    /**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}, initially containing the elements of the
     * given collection,
     * added in traversal order of the collection's iterator.
     *
     * @param c the collection of elements to initially contain
     * @throws NullPointerException if the specified collection or any
     *         of its elements are null
     */
    public LinkedBlockingQueue(Collection<? extends E> c) {
        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是原子类AtomicInteger,所以有set()方法
            count.set(n);
        } finally {
            putLock.unlock();
        }
    }

从上面的构造方法中可以得出3点结论:
1. 当调用无参的构造方法时,容量是int的最大值
2. 队列中至少包含一个节点,哪怕队列对外表现为空
3. LinkedBlockingQueue不支持null元素

入队

入队同样有四个方法,我们这里依次分析一下:

1.offer操作,向队列尾部插入一个元素,如果队列有空闲容量则插入成功后返回true,如果队列已满则丢弃当前元素然后返回false,如果 e元素为null,则抛出空指针异常(NullPointerException ),还有一点就是,该方法是非阻塞的。源码如下:

//如果可以在不超过队列容量的情况下立即插入指定的元素到队列的尾部,成功后返回{@code true},
//如果队列已满,返回{@code false}。当使用容量受限的队列时,此方法通常比方法{@link BlockingQueue#add add}更可取,后者只能通过抛出异常才能插入元素
public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        if (count.get() == capacity)
            return false;
        final int c;
        final Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            //连续两次判断,是为了防止在第一次判断与获取ReentrantLock锁之间有节点添加进来
            if (count.get() == capacity)
                return false;
            enqueue(node);
            //原子操作
            c = count.getAndIncrement();  //getAndIncrement()为原子递增当前值,并返回递增前的旧值
            //与ArrayBlockingQueue不一样,这里notFull()唤醒入队线程是在offer()中,而不是在poll()中。
            //并且ArrayBlockingQueue中的notFull()是在dequeue中,
            //究其原因应该在两者使用的参数不一样,所以把他们放在不同的方法中
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
         //c == 0说明offer()操作之前为空,操作之后有节点进入,通知出队线程队列非空
        if (c == 0)    
            signalNotEmpty();     //类似于ArrayBlockingQueue的offer(),会提醒阻塞的出队线程
        return true;
 }


/**
     * 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();
        }
    }


public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {

        if (e == null) throw new NullPointerException();
        long nanos = unit.toNanos(timeout);
        final int c;
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            while (count.get() == capacity) {
                if (nanos <= 0L)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            //与其他的进队不一样,只有在进队的时候才将e包装成node节点,
            //而put()与offer()都是在获取锁之前就进行过包装
            enqueue(new Node<E>(e));      
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return true;
    }

注意:与ArrayBlockingQueue不一样,这里notFull()唤醒入队线程是在offer()中,而不是在poll()中,因为这里是两把锁。offer()与offer(E e, long timeout, TimeUnit unit)(与put()类似)最大的不同是在try-finally中第一个判断,offer()中是if条件判断,offer(E e, long timeout, TimeUnit unit)中是while()条件判断,因为offer()是非阻塞的,不满足条件立即返回,而offer(E e, long timeout, TimeUnit unit)是指定时间内阻塞的,所以要在规定时间内要一直循环直到时间耗尽或者线程被唤醒

这里有个疑问,为什么LinkedBlockingQueue中的offer()与poll()比ArrayBlockingQueue中的各多了一次count 判断?可能是LinkedBlockingQueue中的count是原子类,执行get()方法是原子操作,而ArrayBlockingQueue中count不是原子类型,操作会受其他线程影响

 2.put操作,向队列尾部插入一个元素,如果队列有空闲则插入后直接返回true,如果队列已经满则阻塞当前线程知道队列有空闲插入成功后返回true,如果在阻塞的时候被其他线程设置了中断标志,则被阻塞线程会抛出InterruptedException 异常而返回,另外如果 e 元素为 null 则抛出 NullPointerException 异常。源码如下:

public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        final int c;
        final 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.
             * 注意count在wait guard中使用,即使它没有被锁保护。这是因为count只能在此时减少(所有其他put都被锁定关闭),如果它因容量更改,我们(或其他一些等待put)将收到信号。类似地,count在其他等待守卫中的所有其他用途也是如此。
             */
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            // 如果现队列长度如果小于容量
            // 就再唤醒一个阻塞在notFull条件上的线程
            // 这里为啥要唤醒一下呢?
            // 因为可能有很多线程阻塞在notFull这个条件上的
            // 而取元素时只有取之前队列是满的才会唤醒notFull
            // 为什么队列满的才唤醒notFull呢?
            // 因为唤醒是需要加putLock的,这是为了减少锁的次数
            // 所以,这里索性在放完元素就检测一下,未满就唤醒其它notFull上的线程
            // 说白了,这也是锁分离带来的代价
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }
/**
     * Links node at end of queue.
     *
     * @param node the node
     */
    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }

(1)使用putLock加锁;

(2)如果队列满了就阻塞在notFull条件上;

(3)否则就入队;

(4)如果入队后元素数量小于容量,唤醒其它阻塞在notFull条件上的线程;

(5)释放锁;

(6)如果放元素之前队列长度为0,就唤醒notEmpty条件;

出队

出队同样也有四个方法,我们依次分析一下:

1.poll操作,从队列头部获取并移除一个元素,如果队列为空则返回 null,该方法是不阻塞的。源码如下:

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

/**
     * Removes a node from head of queue.出队列操作,因为队列的head节点为null节点,在出队列时,会始终保持head节点为空,next节点为真正意义上的首节点
     *
     * @return the node
     */
    private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        // assert head.item == null;
        //要分清对象引用与真正在堆中的对象的区别,引用和其被引用的对象共用一组属性
        Node<E> h = head;     //这里为什么要引入h节点?感觉没必要
        Node<E> first = h.next;
        h.next = h; // help GC
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }

  poll 代码逻辑比较简单,值得注意的是获取元素时候只操作了队列的头节点。

 2.peek 操作,获取队列头部元素但是不从队列里面移除,如果队列为空则返回 null,该方法是不阻塞的。源码如下:

public E peek() {
        final AtomicInteger count = this.count;
        if (count.get() == 0)
            return null;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            return (count.get() > 0) ? head.next.item : null;   //写的很精炼,直接通过count是否大于0来判断而不是通过head.next == null?来判断
        } finally {
            takeLock.unlock();
        }
}

3.take 操作,获取当前队列头部元素并从队列里面移除,如果队列为空则阻塞当前线程直到队列不为空,然后返回元素,如果在阻塞的时候被其他线程设置了中断标志,则被阻塞线程会抛出InterruptedException 异常而返回。源码如下:

public E take() throws InterruptedException {
        final E x;
        final int c;
        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;
}

当队列为空时,就加入到notEmpty(的条件等待队列中,当队列不为空时就取走一个元素,当取完发现还有元素可取时,再通知一下自己的伙伴(等待在条件队列中的线程);最后,如果队列从满到非满,通知一下put线程。

4.remove操作,删除队列里面指定元素,有则删除返回 true,没有则返回 false之前的入队和出队都是只获取一个锁,而remove()方法需要同时获得两把锁,源码如下:


    public boolean remove(Object o) {
        if (o == null) return false;
        fullyLock();
        try {
            for (Node<E> pred = head, p = pred.next;
                 p != null;
                 pred = p, p = p.next) {
                if (o.equals(p.item)) {
                    unlink(p, pred);
                    return true;
                }
            }
            return false;
        } finally {
            fullyUnlock();
        }
    }

    void fullyLock() {
        putLock.lock();
        takeLock.lock();
    }

    void fullyUnlock() {
        takeLock.unlock();
        putLock.unlock();
    }

    /**
     * 将p与它的前继节点pred断开连接,目的是删除p.
     */
    void unlink(Node<E> p, Node<E> pred) {
        // assert putLock.isHeldByCurrentThread();
        // assert takeLock.isHeldByCurrentThread();
        // p.next is not changed, to allow iterators that are
        // traversing p to maintain their weak-consistency guarantee.p.next没有改变,允许遍历p的迭代器维护它们的弱一致性保证。
        p.item = null;
        pred.next = p.next;    //先将pred.next赋值为p.next
        if (last == p)
            last = pred;        //last指针也要处理
        if (count.getAndDecrement() == capacity)     //还要唤醒添加线程,这里为什么不唤醒出队线程。有待思考?
            notFull.signal();
    }

那么问题来了,为什么remove()方法同时需要两把锁?
remove()操作会从队列的头遍历到尾,用到了队列的两端,所以需要对两端加锁,而对两端加锁就需要获取两把锁;入队和出队均只在队列的一端操作,所以只需获取一把锁。

size()方法

size()方法用于返回队列中元素的个数,其实现如下: 

public int size() {
        return count.get();
    }

由于count是一个AtomicInteger的变量,所以该方法是一个原子性的操作,是线程安全的。

最后用一张图来加深LinkedBlockingQueue的理解,如下图:

因此我们要思考一个问题:为何 ConcurrentLinkedQueue 中需要遍历链表来获取 size 而不使用一个原子变量呢?

这是因为使用原子变量保存队列元素个数需要保证入队出队操作和操作原子变量是原子操作,而ConcurrentLinkedQueue 是使用 CAS 无锁算法的,所以无法做到这个。

总结

在上面分析LinkedBlockingQueue的源码之后,可以与ArrayBlockingQueue做一个比较。
相同点有如下2点:
1. 不允许元素为null
2. 线程安全的队列

不同点有如下几点:
1. ArrayBlockingQueue底层基于定长的数组,所以容量限制了;LinkedBlockingQueue底层基于链表实现队列,所以容量可选,如果不设置,那么容量是int的最大值
2. ArrayBlockingQueue内部维持一把锁和两个条件,同一时刻只能有一个线程队列的一端操作,导致入队出队相互阻塞,效率低下;LinkedBlockingQueue内部维持两把锁和两个条件,同一时刻可以有两个线程在队列的两端操作,但同一时刻只能有一个线程在一端操作,效率较高。
3. LinkedBlockingQueue的remove()类似方法时,由于需要对整个队列链表实现遍历,所以需要获取两把锁,对两端加锁。

4.LinkedBlockingQueue在puts操作都会生成新的Node对象,takes操作Node对象在某一时间会被GC,可能会影响GC性能;ArrayBlockingQueue是固定的数组长度循环使用, 不会出现对象的产生与回收

 

为了让大家更加明白 ReentrantLock,我这里给出一个例子供大家学习

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Create by xuantang
 * @date on 8/22/18
 */
public class ReentrantLockDemo {
    private static ReentrantLock mLock = new ReentrantLock();
    private static Condition mCondition = mLock.newCondition();

    public static void main(String[] args) {
        new WaitThread("waiter one").start();
        new WaitThread("waiter two").start();
        new WaitThread("waiter three").start();
        new NotifyThread("notify one").start();
    }

    static class WaitThread extends Thread {
        WaitThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            try {
                mLock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                System.out.println(this.getName() + " Waiting......");
                mCondition.await();
                System.out.println(this.getName() + " Finished.....");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                mLock.unlock();
            }
        }
    }

    static class NotifyThread extends Thread {

        NotifyThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            try {
                mLock.lockInterruptibly();

                mCondition.signal();
                System.out.println(this.getName() + " Notify.....");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                mLock.unlock();
            }
        }
    }
}

输入结果,只能唤醒一个,当然你可以使用 signalAll() 唤醒所有的

waiter one Waiting......
waiter two Waiting......
waiter three Waiting......
notify one Notify.....
waiter one Finished.....

参考:

Java并发编程笔记之LinkedBlockingQueue源码探究

https://www.cnblogs.com/huangjuncong/p/9218194.html

入队出队总结

ConcurrentLinkedQueue 与LinkedBlockingQueue比较

ReentrantLock使用实例

死磕 java集合之LinkedBlockingQueue源码分析

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章