java queue LinkedBlockingQueue源码解析

一、概念

1、LinkedBlockingQueue是一个单向链表实现的阻塞队列,先进先出的顺序;

2、它的容量限制是可选的,默认容量是int的最大值;

3、支持多线程并发操作;

4、在队列元素的出队和入队使用不同的锁,添加和删除数据的时候可以并行;

5、队头的元素是插入时间最长的,队尾的元素是最新插入的,新的元素被插入到队列的尾部

二、源码分析

1、LinkedBlockingQueue中的字段

    在下面定义的几个变量和一个内部类可以看出LinkedBlockingQueue在内部是维持着一个队列,所以有一个头结点head和一个尾结点last,并且在内部维持着两把锁,takeLock用于出队列,putLock用于入队列,还有与两把锁关联的condition对象。

/**队列的容量,默认值是int的最大值*/
    private final int capacity;

    /**队列中元素 的个数*/
    private final AtomicInteger count = new AtomicInteger();

    /**队列的头结点,始终是head.item=null*/
    transient Node<E> head;

    /**队列的为节点,始终是last.next=null*/
    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();
    /**
     * 内部类,队列中的节点,用于存在元素和指向下一个结点
     */
    static class Node<E> {
    	//元素值
        E item;
        //指向下一个结点
        Node<E> next;

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

2、队列的初始化

    LinkedBlockingQueue队列的初始化主要有三种方法,在下面的三种方法中可以看出队列的特性有:

    (1)可以指定容量,也可以使用使用默认容量为int的最大值;

    (2)当初始化队列时,默认队列的头结点head和尾结点last都为null

    (3)队列中的元素不能为null

/**
     * 初始化队列,默认的容量是int的最大值 
     */
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

    /**
     * 初始化队列,指定队列的容量,初始化时,头结点和为节点都为null
     * @param capacity
     */
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

    
    /**
     * 初始化包含指定集合元素的队列
     * @param c
     */
    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) {
            	//如果元素为null,抛出一查昂
                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();
        }
    }

3、LinkedBlockingQueue中入队列的方法

(1)put(E e)方法

      下面是使用put方法向队列中插入一个元素的过程:首先需要对元素进行判空检查,如果为空,直接抛出异常;创建一个新的node结点,其元素值为插入的元素e;获取到入队列的锁putLock;如果队列中的元素已经满了,那么需要等待;当所有的条件都满足时,才向队列中插入元素,插入元素之后,如果队列没有满,那么要唤醒notFull条件,告诉其他执行入队列操作的元素可以执行操作了,在执行插入操作完成之后,需要对锁进行释放。在最后会检查队列中的元素是否为空,如果为空,需要唤醒notEmpty条件,使得取元素的线程进行等等待。

public void put(E e) throws InterruptedException {
    	//如果元素为空,抛出异常
        if (e == null) throw new NullPointerException();
        int c = -1;
        //创建一个新的节点,元素为e
        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();
            //查看元素的合适是否满了,如果没有满,唤醒在notFull条件上等待的某个线程
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
        	//释放入队列锁
            putLock.unlock();
        }
        //如果元素的个数是0,则唤醒在notEmpty条件上等待的某个线程
        if (c == 0)
            signalNotEmpty();
    }

(2)offer(E e, long timeout, TimeUnit unit)方法

    该方法与put方法向队列中插入元素的区别是当队列中的元素满时,并不是一直等待下去,而是有一定的等待时间,如果超过这个时间仍未满足向队列中插入元素的条件,那么停止等待,直接返回。

