JUC阻塞队列(二):LinkedBlockingQueue

1、LinkedBlockingQueue 介绍

LinkedBlockingQueue 也是接口BlockingQueue的一个实现类,与 ArrrayBlockingQueue基于

数组实现不同的是,LinkedBlockingQueue是基于单项链表实现的,在LinkedBlockingQueue

内部维护了一个单向链表来存储数据;链表原则上是无边界的,但LinkedBlockingQueue维护

了一个常量 capacity 表示队列的容量,new 创建 LinkedBlockingQueue 时若不指定 capacity

的值,capacity 默认是 Integer.MAX_VALUE

LinkedBlockingQueue 结构如下:

java 复制代码
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    private static final long serialVersionUID = -6903933977591709194L;


    //存放数据的节点
    //从这里可以发现,LinkedBlockingQueue采用单向链表来存储数据
    /**
     * Linked list node class
     */
    static class Node<E> {
        E item;

        Node<E> next;

        Node(E x) { item = x; }
    }

    private final int capacity;//队列容量

    /**
     * 使用 AtomicInteger 来记录 数据的个数
     * todo 问题:在ArrayBlockingQueue采用int 类型来记录数据个数,但在
     *           该类中为什么使用 AtomicInteger 来记录数据个数?
     *        因为 ArrayBlockingQueue 是通过一个锁来保证数据的 入队/出队,可以通过锁来
     *        保证int数据的原子性;而LinkedBlockingQueue 的 入队/出队 采用不同的锁,但
     *        count 在 入队/出队 都需要操作,所以要想保证 count需要CAS来保证原子性
     *     
     */
    private final AtomicInteger count = new AtomicInteger();

    transient Node<E> head;//队列头节点

    private transient Node<E> last;//队列尾节点

    private final ReentrantLock takeLock = new ReentrantLock();//取队列数据的锁

    private final Condition notEmpty = takeLock.newCondition(); //取数据的Condition

    private final ReentrantLock putLock = new ReentrantLock();//向队列添加数据的锁

    private final Condition notFull = putLock.newCondition();//入队列的Condition

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

    /**
     * 初始化时向队列中添加数据
     */
    public LinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
        final ReentrantLock putLock = this.putLock;
        putLock.lock(); 
        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();
        }
    }
}

2、LinkedBlockingQueue 使用示例

LinkedBlockingQueue常用方法也是 BlockingQueue定义的那几个方法,使用方式与

ArrayBlockingQueue差不多,只是每个方法的具体实现不同而已,、;

LinkedBlockingQueue 使用示例如下:

java 复制代码
public class LinkedBlockingQueueDemo01 {

    public static void main(String[] args) throws InterruptedException {

        //若创建队列时不指定队列容量大小,则默认大小是 Integer.MAX_VALUE
        LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<String>(3);

        //add 添加失败抛出异常
        queue.add("1");
        queue.add("2");
        queue.add("3");
        queue.add("4");
        //put 添加失败,则一直阻塞,直到队列有空位数据添加成功
        queue.put("4");
        //添加成功返回true,添加失败返回false
        boolean b = queue.offer("5");
        System.out.println(b);
        //带超时时间的添加
        b = queue.offer("6",5, TimeUnit.SECONDS);
        System.out.println(b);

        //从队列中取数据
        //remove 若队列中没有数据,则抛出异常
        String s = queue.remove();
        //poll 若队列为空,则返回null
        s = queue.poll();
        //带超时时间的取数据,若队列为空,则线程阻塞,若阻塞超过超时时间之后队列中还没有数据,则返回null
        s= queue.poll(5,TimeUnit.SECONDS);
        //take 从队列中取数据,若队列为空,则一直阻塞,直到队列中有数据
        s = queue.take();


    }
}

3、LinkedBlockingQueue常用方法解析

3.1、add(E e) 方法

该方法作用是向队列中添加数据,若添加失败,则直接抛出异常;

方法代码如下:

3.2、offer(E e) 方法

该方法作用也是向队列中添加数据,若添加成则返回true,添加失败返回false

offer方法代码如下:

