深入JAVA并发编程(六):并发容器(二)

并发容器

ConcurrentLinkedQueue

ConcurrentLinkedQueue是线程安全的无界非阻塞队列,其底层数据结构使用单向链表实现,出队和入队操作使用CAS来实现线程安全。它采用的是先进先出的规则,当我们增加一个元素时,它会添加到队列的末尾,当我们取一个元素时,它会返回一个队列头部的元素。

源码分析

既然是链表,就肯定是由节点组成的,我们来看一下Node类

	private static class Node<E> {
		//存储的数据
        volatile E item;
        //下一个节点引用
        volatile Node<E> next;

        /**
         * Constructs a new node.  Uses relaxed write because item can
         * only be seen after publication via casNext.
         */
        //创建一个Node节点
        Node(E item) {
            UNSAFE.putObject(this, itemOffset, item);
        }
		//CAS修改节点的item
        boolean casItem(E cmp, E val) {
            return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
        }
		//懒修改节点的next
        void lazySetNext(Node<E> val) {
            UNSAFE.putOrderedObject(this, nextOffset, val);
        }
		//cas修改节点的next节点
        boolean casNext(Node<E> cmp, Node<E> val) {
            return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
        }

       	......
    }

再来看一下ConcurrentLinkedQueue中的两个重要成员变量

	//头节点
	private transient volatile Node<E> head;
	
	//尾节点
	private transient volatile Node<E> tail;

然后我们看下无参构造函数

    public ConcurrentLinkedQueue() {
        head = tail = new Node<E>(null);
    }

可以看到无参函数中会先构造一个节点,但是item为null,然后头节点和尾节点都指向该节点。如下图所示

在这里插入图片描述

接下来我们来看下重要的入队操作

	public boolean add(E e) {
        return offer(e);
    }
	public boolean offer(E e) {
		//入队元素不能为空
        checkNotNull(e);
        //创建新节点
        final Node<E> newNode = new Node<E>(e);
		//死循环自旋插入节点,从尾节点插入
		//把尾节点赋值给p
        for (Node<E> t = tail, p = t;;) {
        	//获取尾节点的下一个节点
            Node<E> q = p.next;
            //如果尾节点的下一个节点为空
            if (q == null) {
                //说明p为尾节点
                //CAS设置当前尾节点的next指向新的节点
                if (p.casNext(null, newNode)) {
                    //更新尾节点,不是实时更新的,而是每插入两个节点才会更新一次尾节点
                    //这个过程下面分析
                    if (p != t) 
                        casTail(t, newNode);  
                    return true;
                }
            }
            //寻找新的head,看下面分析
            else if (p == q)
                // We have fallen off list.  If tail is unchanged, it
                // will also be off-list, in which case we need to
                // jump to head, from which all live nodes are always
                // reachable.  Else the new tail is a better bet.
                p = (t != (t = tail)) ? t : head;
            //寻找尾节点,看下面分析
            else
                // Check for tail updates after two hops.
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

入队的过程代码不多,不过确实有点复杂,我们来详细画图分析一下这个过程。
刚开始初始化一个ConcurrentLinkedQueue,里面只有一个节点。

在这里插入图片描述

此时一个线程调用offer操作,首先赋值p=t=tail,然后q=p.next,当前就一个节点,所以q=null,因为q=null,所以程序进入if语句,将新的节点CAS设置为旧的尾节点的next,然后进行判断,当前的p是等于t的,所以没有进入,而是直接返回了true。注意,这里没有更新尾节点的值,尾节点仍然还是旧值,还是指向第一个节点。此时情况如下:

在这里插入图片描述

之后又一个线程调用了offer方法,进行赋值p=t=tail,我们别忘了,这时候的tail可不是真正的tail,它还是指向头节点的,然后q=p.next,此时p.next是有值的,就是我们刚才插入的node1,然后进行判断,q!=null,跳过进行下一个判断,并且p!=q,所以执行最后一个else里面的内容,因为当前p==t,所以执行该语句后的结果是将q赋值给p,也就是p=q=node1,然后再次循环,此时q=p.next,所以q=null,进入判断,CAS设置新节点为node1的next节点,然后继续进行判断,此时p!=t成立,进入判断,更新尾节点的值为刚刚新插入的节点node2。通过上面的过程我们可以知道,tail的值并不是实时的,每插入两个元素才会更新一次。此时队列情况如下:

在这里插入图片描述
然后我们再次插入一个元素,情况就变成如下:

在这里插入图片描述

此时我们发现还有p==q的这种情况并未执行到,其实这一步需要在执行poll操作后出现某种情况时才会执行。poll操作的作用是获取并移除队列的头,如果队列为空则返回null。我们先来看一下poll代码。

	public E poll() {
		//标签,能和continue一起实现和goto一样的效果
        restartFromHead:
        //死循环
        for (;;) {
        	//将head赋值,p=h=head
            for (Node<E> h = head, p = h, q;;) {
            	//获取当前p的节点值
                E item = p.item;
				//如果当前节点值不为空,则CAS设置item为null
                if (item != null && p.casItem(item, null)) {
                    //更新头节点,也是每两次更新一次,和offer差不多
                    if (p != h) // hop two nodes at a time
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    return item;
                }
                //空队列
                else if ((q = p.next) == null) {
                    updateHead(h, p);
                    return null;
                }
                // 当一个线程在poll的时候,另一个线程已经把当前的p从队列中删除
                //则跳转到restartFromHead位置重新开始寻找头节点
                else if (p == q)
                    continue restartFromHead;
                else
                    p = q;
            }
        }
    }
	final void updateHead(Node<E> h, Node<E> p) {
		//更新头节点,并且将原来头节点的next指向自己
        if (h != p && casHead(h, p))
            h.lazySetNext(h);
    }

然后我们继续在上面状态的前提下,此时有线程执行了poll操作。

在这里插入图片描述

首先进行赋值,p=h=head,因为head的item为空,所以第一个判断跳过,然后q=p.next=node1,q不为空,第二个判断跳过,又因为p!=q,所以执行最后的else,进行赋值p=q=node1,然后继续执行循环,此时p的item不为空,然后cas设置node1的item为空,设置成功后进入判断,因为p!=h,所以执行更新头节点的方法,传参时的语句((q = p.next) != null) ? q : p,因为p.next不为null,所以更新头节点为node2。如下图所示

在这里插入图片描述

接下来又一个线程执行了poll操作,首先进行赋值p=h=node2,p.item不为空,然后CAS将item设置为空,继续向下执行p!=h不成立,直接返回。此时情况如下:

在这里插入图片描述

然后另一个线程继续执行poll,p=h=node2,此时p.item为空,所以跳过判断,q=p.next=node3,q不为空,执行最后的语句将q赋值给p,p=q=node3,继续向下执行,因为p.item不为空,所以将node3的item设置为空,成功后重新设置头节点,然后就成了如下情况

在这里插入图片描述

紧接着有线程执行了offer操作,进行赋值p=t=tail,q=p.next=p,这时候就造成了p==q的情况,然后会做什么操作呢?看注释就写的很明白了,此时tail已经掉出了队列,我们需要重新找到head,因为从head总是能找到所有在队列中的元素,然后设置新的尾节点。首先判断t是否等于tail,如果等于,则赋值p=head,然后继续执行循环,q=p.next=null,因为q=null,所以进入判断,然后设置head的next节点为新节点,更新尾节点。

在这里插入图片描述

接下来看一下peek操作,peek方法是获取队列头部元素,但是只获取不移除,如果队列为空返回null。peek和poll其实差不多。

    public E peek() {
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
            	//获取头节点的值
                E item = p.item;
                //更新头节点,返回值
                if (item != null || (q = p.next) == null) {
                    updateHead(h, p);
                    return item;
                }
                else if (p == q)
                    continue restartFromHead;
                else
                    p = q;
            }
        }
    }

