学习记录 - 并发阻塞队列 - LinkedBlockingQueue

先给出它的UML图
在这里插入图片描述
从上图中,可以看出,实现了BlockingQueue接口,其次扩展了AbstractQueue接口,最后还实现Serializable接口,这是个标志性接口,使得队列具有序列化的功能,其实主要就是可以网络传输(网络IO),因为跨越JVM了,传输需要转为字节流。


LinkedBlockingQueue

从名字可以知道,这个队列是以链表的方式存储队列的元素,链表么其实就是火车呀,一节节车厢就是一个个的节点呀。那首先看一下链表节点:

    static class Node<E> {
        /**
		 * 节点元素内容
		*/
        E item;

        /**
         * 指向后继节点的引用(也可以说是指针)
         */
        Node<E> next;

		/**
 		 * 构造函数
		*/
        Node(E x) { item = x; }
    }

这是一个静态的内部类,为什么内部类么是因为主要就是它自己用呀,当然这里访问权限默认为protected,它同包下的其他类硬要用也是可以的吧

下面再来看下,成员变量:

 /** 队列长度,默认为 Integer.MAX_VALUE */
    private final int capacity;

    /** 
     * 队列内的元素
     * 这里使用了JUC包下的原子整型保证了count修改的线程安全性,具体是通过CAS实现。 
     */
    private final AtomicInteger count = new AtomicInteger();

    /**
     * 表头指针
     * Invariant: head.item == null
     * 可以看到transient修饰,这个字段不会被序列化
     */
    transient Node<E> head;

    /**
     * 表尾指针
     * Invariant: last.next == null
     * 可以看到transient修饰,这个字段不会被序列化
     */
    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();

小结:
从成员变量看,阻塞队列就是从队列中去元素和放元素需要做同步处理的队列。其实我们学的生产者-消费者问题就能用这个解决,可以看成N个消费者(取元素的线程),M个生产者(放元素的线程)。

下面再看下成员方法:

  1. 首先当然是看下构造方法:
/**
 * 默认构造(无参构造函数)
 * 设置队列容量为Integer.MAX_VALUE,可以视为一个无界队列。
 * 调用了另外一个构造函数
*/
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

/**
 * 用户指定容量的构造函数
 * 创建的容量必须是一个正整数,否则报参数异常
 * 可以看到,这里创建了一个内容为空的节点,这个设计还是比较好的,这样一来
 就不用每次插入都去判断是不是空,直接插入队列尾部,然后移动队尾指针就好。
 过会看它的enqueue方法就能看出来。同样删除元素也很方便,见dequeue方法。
*/
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        //创建一个内容为空的头结点,头,尾都指向这个空节点
        last = head = new Node<E>(null);
    }

/**
 * 这个方法是用一个已知的集合来创建队列
 * 首先创建一个无界队列
*/
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();
        }
    }
  1. 队列元素插入删除的实际操作

插入

/**
 * 由于有头节点的存在,不需要判断队列是否为空(因为空的时候,队列里有一个内容为空的头结点,队尾
 指针指向这个内容为空的节点)
 * 新增一个元素,只需要直接把它插到队尾,然后调整队尾指针即可
*/
    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }

删除

/**
 * head指向的是内容为空的头节点,每次实际要删除的是头节点的next,然后这个被删节点作为
 新的头结点,如下图:
 
       head									last
   Node(null)->  Node(A)  ->  Node(B)  -> Node(C)   

null 是指Node的item为null,不是说节点为null
取元素就是把A返回,把head移动到Node(A)的位置,然后Node(A)的item设为null
      old head           head						last
     Node(null)		  Node(null)  ->  Node(B)  -> Node(C)

*/
    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;
    }
  1. 阻塞队列的同步队列操作
