在Java多线程应用中,队列的使用率很高,多数生产消费模型的首选数据结构就是队列。Java提供的线程安全的Queue可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是BlockingQueue,非阻塞队列的典型例子是ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。
注:什么叫 线程安全 ?这个首先要明确。 线程安全的类 ,指的是 类内共享的全局变量的访问必须保证是 不受多线程形式影响的 。如果由于多线程的访问(比如修改、遍历、查看)而使这些变量结构被破坏或者针对这些变量操作的原子性被破坏,则这个类就不是线程安全的。可能报异常 | 返回布尔值 | 可能阻塞 | 设定等待时间 | |
入队 | add(e) | offer(e) | put(e) | offer(e, timeout, unit) |
出队 | remove() | poll() | take() | poll(timeout, unit) |
查看 | element() | peek() | 无 | 无 |
public boolean add(E e) { if (offer(e)) return true; else throw new IllegalStateException("Queue full");//队列已满,抛异常 } public E remove() { E x = poll(); if (x != null) return x; else throw new NoSuchElementException();//队列为空,抛异常 }对于第二类方法,很标准的ReentrantLock使用方式(不熟悉的朋友看一下我上一篇帖http://hellosure.iteye.com/blog/1121157),另外对于insert和extract的实现没啥好说的。
public boolean offer(E e) { if (e == null) throw new NullPointerException(); final ReentrantLock lock = this.lock; lock.lock(); try { if (count == items.length)//队列已满,返回false return false; else { insert(e);//insert方法中发出了notEmpty.signal(); return true; } } finally { lock.unlock(); } } public E poll() { final ReentrantLock lock = this.lock; lock.lock(); try { if (count == 0)//队列为空,返回false return null; E x = extract();//extract方法中发出了notFull.signal(); return x; } finally { lock.unlock(); } }
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这个条件,这时当前线程被阻塞 notFull.await(); } catch (InterruptedException ie) { notFull.signal(); //唤醒受notFull阻塞的当前线程 throw ie; } insert(e); } finally { lock.unlock(); } } public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { try { while (count == 0)//如果队列为空,等待notEmpty这个条件,这时当前线程被阻塞 notEmpty.await(); } catch (InterruptedException ie) { notEmpty.signal();//唤醒受notEmpty阻塞的当前线程 throw ie; } E x = extract(); return x; } finally { lock.unlock(); } }
static class Node<E> { /** The item, volatile to ensure barrier separating write and read */ volatile E item; Node<E> next; Node(E x) { item = x; } }然后,对于链表来说,肯定需要两个变量来标示头和尾:
/** 头指针 */ private transient Node<E> head; //head.next是队列的头元素 /** 尾指针 */ private transient Node<E> last; //last.next是null那么,对于入队和出队就很自然能理解了:
private void enqueue(E x) { last = last.next = new Node<E>(x); //入队是为last再找个下家 } private E dequeue() { Node<E> first = head.next; //出队是把head.next取出来,然后将head向后移一位 head = first; E x = first.item; first.item = null; return x; }
private final AtomicInteger count = new AtomicInteger(0); /** 用于读取的独占锁*/ private final ReentrantLock takeLock = new ReentrantLock(); /** 队列是否为空的条件 */ private final Condition notEmpty = takeLock.newCondition(); /** 用于写入的独占锁 */ private final ReentrantLock putLock = new ReentrantLock(); /** 队列是否已满的条件 */ private final Condition notFull = putLock.newCondition();
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; final ReentrantLock putLock = this.putLock;//入队当然用putLock putLock.lock(); try { if (count.get() < capacity) { enqueue(e); //入队 c = count.getAndIncrement(); //队长度+1 if (c + 1 < capacity) notFull.signal(); //队列没满,当然可以解锁了 } } finally { putLock.unlock(); } if (c == 0) signalNotEmpty();//这个方法里发出了notEmpty.signal(); return c >= 0; } public E poll() { final AtomicInteger count = this.count; if (count.get() == 0) return null; E x = null; int c = -1; final ReentrantLock takeLock = this.takeLock;出队当然用takeLock takeLock.lock(); try { if (count.get() > 0) { x = dequeue();//出队 c = count.getAndDecrement();//队长度-1 if (c > 1) notEmpty.signal();//队列没空,解锁 } } finally { takeLock.unlock(); } if (c == capacity) signalNotFull();//这个方法里发出了notFull.signal(); return x; }看看源代码发现和上面ArrayBlockingQueue的很类似,关键的问题在于:为什么要用两个ReentrantLockputLock和takeLock?
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) { //------------------------------a if (s == null) { //---------------------------b if (t.casNext(s, n)) { //-------------------c casTail(t, n); //------------------------d return true; } } else { casTail(t, s); //----------------------------e } } } }此方法的循环内首先获得尾指针和其next指向的对象,由于tail和Node的next均是volatile的,所以保证了获得的分别都是最新的值。
if(!queue.isEmpty()) { queue.poll(obj); }我们很难保证,在调用了isEmpty()之后,poll()之前,这个queue没有被其他线程修改 。所以对于这种情况,我们还是 需要自己同步:
synchronized(queue) { if(!queue.isEmpty()) { queue.poll(obj); } }
public final class Counter { private long value = 0; public synchronized long getValue() { return value; } public synchronized long increment() { return ++value; } }下面的代码显示了一种 最简单的非阻塞算法 :使用 AtomicInteger的compareAndSet()( CAS方法 )的计数器。 compareAndSet() 方法规定“ 将这个变量更新为新值,但是如果从我上次看到这个变量之后其他线程修改了它的值,那么更新就失败 ”
public class NonblockingCounter { private AtomicInteger value;//前面提到过,AtomicInteger类是以原子的方式操作整型变量。 public int getValue() { return value.get(); } public int increment() { int v; do { v = value.get(); while (!value.compareAndSet(v, v + 1)); return v + 1; } }非阻塞版本相对于基于锁的版本有几个性能优势。首先,它用硬件的原生形态代替 JVM 的锁定代码路径,从而在更细的粒度层次上(独立的内存位置)进行同步,失败的线程也可以立即重试,而不会被挂起后重新调度。更细的粒度降低了争用的机会,不用重新调度就能重试的能力也降低了争用的成本。即使有少量失败的 CAS 操作,这种方法仍然会比由于锁争用造成的重新调度快得多。
public class LinkedQueue <E> { private static class Node <E> { final E item; final AtomicReference<Node<E>> next; Node(E item, Node<E> next) { this.item = item; this.next = new AtomicReference<Node<E>>(next); } } private AtomicReference<Node<E>> head = new AtomicReference<Node<E>>(new Node<E>(null, null)); private AtomicReference<Node<E>> tail = head; public boolean put(E item) { Node<E> newNode = new Node<E>(item, null); while (true) { Node<E> curTail = tail.get(); Node<E> residue = curTail.next.get(); if (curTail == tail.get()) { if (residue == null) /* A */ { if (curTail.next.compareAndSet(null, newNode)) /* C */ { tail.compareAndSet(curTail, newNode) /* D */ ; return true; } } else { tail.compareAndSet(curTail, residue) /* B */; } } } } }看看这代码完全就是ConcurrentLinkedQueue 源码啊。