Java并发包学习LinkedBlockingQueue

一。概述

LinkedBlockingQueue是cocurrent包下的一个线程安全的阻塞队列,与ArrayBlockingQueue比,LinkedBlockingQueue队列用链表实现,通常用于生产者、消费者模型。

二。LinkedBlockingQueue的数据结构

1. 链表节点

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

2. 链表指针

LinkedBlockingQueue链表提供了头指针和尾指针。
1)头指针:用来管理队列出队,如take(), poll(), peek()
2)尾指针:用来管理队列入队,如put(), offer()

private transient Node head; /* 头结点, 头节点不保存数据信息 /
private transient Node last; /
尾节点, 尾节点保存最新入队的数据信息 */

3. 链表容量的大小

LinkedBlockingQueue是有大小限制的,如果队列已经满了,则无法再向其中添加元素。

private final int capacity; /* 队列容量一般使用中,构造LinkedBlockingQueue时,需要传入当前队列大小,如果不传入,默认是Integer.MAX_VALUE */
private final AtomicInteger count = new AtomicInteger(0); // 队列当前大小

这里的count是AtomicInteger类型,而不是int类型,因为LinkedBlockingQueue的读和写是通过两把锁来控制并发操作。如果用int类型,会有线程安全问题。

4. 控制并发的lock和condition

LinkedBlockingQueue中,读和写操作分别由两把锁控制,两把锁分别管理head节点和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(); /
写锁对应的条件 */

三。关键代码解读

1. 入队的public方法

入队的方法有两种:一种阻塞的方式,一种是非阻塞的方式。
1)put(): 阻塞算法,直到队列有空余时,才能为队列加入新元素。
2)offer():非阻塞算法,如果队列已满,立即返回或等待一会再返回,通过返回值true或false,标记本次入队操作是否成功。

put的源码:

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();     
    int c = -1;                         
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
        while (count.get() == capacity) {      
                notFull.await();
        }
        enqueue(e);
        c = count.getAndIncrement();
        if (c + 1 < capacity)
            notFull.signal();        /* 如果完成当前入队操作后,队列依然有剩余的空间,那么再唤醒另一个等待入队的线程 */
    } finally {
        putLock.unlock();
    }
    if (c == 0)                        /* 如果入队前,队列大小为空,那么唤醒一个等待出队的线程 */
        signalNotEmpty();        
}

offer的源码:

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

2. 出队的public方法

出队与入队的方法类似,也分为阻塞与非阻塞方法。其中:
1)take()为阻塞算法,直到有数据可取时,在取出数据,否则阻塞等待。
2)poll()为非阻塞算法,如果队列为空,立即返回或等待一会再返回,通过返回值true或false来标记是否取到数据。
3)peek():只返回队列中的第一个元素,既不出队,也不阻塞,如果没有元素,就返回null.

take方法源代码:

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

3. 线程安全的迭代器

LinkedBlockingQueue的迭代器Itr,是线程安全的,在获取元素之前,会对上述读锁和写锁同时加锁,为了防止死锁,先加写锁,再加读锁。

void fullyLock() {

    putLock.lock();       // 先加写锁
    takeLock.lock();     // 再加读锁
}

void fullyUnlock() {
    takeLock.unlock();  /* 先解锁读锁 */
    putLock.unlock();   /* 再解锁写锁 */
}

迭代器的源码:

private class Itr implements Iterator<E> {
    /*
     * Basic weakly-consistent iterator.  At all times hold the next
     * item to hand out so that if hasNext() reports true, we will
     * still have it to return even if lost race with a take etc.
     */

    private Node<E> current;
    private Node<E> lastRet;
    private E currentElement;

    Itr() {
        fullyLock();
        try {
            current = head.next;
            if (current != null)
                currentElement = current.item;
        } finally {
            fullyUnlock();
        }
    }

    public boolean hasNext() {
        return current != null;
    }

    /**
     * Returns the next live successor of p, or null if no such.
     *
     * Unlike other traversal methods, iterators need to handle both:
     * - dequeued nodes (p.next == p)
     * - (possibly multiple) interior removed nodes (p.item == null)
     */
    private Node<E> nextNode(Node<E> p) {
        for (;;) {
            Node<E> s = p.next;
            if (s == p)
                return head.next;
            if (s == null || s.item != null)
                return s;
            p = s;
        }
    }

    public E next() {
        fullyLock();
        try {
            if (current == null)
                throw new NoSuchElementException();
            E x = currentElement;
            lastRet = current;
            current = nextNode(current);
            currentElement = (current == null) ? null : current.item;
            return x;
        } finally {
            fullyUnlock();
        }
    }

    public void remove() {
        if (lastRet == null)
            throw new IllegalStateException();
        fullyLock();
        try {
            Node<E> node = lastRet;
            lastRet = null;
            for (Node<E> trail = head, p = trail.next;
                 p != null;
                 trail = p, p = p.next) {
                if (p == node) {
                    unlink(p, trail);
                    break;
                }
            }
        } finally {
            fullyUnlock();
        }
    }
}

可以看到,迭代器中保存了一下内容:

private Node<E> current;     /* 迭代器的下一个位置 */
private Node<E> lastRet;     /* 当前迭代器的位置 */
private E currentElement;    /* 当前需要返回的元素内容 */

如果我们自己实现这个迭代器,会觉得只要一个指向当前位置的指针就行了,何必那么麻烦呢?JDK的开发人员这样设计,是为了应付多线程的问题:

1)首先,通过currrentElement保存了当前需要返回的内容,这样可以保证在当前节点被其他线程删除的情况下,迭代器的next()方法仍能返回当前指向的内容。

2)在迭代器中,如果调用了remove()方法,删除了当前对象,那么,lastRet就派上用场了。可以通过再次遍历列表,找到需要删除的对象,并将其删除,同事为了防止remove()方法被调用两次,在删除时,会将lastRet设置为null, 这样当一个线程在执行删除时,其他线程不会再次执行删除了。

3)current保存了迭代器的下一个指向的位置,调用hasNext()时,可以立即知道是否还有空余的对象,更重要的是,如果在迭代器创建后,其他线程多次调用了出队的方法,可能导致lastRet和current都变成悬挂的指针了,这时,需要判断current的next是否为自己,就可以知道自己是否已经被出队,是否需要重定位current的位置。

  1. 对锁的精巧使用
    LinkedBlockingQueue将读和写分离,可以让读写操作在互不干扰的情况下,完成各自的功能,提高并发的吞吐量。因为,LinkedBlockingQueue的读写分别在头尾进行,所以可以做到互不干涉。

有空需要自己仔细琢磨自己实现一个ConcurrentHashMap和LinkedBlockingQueue,里面有很多值得学习的多线程的底层知识。

猜你喜欢

转载自blog.csdn.net/shijinghan1126/article/details/86500816