本文的主要内容是对jdk并发包中ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue类的代码解析,主要对比他们的不同。
前面两个类都是生产者消费者模型的实现,性能都不错,这毕竟都是大师的杰作。先来认识下ArrayBlockingQueue,这个类的内部数据结构是一个Object数组,通过ReentrantLock控制同步操作,由这个锁派生出两个条件对象,如下代码
public ArrayBlockingQueue(int capacity, boolean fair) { if (capacity <= 0) throw new IllegalArgumentException(); this.items = (E[]) new Object[capacity]; lock = new ReentrantLock(fair); notEmpty = lock.newCondition(); notFull = lock.newCondition(); }
这是非常经典的条件操作的应用。下面看下put方法的实现
public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); final E[] items = this.items; final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { try { while (count == items.length) notFull.await(); } catch (InterruptedException ie) { notFull.signal(); // propagate to non-interrupted thread throw ie; } insert(e); } finally { lock.unlock(); } } private void insert(E x) { items[putIndex] = x; putIndex = inc(putIndex); ++count; notEmpty.signal(); }
它的思路是:如果队列满,等待,否则入队操作,并发出队列不空的信号。
以上的思路似乎很合理,但是仔细想想还可以改进:只要在队列为空的时候,入队成功才需发信号说队列不空了;还有一点就是,入队成功后,如果队列依然不满,可以唤醒正在等待的入队操作。这两点都是可以考虑的。
以上提到的在LinkedBlockingQueue中得到了实现,它的内部数据结构是一个单向链表,通过两个ReentrantLock来控制同步操作,下面是put方法的实现
public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); // Note: convention in all put/take/etc is to preset // local var holding count negative to indicate failure unless set. int c = -1; 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. */ try { while (count.get() == capacity) notFull.await(); } catch (InterruptedException ie) { notFull.signal(); // propagate to a non-interrupted thread throw ie; } insert(e); c = count.getAndIncrement(); if (c + 1 < capacity) // 标记1 notFull.signal(); } finally { putLock.unlock(); } if (c == 0) // 标记2 signalNotEmpty(); } private void insert(E x) { last = last.next = new Node<E>(x); }
上述代码中的两个标记处就实现了前面提到的两个不足。
take()方法的对比类似。
下面再来说说ConcurrentLinkedQueue,这个类和上面两个有本质的不同,它的内部采用的是非阻塞算法实现,性能比前两个都要好(书上说的,用机会测试下),这个类中采用的原子类是域更新器,看下代码更清楚
private static final AtomicReferenceFieldUpdater<ConcurrentLinkedQueue, Node> tailUpdater = AtomicReferenceFieldUpdater.newUpdater (ConcurrentLinkedQueue.class, Node.class, "tail"); private static final AtomicReferenceFieldUpdater<ConcurrentLinkedQueue, Node> headUpdater = AtomicReferenceFieldUpdater.newUpdater (ConcurrentLinkedQueue.class, Node.class, "head");
Node节点中还有两个
private static class Node<E> { private volatile E item; private volatile Node<E> next; private static final AtomicReferenceFieldUpdater<Node, Node> nextUpdater = AtomicReferenceFieldUpdater.newUpdater (Node.class, Node.class, "next"); private static final AtomicReferenceFieldUpdater<Node, Object> itemUpdater = AtomicReferenceFieldUpdater.newUpdater (Node.class, Object.class, "item"); Node(E x) { item = x; } Node(E x, Node<E> n) { item = x; next = n; }
分别是用来更新队列的头结点、尾节点以及每个节点的item和next。
下面看下它的代码
public boolean offer(E e) { if (e == null) throw new NullPointerException(); Node<E> n = new Node<E>(e, null); for (;;) { Node<E> t = tail; Node<E> s = t.getNext(); if (t == tail) { if (s == null) { if (t.casNext(s, n)) { casTail(t, n); return true; } } else { casTail(t, s);// 标记1 } } } } public E poll() { for (;;) { Node<E> h = head; Node<E> t = tail; Node<E> first = h.getNext(); if (h == head) { if (h == t) { if (first == null) return null; else casTail(t, first);// 标记2 } else if (casHead(h, first)) { E item = first.getItem(); if (item != null) { first.setItem(null); return item; } // else skip over deleted item, continue loop, } } } }
代码中的关键问题就是操作的原子性:单个原子操作当然是原子性的,但两个原子操作合起来就不是原子的了。比如在入队操作中要加入节点n,在更新tail节点的next成功后,tail节点指向n不一定会成功;这就需要在其他的操作中弥补这一“过错”,这在offer和poll中都有考虑的,如上述代码中的标记1和标记2。