JAVA并发包解析(四)—— Queue

127 阅读7分钟

前言

这篇文章介绍并发包内不同的Queue实现方法,Queue是常用的数据结构之一,并发包对Queue进行了不同的实现。

正文

ArrayBlockingQueue

ArrayBlockingQueue是用数组实现的同步队列,构造时指定数组大小,以循环的方式写入和取出,过程中不涉及到底层数组的变化扩容,看一下属性列表:

    final Object[] items;

    int takeIndex;

    int putIndex;

    int count;

    final ReentrantLock lock;

    private final Condition notEmpty;

    private final Condition notFull;

以两个指针来表示写入和添加索引,用锁和条件变量来替代synchronized和线程的阻塞唤醒,那么实现方法就很容易就写出来了

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

public boolean offer(E e) {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }

public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }

看一下put、offer和add方法,都是使用了ReentrantLock加锁,然后判断队列是否已满,区别就在于put是一个阻塞方法,当队列满的时候被条件变量阻塞自旋,而offer和add不进行阻塞,add调用了offer,当失败的时候抛出异常。

private void enqueue(E x) {
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal();
    }

实际的添加节点逻辑,将putIndex自增且到了尾部则重新回到头部,并且通知因为队列长度为0而获取不到被阻塞的线程。

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

private E dequeue() {
        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;
    }

take方法正好反过来,当队列为空的时候阻塞,同时对应的poll方法则是非阻塞,当获取不到返回null,实现原理简单就不放上来了。

++ArrayBlockingQueue利用了固定长度数组作为存储,ReentrantLock作为锁实现起来很简单,由于是同步锁实现,不太适用于高并发场景。++

LinkedBlockingQueue

LinkedBlockingQueue使用的链表结构,同样也是ReentrantLock来作为同步器

static class Node<E> {
        E item;
        Node<E> next;

        Node(E x) { item = x; }
    }
    //初始容量
    private final int capacity;

    private final AtomicInteger count = new AtomicInteger();


    transient Node<E> head;

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

Node内部没有复杂的关系,只保留了实际元素和下一个node,下面只看阻塞的方法,非阻塞方法实现和阻塞差不多。

public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

这是一个构造方法,用于指定队列长度,从这里可以看出head节点初始化后是一个空节点

public void put(E e) throws InterruptedException {
     
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            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;
    }

直接在last后添加一个节点然后把自己成为last....put方法和传统queue无异,在边界(0和最大)处进行了判断去通知条件变量

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();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

take主要看看dequeue方法,其他和put只是反过来

private E dequeue() {
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h;
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }

从这里可以看出来,take方法删除了head节点,但是返回的却是head.next的值,结合构造方法初始化head为空,可以了解LinkedBlockingQueue使用两把锁的原因:使用了一个虚拟节点来延迟删除实际的数据节点,好处是put和take方法可以同时进行。

++LinkedBlockingQueue实现了读写分离,提高了效率,但是需要注意的是默认容量是Integer.MAX_VALUE,需要自行调整不然会造成内存飙升++

LinkedTransferQueue

LinkedTransferQueue是一个由链表结构组成的无界阻塞队列。于上文介绍的阻塞队列比较,LinkedTransferQueue多了tryTransfer和transfer方法。

LinkedTransferQueue使用了预占模式,当一个线程想要获取数据的时候,如果没有可以获取的节点就用一个null节点来添加队列的尾部并阻塞自己,如果有一个线程添加节点,发现了尾节点是一个空节点就将值设置进去并唤醒线程。

static final class Node {
	//消费节点为false
        final boolean isData;  
        volatile Object item;
        volatile Node next;
	//消费者线程
        volatile Thread waiter;
	//使用Unsafe CAS的方式来替换属性
        final boolean casNext(Node cmp, Node val) {
            return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
        }

        final boolean casItem(Object cmp, Object val) {
       		return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
        }

      
        Node(Object item, boolean isData) {
            UNSAFE.putObject(this, itemOffset, item); // relaxed write
            this.isData = isData;
        }

        //将next指向自己
        final void forgetNext() {
            UNSAFE.putObject(this, nextOffset, this);
        }

        //将线程设置为空和next指向自己,帮助GC
        final void forgetContents() {
            UNSAFE.putObject(this, itemOffset, this);
            UNSAFE.putObject(this, waiterOffset, null);
        }

        //节点是否被匹配过了
        final boolean isMatched() {
            Object x = item;
            return (x == this) || ((x == null) == isData);
        }

        //节点是否被匹配过
        final boolean isUnmatchedRequest() {
            return !isData && item == null;
        }

        // 如果给定节点不能连接在当前节点后则返回true
        final boolean cannotPrecede(boolean haveData) {
            boolean d = isData;
            Object x;
            return d != haveData && (x = item) != this && (x != null) == d;
        }

        //尝试匹配一个有数据的节点
        final boolean tryMatchData() {
            // assert isData;
            Object x = item;
            if (x != null && x != this && casItem(x, null)) {
                LockSupport.unpark(waiter);
                return true;
            }
            return false;
        }
    }

几个重要的参数

// 作为第一个等待节点在阻塞之前的自旋次数
private static final int FRONT_SPINS   = 1 << 7;
 
// 前驱节点正在处理,当前节点在阻塞之前的自旋次数
private static final int CHAINED_SPINS = FRONT_SPINS >>> 1;
 