接下来看一下size方法,在并发环境下并不准确,因为没有加锁,所以在调用该方法的过程中可能增删元素。

	public int size() {
        int count = 0;
        //遍历节点
        for (Node<E> p = first(); p != null; p = succ(p))
            if (p.item != null)
                // Collection.size() spec says to max out
                if (++count == Integer.MAX_VALUE)
                    break;
        return count;
    }
	//获取第一个队列元素,哨兵元素不算,没有则返回null
    Node<E> first() {
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                boolean hasItem = (p.item != null);
                if (hasItem || (q = p.next) == null) {
                    updateHead(h, p);
                    return hasItem ? p : null;
                }
                else if (p == q)
                    continue restartFromHead;
                else
                    p = q;
            }
        }
    }
	//返回当前节点的next节点,如果是自引用节点则返回头节点
    final Node<E> succ(Node<E> p) {
        Node<E> next = p.next;
        return (p == next) ? head : next;
    }

最后看一下删除元素,如果存在则删除,存在多个则删除第一个。

    public boolean remove(Object o) {
        if (o != null) {
            Node<E> next, pred = null;
            //遍历队列
            for (Node<E> p = first(); p != null; pred = p, p = next) {
            	//删除标志
                boolean removed = false;
                //获取当前节点的值
                E item = p.item;
                //如果值不为空
                if (item != null) {
                	//如果当前节点不是要删除的节点
                    if (!o.equals(item)) {
                    	//获取下一个节点,跳出,直接执行下一次循环
                        next = succ(p);
                        continue;
                    }
                    //将item设置为null
                    removed = p.casItem(item, null);
                }
				//获取下一个节点
                next = succ(p);
                //如果当前节点的前驱节点和后继节点不为空,则将前驱节点的next设置为后继节点
                if (pred != null && next != null) // unlink
                    pred.casNext(p, next);
                if (removed)
                    return true;
            }
        }
        return false;
    }

ConcurrentLinkedQueue特性

虽然ConcurrentLinkedQueue的性能很好,但是在调用size()方法的时候,会遍历一遍集合,对性能损害较大,执行很慢,因此应该尽量的减少使用这个方法,如果判断是否为空,最好用isEmpty()方法。