/**
     * 向队列中添加元素,有等待时间,超时则结束等待
     * @param e :要插入的元素
     * @param timeout:等待的时间
     * @param unit:等待的时间单位
     */
    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 {
        	//如果队列满了
            while (count.get() == capacity) {
            	//等待的时间小于等于0,结束等待,直接返回
                if (nanos <= 0)
                    return false;
                //队列满,则根据阻塞的时间进行等待
                nanos = notFull.awaitNanos(nanos);
            }
            //队列没满插入元素
            enqueue(new Node<E>(e));
            c = count.getAndIncrement();
            //插入元素后队列没满,则唤醒notFull条件上等待的某个队列
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
        	//释放锁
            putLock.unlock();
        }
        //如果队列元素为0,唤醒notEmpty条件上的线程
        if (c == 0)
            signalNotEmpty();
        return true;
    }

(3)offer(E e)方法

    该方法与上面两个方法的区别是在插入元素时,完全不进行等待,如果当前满足插入的条件,那么立刻结束执行返回。

/**
     * 向队列中插入元素,如果队列是满的,不进行等待,直接返回
     * @param e:要插入的元素
     */
    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;
    }

    在上面三种向队列中插入元素的方法中都调用了两个方法,enqueue(node)和signalNotEmpty()方法,其中:

enqueue方法的源码如下:可以看到每次插入元素时,都是插入到队列的尾部,在插入时,是将新的节点赋值给当前last节点的下一个节点,然后将新的节点设置为last

    private void enqueue(Node<E> node) {
    	//新节点赋给当前的最后一个节点的下一个节点,然后在将这个节点设为最后一个节点
        last = last.next = node;
    }

singalNotEmpty方法的源码如下:该方法主要是永不唤醒在notEmpty条件上等待的线程,首先是获取到出队列锁,然后上锁,唤醒在notEmpty条件上等待的线程,最后释放锁。


    private void signalNotEmpty() {
    	//出队列锁
        final ReentrantLock takeLock = this.takeLock;
        //获取锁
        takeLock.lock();
        try {
        	//唤醒某个在notEmpty条件上等待的线程
            notEmpty.signal();
        } finally {
        	//释放锁
            takeLock.unlock();
        }
    }

    这是一个元素插入队列的示例图,其操作主要分为三步:创建新的节点;将尾结点的next指向新的节点;将新节点置为尾结点

4、LinkedBlockingQueue中出队列的方法

(1)take()方法

    在出队列时,首先要获取出队列的锁,如果队列为空,则在notEmpty条件上进行等待,满足条件后,取出队列中的元素,同时更新队列中元素的个数,如果元素的个数大于1,则唤醒在notEmpty条件上等等待的线程,表示可以继续取出元素,最后去是释放锁,判断结点出队列时队列是否是满的,如果是则唤醒在notFull条件上等待的线程,表示队列已经满了,入队列的线程需要进行等待。

public E take() throws InterruptedException {
        E x;
        int c = -1;
        //获取队列中的元素的个数
        final AtomicInteger count = this.count;
        //获取出队列锁
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
        	//如果队列中的元素个数为0,在notEmpty条件上等待
            while (count.get() == 0) {
                notEmpty.await();
            }
            //获取出队列的元素
            x = dequeue();
            //更新元素的个数,返回的是以前的元素的个数
            c = count.getAndDecrement();
            //如果队列中的元素个数不为0,则唤醒在notEmpty上等待的线程
            if (c > 1)
                notEmpty.signal();
        } finally {
        	//释放出队列锁
            takeLock.unlock();
        }
        //如果队列满了,唤醒在notFull条件上等待的线程
        if (c == capacity)
            signalNotFull();
        return x;
    }

(2)poll(long timeout, TimeUnit unit)方法

    该出队列的方法与take的区别是当队列为空时,等待的时间可以进行指定,并不是无限制等待下去,如果超出这个等待时间,队列仍为空,那么结束等待,直接返回。

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 {
        	//如果队列中的元素个数为0,进入等待,如果等待时间达到0,则结束等待
            while (count.get() == 0) {
                if (nanos <= 0)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
            //执行出队列,并获取出队列的元素
            x = dequeue();
            //更新当前队列中的元素个数
            c = count.getAndDecrement();
            //如果队列中的元素个数不为0,则唤醒notEmpty条件中等待的某个线程
            if (c > 1)
                notEmpty.signal();
        } finally {
        	//释放锁
            takeLock.unlock();
        }
        //如果队列满了,唤醒在notFull条件上等待的线程
        if (c == capacity)
            signalNotFull();
        return x;
    }