// sweepVotes的阈值
static final int SWEEP_THRESHOLD = 32;


// 断开被删除节点失败的次数
private transient volatile int sweepVotes;

public void put(E e) {
        xfer(e, true, ASYNC, 0);
    }

put方法调用了xfer,true表示这是数据节点

private E xfer(E e, boolean haveData, int how, long nanos) {
	//首先判断数据节点的数据不能为空
        if (haveData && (e == null))
            throw new NullPointerException();
        Node s = null;                        
retry:
        for (;;) {                           
	    //从head开始循环找未数据节点
            for (Node h = head, p = h; p != null;) { 
                boolean isData = p.isData;
                Object item = p.item;
                if (item != p && (item != null) == isData) { 
		    //是一个数据节点跳过
                    if (isData == haveData)
                        break;
		    //尝试设置数据
                    if (p.casItem(item, e)) {
			//标注1
                        for (Node q = p; q != h;) {
                            Node n = q.next;  
                            if (head == h && casHead(h, n == null ? q : n)) {
                                h.forgetNext();
                                break;
                            }                 
                            if ((h = head)   == null ||
                                (q = h.next) == null || !q.isMatched())
                                break;     
                        }
			//设置完数据后唤醒这个线程进行消费
                        LockSupport.unpark(p.waiter);
                        return LinkedTransferQueue.<E>cast(item);
                    }
                }
		//标注2
                Node n = p.next;
                p = (p != n) ? n : (h = head);
            }
	    //标注3
            if (how != NOW) {                 
                if (s == null)
                    s = new Node(e, haveData);
                Node pred = tryAppend(s, haveData);
                if (pred == null)
                    continue retry;          
                if (how != ASYNC)
                    return awaitMatch(s, pred, e, (how == TIMED), nanos);
            }
            return e;
        }

标注1:当找到一个节点是一个消费者节点,将值设置成功后,需要将head变为这个节点的后面一个节点,因为只有在链表为空的情况下才会添加消费者节点,当完成一个设置完一个消费者节点后就断开后面的连接并设置head,有助于GC回收这个节点

标注2:继续寻找下一个节点,如果这个过程中被其他线程给填充了数据了,则又从head开始寻找

标注3:如果没有找到一个消费节点,说明还没有消费者,这个时候根据how的值来控制逻辑:

  • NOW 不插入节点直接返回,必须得有消费者才能成功插入
  • SYNC 插入一个节点,在被取消或者匹配之前一直不返回
  • ASYNC 插入后立即返回
  • TIMED SYNC的超时模式
private Node tryAppend(Node s, boolean haveData) {
        for (Node t = tail, p = t;;) {        
            Node n, u;      
	    //如果head为空直接设置head                  
            if (p == null && (p = head) == null) {
                if (casHead(null, s))
                    return s;                
            }
            else if (p.cannotPrecede(haveData))
                return null;     
            //如果这个节点不是最后一个(说明中途被加入了)             
            else if ((n = p.next) != null)    
                p = p != t && t != (u = tail) ? (t = u) : 
                    (p != n) ? n : null;
	    //CAS next失败 继续下一个节点      
            else if (!p.casNext(null, s))
                p = p.next;                   
            else {
		//不断设置tail 失败了则重新设置tail继续
                if (p != t) {                
                    while ((tail != t || !casTail(t, s)) &&
                           (t = tail)   != null &&
                           (s = t.next) != null && // advance and retry
                           (s = s.next) != null && s != t);
                }
                return p;
            }
        }
    }

tryAppend两种模式对应了消费模式和添加模式,采用自旋方式将自身设置为tail


private E awaitMatch(Node s, Node pred, E e, boolean timed, long nanos) {
    // 超时时间
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    Thread w = Thread.currentThread();
    // 自旋次数
    int spins = -1;
    // 随机数
    ThreadLocalRandom randomYields = null;
 
    for (;;) {
        Object item = s.item;
        // 消费线程匹配了该节点
        if (item != e) {                  
            s.forgetContents();
            return LinkedTransferQueue.<E>cast(item);
        }
        // 线程中断或者超时,则将s的节点item设置为s
        if ((w.isInterrupted() || (timed && nanos <= 0)) &&
                s.casItem(e, s)) {     
            // 断开节点
            unsplice(pred, s);
            return e;
        }
 
        if (spins < 0) {                 
            // 计算自旋次数
            if ((spins = spinsFor(pred, s.isData)) > 0)
                randomYields = ThreadLocalRandom.current();
        }
        else if (spins > 0) {             // spin
            --spins;
            // 生成随机数来让出CPU时间
            if (randomYields.nextInt(CHAINED_SPINS) == 0)
                Thread.yield();
        }
        // 将s的waiter设置为当前线程
        else if (s.waiter == null) {
            s.waiter = w;                
        }
        // 带超时时间的阻塞
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos > 0L)
                LockSupport.parkNanos(this, nanos);
        }
        // 非超时阻塞
        else {
            LockSupport.park(this);
        }
    }
}

等待这个节点被匹配,对应了take方法的逻辑

++节点的的take和put都是xfer方法,根据now传递的参数不同有不同的行为。++

结尾

本文介绍了各种同步的Queue实现方法,根据需求来使用。