ConcurrentLinkedQueue不允许插入null元素,会抛出空指针异常。

ConcurrentLinkedQueue是无界的,所以使用时,一定要注意内存溢出的问题。

队列中的有效元素都可以从head通过succ方法遍历到

head和tail不会是null(哨兵节点的设计)

和ConcurrentLinkedQueue类似的还有一个ConcurrentLinkedDeque类,该类从JDK1.7引入,不同的是其内部是一个双向链表结构,可以在任意一端入队,也可以在任意一端出队,因为这个特性,实际运用中可以当作线程安全高效的栈来使用,底层实现原理和ConcurrentLinkedQueue一样,采取了无锁算法,基于CAS+自旋实现,入队出队等操作的原理和ConcurrentLinkedQueue也高度类似,这里就不再细说。有兴趣的可以自己去看下源码

阻塞队列

阻塞队列是指当前队列不满足条件时会阻塞线程的队列,例如队列已满就会阻塞入队操作的线程,队列为空则阻塞出队操作的线程。JAVA并发包提供了七种阻塞队列,它们都是线程安全的,接下来我们来看一下这些阻塞队列。

LinkedBlockingQueue

LinkedBlockingQueue 是一个单向链表实现的无界阻塞队列,其实可以看做是有界的,因为默认大小是Integer.MAX_VALUE,但是这个数确实太大了。它是先进先出的队列。内部由两个ReentrantLock来实现出入队列的线程安全,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能,但是要注意内存溢出的情况。

源码分析

因为底层是链表结构,首先我们来看下构成链表的节点类

    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; }
    }

然后我们来看下类中的重要属性

	
	//阻塞队列的大小,如果不传则默认为Integer.MAX_VALUE
    private final int capacity;

    //当前队列中的元素个数
    private final AtomicInteger count = new AtomicInteger();

    //头节点
    transient Node<E> head;

    //尾节点
    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();

当线程执行take、poll等出队操作时需要获取takeLock锁,从而保证只有一个线程可以对队列的头节点进行出队操作,因为条件变量notEmpty是takeLock的,所以调用notEmpty的方法前必须先获取到takeLock。notEmpty内部维护着一个条件队列,当线程获取到takeLock调用notEmpty的await方法时,当前线程会被阻塞,然后放入条件队列进行等待,直到有线程调用了notEmpty的signal方法进行唤醒。另外入队锁同理。

接下来我们来看下构造函数

	//调用有参构造方法,构造一个Integer.MAX_VALUE大小的队列
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

	//构建一个capacity大小的队列,并且创建一个item为空的节点,head和last同时指向该节点
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

    //创建一个Integer.MAX_VALUE大小的队列,然后将传入的集合里的元素依次入队
    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.set(n);
        } finally {
            putLock.unlock();
        }
    }

然后我们来看下该类的一些重要方法

首先来看一下put方法,该方法向队列尾部插入一个元素,如果队列中有空闲则插入后直接返回,如果队列已满则阻塞当前线程,直到队列有空闲插入成功后返回。如果在阻塞时被其他线程设置了中断标志,则被阻塞的线程会抛出InterruptedException异常而返回。

    public void put(E e) throws InterruptedException {
    	//如果插入的元素为null,则抛出异常
        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 {
           	//如果队列满,则等待,用while是为了防止虚假唤醒的情况
            while (count.get() == capacity) {
                notFull.await();
            }
            //入队
            enqueue(node);
            //增加计数
            c = count.getAndIncrement();
            //判断是否还有空间,如果有,则唤醒其他被阻塞的线程继续执行put操作
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
        	//释放锁
            putLock.unlock();
        }
        //如果c==0,则说明之前队列中无元素,此时增加了一条,所以需要唤醒其他获取操作线程执行出队等操作
        if (c == 0)
            signalNotEmpty();
    }
	//入队操作,先把节点赋值给当前尾节点的next,再将尾节点设置为当前节点
    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }
    private void signalNotEmpty() {
    	//先获取出队的独占锁,调用条件变量的方法时一定先要获取对应的锁
        final ReentrantLock takeLock = this.takeLock;
        //加锁
        takeLock.lock();
        try {
        	//唤醒
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

接下来我们来看下offer方法,该方法和put基本类似,唯一的区别就是当队列满时直接返回false,该方法是不阻塞的。

	public boolean offer(E e) {
		//如果元素为null,抛出异常
        if (e == null) throw new NullPointerException();
        //获取当前队列中的元素
        final AtomicInteger count = this.count;
        //如果队列已满,直接返回false
        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;
    }

此外还有一个offer方法可以设置超时时间

    public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {
		//如果元素为null,抛出异常
        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();
        return true;
    }

接下来我们来看下出队的方法。
take方法获取当前队列的头部元素并从队列中移除它。如果队列为空则阻塞当前线程直到队列不为空返回元素,如果在阻塞时被其他线程设置了中断标志,则被阻塞线程会抛出InterruptedException异常。

    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();
        }
        //如果c==capacity则说明之前队列是满的,此时出队了一条数据,所以唤醒入队线程继续入队
        if (c == capacity)
            signalNotFull();
        return x;
    }
	//出队操作
    private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        // assert head.item == null;
        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;
    }

