JDK-阻塞队列、非阻塞队列原理

本文介绍了JDK中的阻塞队列与非阻塞队列原理,重点讨论了ArrayBlockingQueue和LinkedBlockingQueue的实现细节。ArrayBlockingQueue使用ReentrantLock和条件队列实现阻塞,而LinkedBlockingQueue通过不同的锁和条件队列提高性能。非阻塞队列如ConcurrentLinkedQueue利用CAS避免阻塞,但可能带来CPU占用问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

在jdk提供的队列中,有阻塞队列和非阻塞队列
阻塞队列是指:在添加元素的时候,如果队列已经满了,此时会阻塞当前入队的线程,将入队的线程放入到AQS的同步条件队列中,在队列元素出队之后,会尝试唤醒阻塞的线程,阻塞的线程将从条件队列进入到同步等待队列中进行排队;
在出队的时候,也是同样的道理,如果队列中元素为空,就会阻塞出队元素,进入到同步条件队列中,在队列中入队了元素之后,会唤醒阻塞的线程,阻塞的出队线程,从条件队列进入到等待队列中。进行排队,等待获取执行权限

非阻塞队列:在juc中,提供的非阻塞队列,是通过CAS来实现的,也就是说,如果在入队的时候,如果入队失败,会进行CAS自旋。而不是阻塞,进入到条件队列

除了阻塞和非阻塞队列之外,jdk还提供了优先级队里、延迟队列

阻塞队列

阻塞队列中,我比较熟悉的是ArrayBlockingQueue和LinkedBlockingQueue,所以,以这两个为例,来做一个学习

ArrayBlockingQueue

在这里插入图片描述

ArrayBlockingQueue,从名字就可以看出来,内部是数组结构,是有界的,那阻塞是怎么实现的呢?
在ArrayBlockingQueue中,维护了一个ReentrantLock和两个condition

/** Main lock guarding all access */
final ReentrantLock lock;

/** Condition for waiting takes */
private final Condition notEmpty;

/** Condition for waiting puts */
private final Condition notFull;

入队的源码

/**
* 这是入队的操作
* 1.加锁
* 2.判断当前数组中的元素个数等于数组长度,就返回false,表示此时队列已经满了
* 3.入队,入队成功的话,返回true
* 4.解锁
*/
public boolean offer(E e) {
  checkNotNull(e);
  final ReentrantLock lock = this.lock;
  lock.lock();
  try {
      if (count == items.length)
          return false;
      else {
          enqueue(e);
          return true;
      }
  } finally {
      lock.unlock();
  }
}

出队的源码

/**
* 这是出队的逻辑
* 1.首先加锁
* 2.如果当前队列中为空,就阻塞,调用condition的await()方法,将当前线程放入到同步条件队列中进行休眠
* 3.如果不为空,正常出队,所谓的出队,就是从数组头部出队,返回对应的元素
* 4.然后释放锁
* @return
* @throws InterruptedException
*/
public E take() throws InterruptedException {
  final ReentrantLock lock = this.lock;
  lock.lockInterruptibly();
  try {
      while (count == 0)
          notEmpty.await();
      return dequeue();
  } finally {
      lock.unlock();
  }
}

可以看到,对于ArrayBlockingQueue来说,出队和入队加的lock是同一把锁lock,所以,在同一时刻,对于ArrayBlockingQueue来说,只会有一个线程去入队或者出队

这样的话,效率就是大大的降低了,所以,linkedBlockingQueue就不一样了

LinkedBlockingQueue

linkedBlockingQueue从名字也可以看到,内部采用的是linked链表结构,假如在初始化的时候,不指定链表的长度,那就是无界的,也就是说,如果我们不指定链表长度,那就是无界的,在put的时候,永远会也不会阻塞;所以,这里所说的阻塞,是指我们在指定链表长度的前提下,才会阻塞

但是,对于get()操作,如果链表为空,都会阻塞

这是两个构造函数,可以看到,在不指定阈值的情况下,capacity的值就是Integer.MAX_VALUE

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

/**
 * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
 *
 * @param capacity the capacity of this queue
 * @throws IllegalArgumentException if {@code capacity} is not greater
 *         than zero
 */
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);
}

在LinkedBlockingQueue中,有一个和ArrayBlockingQueue,区别最大的地方是:LinkedBlockingQueue内部维护了两个lock和两个condition,所以,对于linkedBlockingQueue来说,入队和出队用的不是同一个lock,在性能上,会有一定的提升
可以看到,在类中,维护了两个lock和两个condition