/**
 这个方法在往队列里放置元素的时候,如果队列满了就会阻塞等待至队列为不满
*/
    public void put(E e) throws InterruptedException {
    	//添加空元素抛空指针异常
        if (e == null) throw new NullPointerException();
        final int c;
        //要添加的节点
        final Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        //抢锁来锁定添加元素
        putLock.lockInterruptibly();
        try {
            /*
             * 这里有一段解释为什么count修改不用锁保护
             * 首先,count是原子类型的,它的修改是线程安全的,即多个线程去操作不论加还是减
             最终结果肯定是对的。所以放和拿是可以同时执行的。
             * 其次,在这里其他能够放置而修改count的线程要么就是拿不到锁进不来,要么就是在等待notFull
             从而不会修改count
             * 还有就是,这里只要判断能不能放,保证c + 1 < capacity,c其实具体是几不重要,也就是取的线程不					    
             会干扰放置这个动作
             */

			//只要队列满了,就死等,因为这个时候放置的入口已经卡死,只能是减
            while (count.get() == capacity) {
                notFull.await();
            }
            //加入队列
            enqueue(node);
            //获取队列元素数量,之后可能会变,因为这个时候还是可以取出元素的
            //这里是先获取当前情况,再自增1
            c = count.getAndIncrement();
            //判断还能放元素
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        /**
         * 这次put之前,队列为空,说明可能有后续的取元素的线程在等待
         * 这里唤醒操作还上了锁,大概是为了只唤醒一个等待取元素的线程,防止多次唤醒同一线程      
        */
        if (c == 0)
            signalNotEmpty();
    }
/**
 * 这个方法与put相似,只是多了一个等待延迟
*/
public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {

        if (e == null) throw new NullPointerException();
        long nanos = unit.toNanos(timeout);
        final int c;
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
        	
            while (count.get() == capacity) {
                //检查等待时间有没有过,每次被唤醒都要检查,超时返回false表示插入失败
                if (nanos <= 0L)
                    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;
    }
/**
 * 从队列头部取元素,队列为空的时候,会阻塞等待至有元素
*/
public E take() throws InterruptedException {
        final E x;
        final int c;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        //上锁
        takeLock.lockInterruptibly();
        try {
        	/**死等队列有元素,和put一样,只会有一个取的线程能成判断往下走
        	,所以这里count只能是增加*/
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();
            //当前时刻的元素数量并对其减1
            c = count.getAndDecrement();
            //队列还有元素,就唤醒取的线程
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        //如果取的时候队列是满的,说明可能有放的线程在等,将其唤醒
        //这里的唤醒操作还上了锁,大概是为了同一时刻只唤醒一个放置的线程
        if (c == capacity)
            signalNotFull();
        return x;
    }
/**
 * 比take多了一个阻塞等待的延迟
*/
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        final E x;
        final int c;
        long nanos = unit.toNanos(timeout);
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                if (nanos <= 0L)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }
/**
 * 从队列中删除某个元素,这个操作需要锁定放和取
*/
public boolean remove(Object o) {
        if (o == null) return false;
        //放和取都锁定
        fullyLock();
        try {
        	//用两个指针遍历删除元素,每次判断后一个指针是否为空或者该删的元素
            for (Node<E> pred = head, p = pred.next;
                 p != null;
                 pred = p, p = p.next) {
                if (o.equals(p.item)) {
                    unlink(p, pred);
                    return true;
                }
            }
            return false;
        } finally {
            fullyUnlock();
        }
    }
/**
 * 判断是否包含某个元素,也是全球锁定,不让放,不然取
*/
    public boolean contains(Object o) {
        if (o == null) return false;
        //全球锁定
        fullyLock();
        try {
            for (Node<E> p = head.next; p != null; p = p.next)
                if (o.equals(p.item))
                    return true;
            return false;
        } finally {
            fullyUnlock();
        }
    }
  1. 非阻塞操作
/**
 * 队列满了,直接返回false,其他操作和put相似
*/
public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        //先快速检查一下,是不是满,防止频繁的上锁
        if (count.get() == capacity)
            return false;
        final int c;
        final Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            if (count.get() == capacity)
                return false;
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return true;
    }

/**
 * 和take相似,只是不再阻塞,没有元素时候直接返回null
*/
public E poll() {
        final AtomicInteger count = this.count;
        if (count.get() == 0)
            return null;
        final E x;
        final int c;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            if (count.get() == 0)
                return null;
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

/**
 * 只取元素,不删除节点
 * 不阻塞,没有元素时候直接返回null
*/
    public E peek() {
        final AtomicInteger count = this.count;
        if (count.get() == 0)
            return null;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            return (count.get() > 0) ? head.next.item : null;
        } finally {
            takeLock.unlock();
        }
    }

通知可以取

/**
 * 唤醒取元素的线程,告诉他们队列里有东西了,你们可以来取了。
 * 然而唤醒谁,这个就看使用的锁是不是公平锁了。默认是非公平的
 也就是对一批等待的线程吼一嗓子,你们取吧,然后谁拿到锁谁就可以取元素。
 * 可以看到这里也加了取锁,应该是为了一次唤醒一个吧
*/
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

通知可以放

/**
 * 这个和signalNotEmpty相似,是通知放置元素的线程,队列有空位置了,你们可以放元素了。
*/
    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }

小结:

阻塞队列通过加锁和条件变量实现线程同步
需要注意的是,条件变量必须和锁同时使用,其次判断队列状态要用while循环,因为await方法是会释放锁的,不用死等的话,会出现多个线程在await出等待,然后唤醒就不再去加锁,直接往下走。。。这样就出问题了。

发布了9 篇原创文章 · 获赞 2 · 访问量 182

猜你喜欢

转载自blog.csdn.net/weixin_42579367/article/details/104233167