接下来我们来看看poll方法,和take操作基本一样
poll方法从队列头部获取并移除一个元素,队列为空不等待直接返回null。

    public E poll() {
        final AtomicInteger count = this.count;
        //队列为空直接返回null
        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;
    }

还有一个同样类似的peek方法,该方法获取头部元素但是不移除,如果队列为空则返回null。

    public E peek() {
    	//如果队列为空则返回null
        if (count.get() == 0)
            return null;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            Node<E> first = head.next;
            if (first == null)
                return null;
            else
                return first.item;
        } finally {
            takeLock.unlock();
        }
    }

看完入队出队等操作后,我们来看一下删除remove方法。
remove方法删除队列中的元素,有则删除返回true,没有返回false

    public boolean remove(Object o) {
    	//元素为null,返回false
        if (o == null) return false;
        //双重加锁
        //这样不管其他线程执行入队还是出队操作都会被阻塞挂起
        fullyLock();
        try {
        	//遍历链表,删除元素
            for (Node<E> trail = head, p = trail.next;
                 p != null;
                 trail = p, p = p.next) {
                if (o.equals(p.item)) {
                    unlink(p, trail);
                    return true;
                }
            }
            return false;
        } finally {
        	//双重解锁
            fullyUnlock();
        }
    }
    void fullyLock() {
        putLock.lock();
        takeLock.lock();
    }

    void fullyUnlock() {
        takeLock.unlock();
        putLock.unlock();
    }
	//删除元素
    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();
    }

ArrayBlockingQueue

ArrayBlockingQueue是一个数组实现的有界阻塞队列,此队列元素先进先出。该队列内部出队入队都是用的同一个独占锁,所以性能比LinkedBlockingQueue差一点。

源码分析

首先我们来看下该类中的重要属性

	//数组,用来存放队列元素
    final Object[] items;

    //出队元素下标
    int takeIndex;

    //入队元素下标
    int putIndex;

   	//队列元素数量
    int count;

    //出、入队操作时需要获取的独占锁
    final ReentrantLock lock;

    //条件变量,当队列为空时,执行出队操作的线程会被放入这个条件队列等待
    private final Condition notEmpty;

    //条件变量,当队列满时,执行入队操作的线程会被放入这个条件队列等待
    private final Condition notFull;

接下来我们来看下它的构造函数,因为是有界队列,所以必须传入容量,默认是非公平锁,也就是说当多个线程同时操作队列时,未获取锁而阻塞的线程再次抢占锁的资格时完全是随机的。

	//创建一个指定容量的、非公平的阻塞队列
    public ArrayBlockingQueue(int capacity) {
        this(capacity, 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, boolean fair,
                              Collection<? extends E> c) {
        this(capacity, fair);

        final ReentrantLock lock = this.lock;
        lock.lock(); // Lock only for visibility, not mutual exclusion
        try {
            int i = 0;
            try {
                for (E e : c) {
                    checkNotNull(e);
                    items[i++] = e;
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                throw new IllegalArgumentException();
            }
            count = i;
            putIndex = (i == capacity) ? 0 : i;
        } finally {
            lock.unlock();
        }
    }

我们继续来看下类中的重要方法

offer方法,向队列尾部插入一个元素,如果队列未满则插入成功返回,如果队列已满则直接返回false,该方法不阻塞。

    public boolean offer(E e) {
    	//检查元素是否为null,如果是抛出异常
        checkNotNull(e);
        //获取独占锁,加锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	//如果队列已满,则直接返回false
            if (count == items.length)
                return false;
            else {
            	//入队
                enqueue(e);
                return true;
            }
        } 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++;
        //唤醒因为调用出队操作而阻塞的线程
        notEmpty.signal();
    }

put方法,向队列尾部插入一个元素,如果队列未满则插入成功,如果队列已满则阻塞当前线程直到队列有空闲插入成功返回,如果在阻塞过程时被其他线程设置了中断标志,则抛出InterruptedException异常返回。

    public void put(E e) throws InterruptedException {
    	//检查元素是否为空,如果为空则抛出异常
        checkNotNull(e);
        //获取独占锁,加锁
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
        	//如果队列已满,则阻塞当前线程等待
            while (count == items.length)
                notFull.await();
            //入队
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

接下来看一下poll操作,从队列头部获取一个元素并移除,如果队列为空返回null,该方法不阻塞。

    public E poll() {
    	//获取锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	//如果队列为空,返回null,否则获取头部元素并移除
            return (count == 0) ? null : 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];
        //将数组中该位置值置为null
        items[takeIndex] = null;
        //计算下一个出队元素的位置
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        //唤醒执行入队操作阻塞的线程
        notFull.signal();
        return x;
    }

接下里看看take方法,获取当前队列头部元素并从队列中移除它,如果队列为空则阻塞当前线程直到队列不为空返回元素,如果在阻塞过程时被其他线程设置了中断标志,则抛出InterruptedException异常返回。

    public E take() throws InterruptedException {
    	//获取锁
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
        	//如果队列为空则阻塞当前线程
            while (count == 0)
                notEmpty.await();
            //出队并移除
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

还有peek操作,peek方法更简单,获取队列头部元素但是不移除

    public E peek() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return itemAt(takeIndex); // null when queue is empty
        } finally {
            lock.unlock();
        }
    }