/** 出队锁 Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();

/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();

/** 入队锁 Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();

/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();

来看下linkedBlockingQueue的出队和入队的源码
需要注意的是:对于linkedBlockingQueue来说,offer入队操作,不会阻塞线程,如果当前链表中元素个数超过了阈值,就会返回false,put方法在入队的时候,如果队列已经满了,就会调用condition的await()方法进入到条件队列中,就不贴源码了,基本思想都是一样的

/**
 * @throws NullPointerException if the specified element is null
 * 这是入队的源码
 * 1.如果入队元素为null,抛出异常
 * 2.加锁,加putLock
 * 3.如果当前元素个数小于链表capacity阈值,就入队
 * 4.
 */
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节点添加到链表的尾部
             */
            enqueue(node);
            c = count.getAndIncrement();
            /**
             * 如果 + 1之后依旧没有超过阈值,就唤起生产的线程,继续入队
             */
            if (c + 1 < capacity)
                notFull.signal();
        }
    } finally {
        putLock.unlock();
    }
    /**
     * c == 0这个条件满足,说明之前是空的队列,现在插入了一个,因为初始化的时候,c为 -1
     * 如果插入了一个元素,就唤起消费的线程去出队
     */
    if (c == 0)
        signalNotEmpty();
    return c >= 0;
}
/**
* 这是出队的方法
* 1.加takeLock锁
* 2.如果当前链表为空,就await
* 3.否则,就出队,然后将链表数量 - 1
* 4.如果数量 > 1,就唤醒出队线程,继续出队
* 5.解锁
* 6.最后会判断,如果当前链表长度 = capacity阈值。就唤醒入队线程继续入队
* @return
* @throws InterruptedException
*/
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();
  }
  if (c == capacity)
      signalNotFull();
  return x;
}

其实对于阻塞队列的阻塞,我之前是一直有疑问的,把源码整个读下来之后,我认为,所谓的阻塞是指:
1.在尝试加锁的时候,线程有可能加锁失败,此时会进入到同步等待队列中排队,等待获取执行权限
2.在加锁成功之后,如果队列为空,出队线程会阻塞;如果队列已经满了,入队线程会阻塞;此时的阻塞,是指线程会进入到同步条件队列中,在队列不为空,或者队列不满的时候,会唤醒该线程,去继续进行队列操作

非阻塞队列

非阻塞队列我是以ConcurrentLinkedQueue为切入点来学习的,所谓的非阻塞,简单来说,就是通过cas来进行入队和出队操作,这样的话,如果有多线程同时入队,会通过cas保证,只有一个线程可以cas,这样的话,其他线程会不停的cas去重试,这里有一个疑问点:
对于这种不停的进行自旋的操作,是否有阻塞的效率高?我觉得如果长时间cas,是否会对CPU造成影响?

我们先来看下入队的源码

public boolean offer(E e) {
    checkNotNull(e);
    final Node<E> newNode = new Node<E>(e);

    /**
     * 这里的t = p = tail
     * q = tail.next
     * 这里的for循环,如果插入失败,会一直重试,直到成功为止
     */
    for (Node<E> t = tail, p = t;;) {
        Node<E> q = p.next;
        /**
         * 如果p.next = null 表示当前p节点就是尾结点
         * 直接cas设置当前newNode节点到p.next
         */
        if (q == null) {
            // p is last node
            if (p.casNext(null, newNode)) {
                // Successful CAS is the linearization point
                // for e to become an element of this queue,
                // and for newNode to become "live".
                if (p != t) // hop two nodes at a time
                    casTail(t, newNode);  // Failure is OK.
                return true;
            }
            // Lost CAS race to another thread; re-read next
        }
        /**
         * 如果p == p.next?
         */
        else if (p == q)
            // We have fallen off list.  If tail is unchanged, it
            // will also be off-list, in which case we need to
            // jump to head, from which all live nodes are always
            // reachable.  Else the new tail is a better bet.
            p = (t != (t = tail)) ? t : head;
        else
            // Check for tail updates after two hops.
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

可以看到,在入队的时候,并没有像阻塞队列那样,加锁、await等操作

并且,在concurrentLinkedQueue中,维护的node节点,都是volatile修饰的

private transient volatile Node<E> head;

private transient volatile Node<E> tail;

并且node节点中的

volatile E item;
volatile Node<E> next;

也是volatile修饰的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值