Java源码解读系列6—LinkedBlockingQueue(JDK1.8 )

1 概述

LinkedBlockingQueue(简称LBQ)是BlockingQueue接口的实现类,是一种无界、阻塞、线程安全的FIFO队列。LBQ底层存储基于链表,头节点为哨兵节点,里面的数据域(item)为空。插数据时从尾节点插入,取数据时从头节点(若队列非空,则找第一个item不为空的节点)获取。相对于非阻塞的ConcurrentLinkedQueue(简称CLQ队列),LBQ的实现比较简单,就是采用添加和取出两把锁来保证线程安全。

在这里插入图片描述

2 链表节点的数据结构

  static class Node<E> {
  
       //存储当前节点的元素
        E item;
      
       //指向下一个链节点
        Node<E> next;

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

3 构造函数

public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }
    
  public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
 }

4 阻塞式新增操作(put方法)

这里提下上锁的方式

  public void put(E e) throws InterruptedException {
        //非空校验
        if (e == null) throw new NullPointerException();
   
        int c = -1;
        //初始化一个新的节点
        Node<E> node = new Node<E>(e);
        
  
        final ReentrantLock putLock = this.putLock;
        //获取当前时间队列中节点的总数
        final AtomicInteger count = this.count;
        
        /*
         * 三种上锁方法对比:
         * 1)lock()方法:成功则获取锁,失败则使线程排队,甚至阻塞。
         * 2)lockInterruptibly()方法成功则获取锁,失败则使线程排队,甚至阻塞。跟lock比较最大得区别是能响应异常,因为这个方法是抛出异常得。
        * 3)tryLock()方法成功则获取锁,失败则返回false,不再执行获取锁操作。也就当前线程不存在排队或者阻塞。
       putLock.lockInterruptibly();
        try {        
             //如果队列中节点的总数达到队列的容量,则线程进入等待状态,让出CPU
            while (count.get() == capacity) {
                notFull.await();
            }
            //将node节点添加到链表头部
            enqueue(node);
            //队列中节点的总数加1
            c = count.getAndIncrement();
            //count.getAndIncrement()返回的是count未自增前的值
            //c+1表示count自增后的数量如果小于容量,说明可以唤醒notFull上等待的线程
           if (c + 1 < capacity)
                notFull.signal();
        } finally {
        //释放锁
            putLock.unlock();
        }
        //count未自增前的值若为0,说明自增前队列为空,此时可能有线程在notEmpty上等待
        //自增后count的值大于1,说明队列有值,可以唤醒在notEmpty上等待的线程
        if (c == 0)
            signalNotEmpty();
    }
    
    
    //将新增节点插入到链表尾部
  private void enqueue(Node<E> node) {
        last = last.next = node;
    }
    
    
    //唤醒在notEmpty上等待的线程
    // condition.signal操作是唤醒等待最久的节点
     private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

5 非阻塞式新增操作(offer)

offer方法跟put类似,最大区别是offer添加时候如果队列满了,则返回false,表示添加失败,结束当前操作;而put会使当前线程进入阻塞状态,直到添加成功。

 public boolean offer(E e) {
       //非空检验
        if (e == null) throw new NullPointerException();
        //获取当前时间队列中节点的总数
        final AtomicInteger count = this.count;
        //如果队列中排队节点的总数达到队列的容量,则返回false,表示添加失败
        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) {
             //将node节点添加到链表头部
                enqueue(node);
                  //队列中节点的总数加1
                c = count.getAndIncrement();
                 //c+1表示count自增后的数量如果小于容量,说明可以唤醒notFull上等待的线程
                if (c + 1 < capacity)
                    notFull.signal();
            }
        } finally {
           //释放锁
            putLock.unlock();
        }
         //自增后count的值大于1,说明队列有值,可以唤醒在notEmpty上等待的线程
        if (c == 0)
            signalNotEmpty();
            
         //如果c小于0,说明没有添加成功,返回false
        return c >= 0;
    }

6 阻塞式取出操作(take方法)

阻塞式获取头节点元素,(这里的返回值实际是h.next中的值,因为头节点中包含的item永远为空)。如果队列为空,则发生阻塞。
这里 while (count.get() == 0) { notEmpty.await(); }阻塞代码使用while循环,可以换成if吗?答案式不可以。因为添加操作的上锁方式采用lockInterruptibly(),是可以被中断,所以需要通过while循环做保证。

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();
            //队列总数减1操作
            c = count.getAndDecrement();
            //队列总数count减1操作前的数量大于1,说明队列中至少有2个元素,执行减1操作后至少还剩下1个
            if (c > 1)
            //唤醒在notEmpt上等待的线程
                notEmpty.signal();
        } finally {
           //释放锁
            takeLock.unlock();
        }
        //队列总数count减1操作前的数量等于队列容量,说明减1操作后就小于队列容量,在notFull上等待的线程可以被唤醒
        if (c == capacity)
           //唤醒在notFull上等待的线程
            signalNotFull();
        return x;
    }


    //将头节点出队列,但返回值式head.next中包含元素x
    //将head的下一个节点first变为头节点,并且把first的item清空
    这样头节点中item永远为null
        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;
    }

//唤醒在notFull上等待的线程
 private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }

添加操作是操作尾节点,取出操作是操作尾节点,两者互不影响,因此效率比较高。

7 非阻塞式取出操作(poll方法)

poll方法跟take类似,最大区别是poll获取元素时如果队列为空,则返回null,表示获取失败,结束当前操作;而take会使当前线程进入阻塞状态,直到获取成功。

 public E poll() {
       //队列中节点数
        final AtomicInteger count = this.count;
        //队列中节点数为0
        if (count.get() == 0)
            //返回空
            return null;
        E x = null;
        int c = -1;
        final ReentrantLock takeLock = this.takeLock;
        //上锁
        takeLock.lock();
        try {
            if (count.get() > 0) {
             //将头节点出队列
                x = dequeue();
                //队列总数减1操作
                c = count.getAndDecrement();
                //队列总数count减1操作前的数量大于1,说明队列中至少有2个元素,执行减1操作后至少还剩下1个
                if (c > 1)
                  //唤醒在notEmpt上等待的线程
                    notEmpty.signal();
            }
        } finally {
            //释放锁
            takeLock.unlock();
        }
          //队列总数count减1操作前的数量等于队列容量,说明减1操作后就小于队列容量,在notFull上等待的线程可以被唤醒
        if (c == capacity)
            signalNotFull();
        return x;
    }

8 非阻塞式查询操作(peak方法)

非阻塞式获取头节点的元素(这里的返回值实际是h.next中的值,因为头节点中包含的item永远为空)

 public E peek() {
 //判断队列是否为空,为空则返回null
        if (count.get() == 0)
            return null;
        final ReentrantLock takeLock = this.takeLock;
        //上锁
        takeLock.lock();
        try {
            //头节点中包含的item永远为空,所以是判断头节点的下一个节点的item值
            Node<E> first = head.next;
            //为空则分返回null
            if (first == null)
                return null;
            else
            //不为空则返回item值
                return first.item;
        } finally {
           //是否锁
            takeLock.unlock();
        }
    }

9 获取队列的大小(size方法)

  public int size() {
        //返回当前队列的大小
        return count.get();
    }

10 参考文献

1)JDK7在线文档
https://tool.oschina.net/apidocs/apidoc?api=jdk_7u4
2) JDK8在线文档
https://docs.oracle.com/javase/8/docs/api/
3) Bruce Eckel. Java编程思想,第4版,2007,机械工业出版社
4)方腾飞,魏鹏,程晓明. Java并发编程的艺术,第1版,2015年,机械工业出版社

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值