最后看看size操作,这里size操作加了锁,也就保证了size返回的数量是准确的。

    public int size() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }

PriorityBlockingQueue

PriorityBlockingQueue是带优先级的无界阻塞队列,每次出队都返回优先级最高或者最低的元素。其内部优先级的实现原理是采用二叉树堆实现的,所以直接遍历队列元素不保证有序,默认使用对象的compareTo方法进行比较,当然也可以自定义比较器。

源码分析

首先我们来看下类里的重要属性

    //默认数组容量
    private static final int DEFAULT_INITIAL_CAPACITY = 11;

    //数组最大值
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    //存放队列元素的数组
    private transient Object[] queue;

    //当前队列的元素个数
    private transient int size;

    //比较器,如果优先级队列使用元素的自然顺序compareTo方法,则为空。 
    private transient Comparator<? super E> comparator;

    //独占锁
    private final ReentrantLock lock;

    //条件变量,当队列为空时,执行出队操作的线程会被放入这个条件队列等待
    private final Condition notEmpty;

    //自旋锁标识,使用CAS保证只有一个线程可以进行扩容队列,0表示没有扩容,1表示正在扩容
    private transient volatile int allocationSpinLock;

    //优先级队列,仅用于序列化
    private PriorityQueue<E> q;

然后我们来看下该类的构造方法

    //创建一个默认容量的优先级无界阻塞队列
    public PriorityBlockingQueue() {
        this(DEFAULT_INITIAL_CAPACITY, null);
    }

    //创建一个指定容量的优先级无界阻塞队列
    public PriorityBlockingQueue(int initialCapacity) {
        this(initialCapacity, null);
    }

  	//创建一个指定容量的优先级无界阻塞队列,初始化队列,锁,比较器等
    public PriorityBlockingQueue(int initialCapacity,
                                 Comparator<? super E> comparator) {
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.lock = new ReentrantLock();
        this.notEmpty = lock.newCondition();
        this.comparator = comparator;
        this.queue = new Object[initialCapacity];
    }

   	//将集合里的元素复制到队列中
    public PriorityBlockingQueue(Collection<? extends E> c) {
        this.lock = new ReentrantLock();
        this.notEmpty = lock.newCondition();
        boolean heapify = true; // true if not known to be in heap order
        boolean screen = true;  // true if must screen for nulls
        if (c instanceof SortedSet<?>) {
            SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
            this.comparator = (Comparator<? super E>) ss.comparator();
            heapify = false;
        }
        else if (c instanceof PriorityBlockingQueue<?>) {
            PriorityBlockingQueue<? extends E> pq =
                (PriorityBlockingQueue<? extends E>) c;
            this.comparator = (Comparator<? super E>) pq.comparator();
            screen = false;
            if (pq.getClass() == PriorityBlockingQueue.class) // exact match
                heapify = false;
        }
        Object[] a = c.toArray();
        int n = a.length;
        // If c.toArray incorrectly doesn't return Object[], copy it.
        if (a.getClass() != Object[].class)
            a = Arrays.copyOf(a, n, Object[].class);
        if (screen && (n == 1 || this.comparator != null)) {
            for (int i = 0; i < n; ++i)
                if (a[i] == null)
                    throw new NullPointerException();
        }
        this.queue = a;
        this.size = n;
        if (heapify)
            heapify();
    }

接下来我们来看下类中的常用方法

首先是入队方法offer,将一个元素入队

    public boolean offer(E e) {
    	//元素不能为空
        if (e == null)
            throw new NullPointerException();
       	//加锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        int n, cap;
        Object[] array;
        //如果当前元素个数大于等于队列长度,则扩容
        while ((n = size) >= (cap = (array = queue).length))
            tryGrow(array, cap);
        try {
        	//获取比较器,添加元素,使用元素自身的排序规则时,默认的比较器为null
            Comparator<? super E> cmp = comparator;
            if (cmp == null)
                siftUpComparable(n, e, array);
            else
                siftUpUsingComparator(n, e, array, cmp);
           //将元素的个数+1
            size = n + 1;
           //唤醒阻塞的线程
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
        return true;
    }

入队时,该方法首先会判断队列是否需要扩容,那么我们来看下它是如何扩容的

    private void tryGrow(Object[] array, int oldCap) {
    	//释放获取的锁
        lock.unlock(); 
        Object[] newArray = null;
        //如果当前没有线程扩容,使用CAS将扩容标识从0变成1,代表正在进行扩容
        if (allocationSpinLock == 0 &&
            UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                     0, 1)) {
            try {
            	//生成新的队列容量
                int newCap = oldCap + ((oldCap < 64) ?
                                       (oldCap + 2) : // grow faster if small
                                       (oldCap >> 1));
                //如果新的容量超过最大容量
                if (newCap - MAX_ARRAY_SIZE > 0) {    // possible overflow
                    int minCap = oldCap + 1;
                    if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                        throw new OutOfMemoryError();
                   	//赋值最大值
                    newCap = MAX_ARRAY_SIZE;
                }
                if (newCap > oldCap && queue == array)
                    newArray = new Object[newCap];
            } finally {
                allocationSpinLock = 0;
            }
        }
        //因为使用的是CAS操作,所以当第一个线程执行上面的方法成功后,那么其他线程CAS就会执行失败,就会执行这里,这里会让其他线程让出CPU,尽量让刚刚扩容的线程获取锁
        if (newArray == null) // back off if another thread is allocating
            Thread.yield();
        lock.lock();
        //将新的数组赋值给queue
        if (newArray != null && queue == array) {
            queue = newArray;
            System.arraycopy(array, 0, newArray, 0, oldCap);
        }
    }