java 复制代码
public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        //引用成员变量,获取队列数量
        final AtomicInteger count = this.count;
        //队列数据个数是否等于队列限制长度(队列容量)
        if (count.get() == capacity)
            return false;
        //作为标记存在
        //todo 使用数值类型作为标识的特点
        int c = -1;
        //将存储的数据封装成Node
        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();
        }
        //如果c==0,表示添加数据之前队列元素个数为0,这时可能会出现消费者全在阻塞状态
        //所以,添加数据之后需要唤醒消费者
        if (c == 0)
            //唤醒消费者
            signalNotEmpty();
        //c>=0表示添加成功
        return c >= 0;
    }


//添加数据
private void enqueue(Node<E> node) {
        
        last = last.next = node;
    }

3.3、singnalNotEmpty()、signalNotNull()、enqueue()

signalNotEmpty(): 唤醒消费者线程

signalNotFull(): 唤醒生产者线程

enqueue(): 入队列

java 复制代码
private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        //只有持有锁后才能调用 wait/signal
        //所以这里要先获取读锁
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

    
    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        //要想执行Condition 的方法,必须先获取相关的锁
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }

    
    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }

3.4、offer(E e, long timeout, TimeUnit unit)

该方法功能也是向队列添加数据,若添加失败则会阻塞,若超过了超时时间还没添加成功

则表示添加失败,返回false

java 复制代码
public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {

        if (e == null) throw new NullPointerException();
        //将超时时间转换成纳秒
        long nanos = unit.toNanos(timeout);
        //标记位
        int c = -1;
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        //加锁,若被中断,则抛出异常
        putLock.lockInterruptibly();
        try {
            //阻塞,直到超时时间为0
            while (count.get() == capacity) {
                if (nanos <= 0)
                    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;
    }

3.5、put(E e) 方法

该方法功能也是向队列中添加数据,若添加失败,则一直阻塞,直到队列中有空位可以

添加成功;若线程被中断,则直接抛出异常退出

java 复制代码
public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // Note: convention in all put/take/etc is to preset local var
        // holding count negative to indicate failure unless set.
        int c = -1;//作为标记
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        //加锁,若线程被中断,则抛出异常
        putLock.lockInterruptibly();
        try {
            /*
             * 若队列已经满了,则阻塞,直到队列有空位
             */
            while (count.get() == capacity) {
                notFull.await();
            }
            //向队列中添加数据
            enqueue(node);
            //更新队列数据个数
            c = count.getAndIncrement();
            //队列还没有满,通知其他生产者线程
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        //此时所有消费者线程可能都在阻塞,所有生产数据后需要唤醒消费者线程
        if (c == 0)
            signalNotEmpty();
    }

3.6、remove() 方法

该方法是从队列取数据,若队列为空,则抛出异常;

remove方法代码如下:

3.7、poll() 方法

该方法功能是删除队列头元素(第一个进入队列的元素),若队列为空,则返回null;

poll方法代码如下:

java 复制代码
/**
     * 从队列中取数据,若队列为空则返回null
     * @return
     */
    public E poll() {
        final AtomicInteger count = this.count;
        //若队列为空,则返回null
        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();
                //CAS队列元素个数减1
                //先获取再减1
                c = count.getAndDecrement();
                //队列元素多余1,当前线程消费后继续唤醒其他消费者线程
                if (c > 1)
                    notEmpty.signal();
            }
        } finally {
            takeLock.unlock();
        }
        //c获取的当前消费者线程消费之前的线程,若 c == capacity 表示队列满了,此刻当前线程
        //消费后可能会出现所有生产者线程都处于"阻塞等待" 状态,所以需要唤醒生产者线程
        if (c == capacity)
            signalNotFull();
        //返回消费的元素
        return x;
    }

3.8、poll(long timeout, TimeUnit unit)

该方法是带有超时时间的获取队列的第一个元素;若队列为空,则当前线程会阻塞,直到

超过了超时时间 timeout 后,若队列还是为空,则返回null

