• 并发和多线程(二十)LinkedBlockingQueue源码解析 Diamond



    阻塞队列在日常开发中直接使用比较少,但是在很多工具类或者框架中有很多应用,例如线程池,消息队列等。所以,深入了解阻塞队列也是很有必要的。所以这里来了解一下LinkedBlockingQueue的相关源码,从命名可以看出来是由链表实现的数据结构。

    类定义

        public class LinkedBlockingQueue<E> extends AbstractQueue<E>
                implements BlockingQueue<E>, java.io.Serializable {
    
        }
    

    从上面可以看到继承Queue,实现BlockingQueue,我们介绍一下两个类里面方法的作用,为了方便记忆和对比放到表格里进行展示。

    Queue
    作用 方法1 方法2 区别
    新增 add() offer() add()队列满的时候抛出异常,offer()队列满的时候返回false
    查看并删除 remove() poll() remove()队列为空的时候抛出异常,poll()队列为空的时候返回null
    查看不删除 element() peek() element()队列为空的时候抛出异常,peek()队列为空的时候返回null
    BlockingQueue

    BlockingQueue顾名思义带有阻塞的队列,方法有所区别,下面方法包含了Queue,因为属于继承关系,下面表格方法名用序号代替。

    作用 方法1 方法2 方法3 方法4 区别
    新增 add() offer() put() offer(E e, long timeout, TimeUnit unit) 队列满的时候,1和2作用和queue相同,3会一直阻塞,4阻塞一段时间,返回false
    查看并删除 remove() poll() take() poll(long timeout, TimeUnit unit) 队列为空,1和2没有变化,3会一直阻塞,4会阻塞一段时间,返回null
    查看不删除 element() peek() 队列为空,1和2没有变化

    成员变量

        //链表的容量,默认Integer.MAX_VALUE
        private final int capacity;
    
        //当前存在元素数量
        private final AtomicInteger count = new AtomicInteger();
    
        //链表的head节点
        transient Node<E> head;
    
        //链表的tail节点
        private transient Node<E> last;
    
        //主要用于take, poll等方法的加锁
        private final ReentrantLock takeLock = new ReentrantLock();
    
        //主要用在取值的阻塞场景
        private final Condition notEmpty = takeLock.newCondition();
    
        //主要用于put, offer等方法的加锁
        private final ReentrantLock putLock = new ReentrantLock();
    
        //主要用在新增的阻塞场景
        private final Condition notFull = putLock.newCondition();
        
        //Node比较简单,一个item,还有指向下个节点,也就是单向链表
        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; }
        }
    

    构造函数

        //默认队列存储的元素最大为Integer.MAX_VALUE
        	public LinkedBlockingQueue() {
                this(Integer.MAX_VALUE);
            }
        	
        	//自定义capacity,初始化head、tail
        	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
                putLock.lock();
                try {
                    int n = 0;
                    //遍历集合,每次生成一个node,添加到链表尾部
                    for (E e : c) {
                        if (e == null)
                            throw new NullPointerException();
                        //每次判断新增的节点是否超过capacity,如果是,抛出异常
                        if (n == capacity)
                            throw new IllegalStateException("Queue full");
                        //将节点添加到队列tail
                        enqueue(new Node<E>(e));
                        ++n;
                    }
                    //设置当前元素个数count
                    count.set(n);
                    //finally解锁
                } finally {
                    putLock.unlock();
                }
            }
    

    offer()

        public boolean offer(E e) {
            if (e == null) throw new NullPointerException();
            final AtomicInteger count = this.count;
            if (count.get() == capacity)
                return false;
            //初始设置为-1,c < 0,表示新增失败
            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;
        }
    

    流程:

    1. 如果e为空,抛出异常。
    2. 如果当前队列已满,返回false。
    3. 将e封装成一个新的节点,加锁,putLock。
    4. 再次判断队列元素数量 < capacity,然后将node添加到链表tail。
    5. CAS将count+1,注意这里调用的是getAndIncrement返回的是+1之前的值。如果队列没满,唤醒某个某个因为添加而阻塞的线程。
    6. finally解锁,如果c == 0,加锁takeLock,唤醒继续添加。
    7. 返回 c >= 0。

    put()

    相对于offer(),put的代码会判断当前队列是否满了,如果满了,通过Condition阻塞,其他没啥区别。
    在这里插入图片描述

    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();
            }
            //注意这里c是先获取,后-1的
            if (c == capacity)
                signalNotFull();
            return x;
        }
    

    流程:

    1. 加锁,takeLock。
    2. 如果当前队列为空,直接通过notEmpty阻塞,等待被唤醒。
    3. 取出第一个元素,并删除元素。
    4. 如果c > 1,表示队列还有元素,唤醒别的线程获取。
    5. finally解锁,如果c == capacity,表示队列没满,加锁takeLock,唤醒继续添加。
    6. 返回 x。

    enqueue and dequeue

        private void enqueue(Node<E> node) {
            last = last.next = node;
        }
    
        private E dequeue() {
            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;
        }
    

    这里统一讲一下在链表中添加和删除数据的流程,特别是dequeue(),我刚看第一眼的时候有点蒙蔽的,下面举个栗子。

            BlockingQueue<Integer> queue = new LinkedBlockingDeque<>();
            queue.offer(1);
            queue.offer(2);
            queue.offer(3);
            Integer take = queue.take();
            System.out.println(take);
    

    在这里插入图片描述

    这里把每一步都画出来了,还是比较好理解的。其余方法的逻辑都比较相似,下面简单说一下。

    peek()

    peek()和take()的代码差不多,只是不会删除元素,take()通过dequeue(),而peek()通过一句代码Node first = head.next;获得该节点的数据然后返回。

  • 相关阅读:
    SQL Sever 各版本下载
    使用REPLACE更新某表中某个字段详细内容【SQL语句】
    常用css简写
    CSS hack:区分IE6,IE7,IE8,firefox
    浅析vue中的provide / inject 有什么用处
    Git常用命令总结
    ts
    学会使用Vue JSX,一车老干妈都是你的
    关于javascript的Object. hasOwnProperty,看我就够了
    JavaScript进阶笔记(七):异步任务和事件循环
  • 原文地址:https://www.cnblogs.com/huigelaile/p/15780390.html
Copyright © 2020-2023  润新知