这里扩容为什么要释放锁呢?主要是为了性能考虑,扩容是需要时间的,如果不释放锁那么执行出队和入队操作的线程全部都会被阻塞,所以为了提高性能,使用CAS来控制只有一个线程可以扩容,但是其他出队入队操作的线程并不受影响。扩容完毕后会将扩容标志重新设置为0.

当扩容完毕后,会重新加锁,然后将老队列的元素复制到新队列,然后继续向下执行。

接下来呢,会对数组中的元素进行二叉树建堆操作,这样做的目的是让优先级最高的元素始终在队列的头部。这个建堆的原理是这样的,取当前堆树中最下层的父节点,然后与当前入队的元素比较,如果元素大于该父节点直接插入即可,如果小于该父节点,则将父节点下移,使该元素再与上层父节点继续比较,直到找到合适的位置,是一个自下而上的过程。我们来看下建堆的过程。

    private static <T> void siftUpComparable(int k, T x, Object[] array) {
        Comparable<? super T> key = (Comparable<? super T>) x;
        //队列元素大于0则判断入队位置,否则直接插入
        while (k > 0) {
        	//计算父节点的索引
            int parent = (k - 1) >>> 1;
            Object e = array[parent];
            //如果key大于父节点,就结束循环直接赋值
            if (key.compareTo((T) e) >= 0)
                break;
            //否则当前节点小于父节点,则让父节点下移,让当前节点与祖父节点继续比较
            //直到合适的位置
            array[k] = e;
            k = parent;
        }
        array[k] = key;
    }

我们画图来说明一下这个建堆的过程。
假设队列的初始容量为2,里面的元素类型为Integer。
初始化后的队列如下。
在这里插入图片描述

接下来线程调用offer(2)进行入队操作,元素不为空并且也不需要扩容,所以直接执行siftUpComparable建堆方法,由于当前队列为空,所以直接插入,此时的情况如下:

在这里插入图片描述

然后我们继续调用offer(4),因为元素不为空,也不需要扩容,所以执行siftUpComparable建堆方法,此时k=1>0,所以进入循环,进行计算赋值,parent=0,e=2,因为key=4>e,所以直接退出循环,进行赋值。此时情况如下:

在这里插入图片描述

然后继续执行offer(6),此时因为size=length,所以需要扩容。进入扩容方法,因为2<64,所以新数组的容量为2+2+2=6,然后将旧数组的值复制到新数组,接着执行siftUpComparable建堆方法,此时k=2,所以进入循环执行,parent=0,e=2,key=6,key>e,所以退出循环,直接赋值。此时情况如下:

在这里插入图片描述

最后呢我们来执行offer(1),和上面一样执行,同样不需要扩容,执行siftUpComparable建堆操作,因为k=3,所以进入循环,parent=1,e=4,key=1,key<e,所以把元素e移动到array[k]处,也就是array[3]=4,k=parent=1,继续执行循环,parent=0,e=2,key<e,所以将元素e移动到array[1]处,然后k=0退出循环。继续执行array[0]=1,此时情况如下:

在这里插入图片描述

此时,二叉树堆的树形图如下

在这里插入图片描述

堆的根元素是1,这是一个最小堆,当调用出队方法时,会返回堆里面值最小的元素。

接下来我们来看看出队方法poll,poll方法的作用是获取内部堆树的根节点元素,如果队列为空,则返回null。

    public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
    private E dequeue() {
        int n = size - 1;
        //如果队列为空,返回null
        if (n < 0)
            return null;
        else {
            Object[] array = queue;
            //获取队头元素
            E result = (E) array[0];
            //获取队尾元素,并将队尾元素赋值为null
            E x = (E) array[n];
            array[n] = null;
            Comparator<? super E> cmp = comparator;
            //调整二叉树堆
            if (cmp == null)
                siftDownComparable(0, x, array, n);
            else
                siftDownUsingComparator(0, x, array, n, cmp);
            size = n;
            return result;
        }
    }