(3)poll()方法

    该方法在取出队列中的元素时与上面两个方法的区别是不进行等待,当队列中的元素为空时,直接结束返回。

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();
                c = count.getAndDecrement();
                if (c > 1)
                    notEmpty.signal();
            }
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

   在取出队列中的元素的三个方法中都调用了dequeue方法和signalNotFull方法:

    dequeue方法的源码如下:他的作用是将头结点head更新为之前头结点的下一个结点,并且将更新后的头结点的item值置为null。

private E dequeue() {
    	//获取到头结点
        Node<E> h = head;
        //头结点的下一个结点是队列中的第一个元素
        Node<E> first = h.next;
        //头结点的next结点为自己
        h.next = h; // help GC
        //更新头结点
        head = first;
        //返回头结点的元素
        E x = first.item;
        //将头结点的item值置为null
        first.item = null;
        return x;
    }

signalNotFull方法的源码如下:他主要是用于唤醒在notFull条件上等待的某个线程。

private void signalNotFull() {
    	//入队列锁
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
        	//唤醒在notFull条件上等待的某个线程
            notFull.signal();
        } finally {
            //释放锁
            putLock.unlock();
        }
    }

5、LinkedBlockingQueue中的remove方法

    在队列中删除某个指定的元素值时,需要将入队列锁和出队列锁都进行锁定,此时防止队列中的元素进行变动,然后对链表进行遍历,寻找指定的元素,如果找到了该元素所在的节点,那么将这个节点从链表中断开,之后对锁进行释放。

    public boolean remove(Object o) {
    	//如果元素为空,直接返回
        if (o == null) return false;
        //获取出队列锁和入队列锁:此时入队列和出队列操作都不允许执行
        fullyLock();
        try {
        	//开始遍历队列
            for (Node<E> trail = head, p = trail.next;
                 p != null;
                 trail = p, p = p.next) {
            	//如果当前结点的值等于要删除的元素
                if (o.equals(p.item)) {
                    unlink(p, trail);
                    return true;
                }
            }
            return false;
        } finally {
            fullyUnlock();
        }
    }
    
    void unlink(Node<E> p, Node<E> trail) {
    	//将结点的元素值置为空
        p.item = null;
        //断开p结点
        trail.next = p.next;
        //如果p为尾部结点,那么重新复制尾结点
        if (last == p)
            last = trail;
        //更新元素的个数,且如果队列满了,那么唤醒在notFull条件上等待的某个线程
        if (count.getAndDecrement() == capacity)
            notFull.signal();
    }
    
    //获取两把锁
    void fullyLock() {
        putLock.lock();
        takeLock.lock();
    }
    //释放两把锁
    void fullyUnlock() {
        takeLock.unlock();
        putLock.unlock();
    }

三、总结

1、在LinkedBlockingQueue中是以链表的形式存储元素的,head节点为空,第一个元素存放在head.next节点中 ;

2、队列中的元素值不能为null;

3、队列是多线程安全的,它存在两把锁,一把锁是用于元素进队列时,一把锁用于元素出队列时,所以在元素进队和出队是可以同时进行的;

4、队列中元素的个数count类型是AtomicInteger类型的,他是一个提供原子操作的Integer类,通过线程安全的方式进行加或减操作,在这个队列里使用它是因为队列是线程安全的,但是对于出队列操作和如队列操作使用的是不同的锁,但是都会访问这个值来计算队列中元素的个数,所以他也需要是线程安全的;

5、队列操作的两把锁都是用的是ReenTrantLock锁,所以在结束操作后,都需要手动声明去加锁和释放锁,如果忘记释放会造成死锁;








  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值