java 复制代码
/**
     * 删除队列第一个元素,并返回
     * 若队列为空,当前线程会阻塞,直到时间超过 timeout,若队列还是为空,则返回null
     * 若线程被中断,则直接抛出中断异常,并退出
     *
     * @param timeout
     * @param unit
     * @return
     * @throws InterruptedException
     */
    public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        E x = null;
        //标记位
        int c = -1;
        //将时间转换为纳秒
        long nanos = unit.toNanos(timeout);
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        //加锁,若线程被中断,则直接抛出异常,并退出
        takeLock.lockInterruptibly();
        try {
            //若队列为空,则阻塞等待,直到阻塞时间之后
            while (count.get() == 0) {
                if (nanos <= 0)
                    return null;
                //阻塞,并返回剩余阻塞时间
                nanos = notEmpty.awaitNanos(nanos);
            }
            //取数据
            x = dequeue();
            //CAS,队列元素个数减1
            //先获取再减1
            c = count.getAndDecrement();
            //队列元素有多个,则通知其他消费者线程
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        //此刻,生产者线程可能全处于"阻塞等待" 状态,通知唤醒生产者线程
        if (c == capacity)
            signalNotFull();
        return x;
    }

3.9、take() 方法

该方法功能也是删除并返回队列的第一个元素,若队列为空,则一直阻塞,直到队列

不为空,或着线程被中断,异常退出

java 复制代码
 /**
     * 删除并返回队列的第一个元素,若队列为空,则一直阻塞;
     * 若线程被中断,则直接抛出异常,退出
     * 
     * @return
     * @throws InterruptedException
     */
    public E take() throws InterruptedException {
        E x;
        /**
         * todo 问题:这里c为什么设置为-1?
         *    c=-1 作为标记存在,使用数值类型作为标识的特点
         */
        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();
        }
        /**
         * todo 注意:
         *    需要注意 c == capacity 这个判断,c == capacity 表示之前队列满了,当前消费了一个元素后,
         *    但此时可能存在 生产者线程全是 "阻塞" 状态,所以消费数据之后需要唤醒一个生产者线程
         */
        if (c == capacity)
            signalNotFull();
        return x;
    }

3.10、peek() 方法

该方法功能是查看(返回)队列中的第一个元素,但并不会把该元素从队列中删除。

java 复制代码
/**
     * 查看队列头元素,但并不会删除头元素
     * @return
     */
    public E peek() {
        if (count.get() == 0)
            return null;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            //获取队列第一个节点(头节点后边的节点)
            Node<E> first = head.next;
            if (first == null)
                return null;
            else
                return first.item;
        } finally {
            takeLock.unlock();
        }
    }

3.11、signalNotFull()、dequeue()

signalNotFull():唤醒阻塞等待中的生产者线程

dequeue():删除并返回队列的第一个元素

java 复制代码
/**
     * 唤醒阻塞中的生产者线程
     */
    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        //要想执行Condition 的方法,必须先获取相关的锁
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }




/**
     * 删除链表的第一个元素并返回
     */
    private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        // assert head.item == null;
        Node<E> h = head; //头节点(头节点是一个虚拟节点)
        Node<E> first = h.next;//第一个头节点
        //删除头节点,即修改节点的next指向(或 h.next=null也是一样)
        //让下一个节点作为头节点
        h.next = h; // help GC,
        //更新头结点
        head = first;
        E x = first.item;
        //删除节点数据,让其作为虚拟头节点
        first.item = null;
        return x;
    }
相关推荐
java小吕布5 分钟前
Java中Properties的使用详解
java·开发语言·后端
爱吃土豆的程序员7 分钟前
在oracle官网下载资源显示400 Bad Request Request Header Or Cookie Too Large 解决办法
java·数据库·oracle·cookie
尚学教辅学习资料29 分钟前
基于微信小程序的电商平台+LW示例参考
java·微信小程序·小程序·毕业设计·springboot·电商平台
尘浮生31 分钟前
Java项目实战II基于微信小程序的移动学习平台的设计与实现(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·学习·微信小程序·小程序
2401_857610031 小时前
Spring Boot框架:电商系统的技术优势
java·spring boot·后端
希忘auto1 小时前
详解MySQL安装
java·mysql
冰淇淋烤布蕾1 小时前
EasyExcel使用
java·开发语言·excel
拾荒的小海螺2 小时前
JAVA:探索 EasyExcel 的技术指南
java·开发语言
Jakarta EE2 小时前
正确使用primefaces的process和update
java·primefaces·jakarta ee
马剑威(威哥爱编程)2 小时前
哇喔!20种单例模式的实现与变异总结
java·开发语言·单例模式