这个出队的过程比较简单,获取数组中的第一个元素返回即可,但是第一个元素出队之后,当前的堆就乱了,所以之后还有个调整堆的方法,用来将堆调整成最大堆或者最小堆,我们来看下这个方法

    private static <T> void siftDownComparable(int k, T x, Object[] array,
                                               int n) {
        if (n > 0) {
            Comparable<? super T> key = (Comparable<? super T>)x;
            int half = n >>> 1;           // loop while a non-leaf
            while (k < half) {
            	//左子节点
                int child = (k << 1) + 1; // assume left child is least
                Object c = array[child];
                //右子节点
                int right = child + 1;
                //取左右子节点中最小的值
                if (right < n &&
                    ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
                    c = array[child = right];
                // key如果比左右子节点都小,则跳出循环直接结束堆化过程
                if (key.compareTo((T) c) <= 0)
                    break;
                array[k] = c;
                k = child;
            }
            array[k] = key;
        }
    }

这个方法调整堆的思路是这样的,由于队列数组的第0个元素为树根,因此出队时需要移除,这时数组需要重新调整堆,具体是从左右子树中找一个最小的值来当树根,左右子树又会去找自己左右子树中最小的值,直到循环结束。

我们同样用图来说明一下这个调整堆的算法。我们继续使用上面的队列

在这里插入图片描述

首先我们调用poll方法,size=4,n=3,result=1,x=4,此时数组的情况如下:

在这里插入图片描述

然后执行调整堆的方法。首先n>0,进入判断,key=x=4,half=3>>>1=1,因为k=0<half,所以进入while循环,child=1,c=array[1]=2,c是树的左孩子节点然后right=2<n,array[right]就是树的右孩子节点,key>c,array[k]=array[0]=2,k=child=1,然后继续循环,k!<half,所以循环终止,array[k]=array[1]=4。此时情况如下:

在这里插入图片描述

然后我们再次执行poll方法,n=2,size=3,result=2,x=6,array[n]=null,然后执行调整堆的方法。因为n>0,进入判断,key=6,half=1,k=0<half,执行循环,child=1,c=4,right=2,所以array[k]=array[0]=4,k=1,继续执行循环,k!<half,所以赋值,array[1]=6,此时情况如下:

在这里插入图片描述

剩下的元素也同理。

接下来我们来看看出队方法take,该方法同样获取队列内部堆树的根节点元素,但是如果队列为空则阻塞。其他的和poll一样。

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        E result;
        try {
        	//如果队列为空则阻塞
            while ( (result = dequeue()) == null)
                notEmpty.await();
        } finally {
            lock.unlock();
        }
        return result;
    }

最后我们来看看size方法,由于size方法加锁,所以返回的数据是准确的。

    public int size() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return size;
        } finally {
            lock.unlock();
        }
    }

使用案例

把具有优先级的任务放入队列,从队列中获取优先级最高的任务执行。

public class PriorityQueueDemo {
    static class Task implements Comparable<Task>{
        private int priority;

        private String taskName;

        public Task(int priority, String taskName) {
            this.priority = priority;
            this.taskName = taskName;
        }

        @Override
        public int compareTo(Task o) {
            return this.priority<o.priority?-1:(this.priority==o.priority?0:1);
        }

        public void doSomething(){
            System.out.println(taskName+":"+priority);
        }
    }

    public static void main(String[] args) {
        PriorityBlockingQueue<Task> priorityBlockingQueue=new PriorityBlockingQueue<>();
        Random random=new Random();
        for (int i=0;i<10;i++){
            Task task=new Task(random.nextInt(100),"taskName"+i);
            priorityBlockingQueue.offer(task);
        }

        while (!priorityBlockingQueue.isEmpty()){
            Task poll = priorityBlockingQueue.poll();
            if(poll!=null){
                poll.doSomething();
            }
        }
    }
}

在这里插入图片描述

PriorityBlockingQueue在内部使用二叉树堆维护元素优先级,使用数组存储元素,出队时始终保证出队的元素是树的根节点,默认情况下使用元素的compareTo方法比较,当然我们也可以自定义比较器。其内部使用了一个独占锁来控制同时只能有一个线程执行出队入队操作。

DelayQueue

DelayQueue并发队列也是一个无界阻塞队列,它的特点是队列中的每个元素都有过期时间,当从队列获取元素时,只有过期元素才会出队列,队列头元素是最快要过期的元素。
由于队列里的每个元素都需要有过期时间,所以队列里的元素必须要实现提供的Delayed接口,该接口提供一个方法为获取当前元素还有多久过期,并且该接口继承自Comparable接口,队列内部是使用PriorityQueue优先级队列来存放数据的,所以也必须要实现比较方法。

源码分析

首先我们来看下类中的重要属性

	//独占锁
    private final transient ReentrantLock lock = new ReentrantLock();
    //使用PriorityQueue优先级队列来存放数据
    private final PriorityQueue<E> q = new PriorityQueue<E>();
    
	//leader线程作用是用于减少不必要的线程等待。
	//当一个线程调用队列的take方法,如果当前队列中没有过期元素时,会将当前线程变为leader线程
	//然后它会调用条件变量available.awaitNanos(delay)等待一段delay时间,这个delay时间是队列头元素的剩余过期时间
	//除此之外的其他线程就会调用available.await()进行无限等待
	//leader线程延迟时间过期后,会重新再次获取元素,这时候队头的元素已经过期,获取返回,然后通过available.signal()方法唤醒一个无限等待的线程
	//被唤醒的线程就是新的leader线程
    private Thread leader = null;

    //条件变量
    private final Condition available = lock.newCondition();

接下来我们来看下这个类中的常用方法

首先看下offer方法,offer方法作用是插入元素到队列,为空则抛出空指针异常。

    public boolean offer(E e) {
    	//获取独占锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	//调用优先级队列的入队方法入队
            q.offer(e);
            //调用出队方法,由于是优先级队列,所以peek方法返回的并不一定是当前添加的元素
            //如果peek出来的元素是当前插入的,则说明之前队列里没有元素,所以插入之后唤醒线程获取元素
            if (q.peek() == e) {
                leader = null;
                available.signal();
            }
            return true;
        } finally {
            lock.unlock();
        }
    }

然后我们来看下出队方法take(),take方法是获取并移除队列里面延迟时间过期的元素,如果队列里没有过期元素则等待。

    public E take() throws InterruptedException {
    	//获取独占锁
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            for (;;) {
            	//获取首元素
                E first = q.peek();
                //如果为空,则一直等待
                if (first == null)
                    available.await();
                //如果不为空
                else {
                	//查看该元素还有多久过期
                    long delay = first.getDelay(NANOSECONDS);
                    //小于0说明队列头部元素已过期,直接出队返回
                    if (delay <= 0)
                        return q.poll();
                    //如果未过期,把first置为null,防止一直循环
                    first = null; // don't retain ref while waiting
                    //判断leader是否为空,如果不为空说明其他线程正在执行take方法,则阻塞当前线程
                    if (leader != null)
                        available.await();
                    //如果leader为空
                    else {
                    	//将当前线程设置为leader线程
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;
                        //然后使当前线程等待delay时间,Condition条件在阻塞时期间会释放锁
                        try {
                            available.awaitNanos(delay);
                        //等待delay时间后,该线程重新获取锁,使leader=null,重新执行循环,如果队头元素过期则会返回元素
                        } finally {
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        //返回前会执行该代码,如果判断为true,则说明当前线程元素出队后,又有其他线程执行了入队操作,唤醒等待线程
        } finally {
            if (leader == null && q.peek() != null)
                available.signal();
            lock.unlock();
        }
    }

我们来看下poll方法,poll方法是获取并移除队头过期元素,如果没有过期元素则返回null。

    public E poll() {
    	//获取独占锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            E first = q.peek();
            //如果队列没有元素,或者队头元素未过去,则返回空
            if (first == null || first.getDelay(NANOSECONDS) > 0)
                return null;
            else
                return q.poll();
        } finally {
            lock.unlock();
        }
    }

最后我们来看下size方法

    public int size() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return q.size();
        } finally {
            lock.unlock();
        }
    }

使用案例

public class DelayQueueDemo {

    static class DelayedEle implements Delayed{

        private final Long expireTime;
        private String taskName;

        public DelayedEle(Long expireTime,String taskName){
            this.expireTime=expireTime+System.currentTimeMillis();
            this.taskName=taskName;
        }

        @Override
        public long getDelay(TimeUnit unit) {
            return unit.convert(this.expireTime-System.currentTimeMillis(),TimeUnit.MILLISECONDS);
        }

        @Override
        public int compareTo(Delayed o) {
            return (int)(getDelay(TimeUnit.MILLISECONDS)-o.getDelay(TimeUnit.MILLISECONDS));
        }

        @Override
        public String toString() {
            return "taskName:"+taskName+",expireTime:"+expireTime;
        }
    }

    public static void main(String[] args) {
        DelayQueue<DelayedEle> delayedEles=new DelayQueue<>();
        Random random=new Random();
        for (int i=0;i<10;i++){
            delayedEles.add(new DelayedEle((long)random.nextInt(1000),"task"+i));
        }

        DelayedEle ele=null;
        try{
            for (; ; ) {
                while ((ele=delayedEles.take()) != null) {
                    System.out.println(ele.toString());
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

SynchronousQueue

SynchronousQueue是一个非常特别的阻塞队列,它的内部并不存储元素。每一个put操作必须等待take操作,如果put操作后没有线程执行take,那么put操作就会阻塞,当take操作执行后会重新唤醒put操作的线程。SynchronousQueue就像是一个传球手,本身不存储任何元素,负责把生产者产生的数据传递给消费者。SynchronousQueue的吞吐量优于LinkedBlockingQueue和ArrayBlockingQueue。

实际运用中能用到的地方很少,在线程池中有使用,所以这里就不再细讲,贴上两篇非常棒的博客。

SynchronousQueue源码分析

SynchronousQueue源码

SynchronousQueue1.8源码分析

JAVA并发包中还有两种阻塞队列LinkedTransferQueue和LinkedBlockingDeque,有兴趣的可以下去了解。

参考书籍:

《JAVA并发编程之美》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值