阻塞队列、原子类原理分析 -- ArrayBlockingQueue、AtomicInteger

阻塞队列、原子类原理分析

一、常用阻塞队列

	### 1.1 使用场景

​ 阻塞队列比较普遍的使用场景是生产者、消费者, 以便于服务解耦,提高应用性能; 而分布式架构应用比较频繁的是消息队列比如:Kafka、Rocketmq, 阻塞队列类似于broker, 生产者和消费者类似于服务生成逻辑和消费逻辑,基于阻塞队列解耦。同时阻塞队列是一个FIFO队列, 对于需要实现目标服务顺序访问的场景, 也可以使用。

  • 实现逻辑的异步解耦
  • 实现目标服务的顺序访问

1.2 常用的阻塞队列

​ 下面是JDK提供的比较常用的阻塞队列

阻塞队列 说明
ArrayBlockingQueue 基于数组实现的有界阻塞队列, 按照先进先出(FIFO)原则对元素进行排序。
DelayQueue 基于优先级队列实现的阻塞队列
LinkedBlockingDeque 基于链表实现的双向阻塞队列
LinkedBlockingQueue 基于链表实现的有界阻塞队列, 默认最大长度是Integer.MAX_VALUE, 按照先进先出(FIFO)原则排序
LinkedTransferQueue 基于链表实现的无界阻塞队列
PriorityBlockingQueue 基于优先级排序的无界阻塞队列, 默认采用升序排序, 自定义排序规则实现方式, 1) 覆盖compareTo方法实现 2) 初始化PriorityBlockingQueue时, 指定构造参数Comparator对元素进行排序
SynchronousQueue 不存储元素的阻塞队列, 每个put操作必须对应一个take操作, 否则不能继续添加元素

在这里插入图片描述

1.3 阻塞队列的常用方法

  • 插入操作
    • add(e), 添加元素到队列中, 如果队列满了, 继续插入元素会报IllegalStateException异常
    • offer(e), 添加元素到队列中,返回元素是否插入成功, true – 成功, false – 失败
    • offer(e, unit), 如果阻塞队列满了, 再继续添加元素会被阻塞指定时间, 超时后线程直接退出
    • put(e), 如果阻塞队列满了, 再继续添加元素, 线程会被阻塞, 直到队列可用
  • 移除操作
    • remove(), 当队列为空时, 删除会返回false;如果删除元素成功, 返回true
    • poll(), 如果队列存在元素, 则从队列取出一个元素, 如果队列为空, 返回null
    • poll(time, unit), 如果队列为空, 会等待指定超时时间再去获取元素
    • take(),如果队列为空,再获取元素线程会被阻塞, 直到队列可用

二、ArrayBlockingQueue原理分析

​ ArrayBlockingQueue是基于数组、ReentrantLock、Condition实现的阻塞队列, 它的**数据结构是 数组 + 等待队列**构成, 下面看下构造方法

public ArrayBlockingQueue(int capacity, boolean fair) {
  if (capacity <= 0)
    throw new IllegalArgumentException();
  this.items = new Object[capacity];
  lock = new ReentrantLock(fair); // 重入锁, 添加、获取元素需要获得这把锁
  notEmpty = lock.newCondition(); // 初始化非空等待队列
  notFull =  lock.newCondition(); // 初始化非满阻塞队列
}
  • capacity: 表示数组长度, 也就是队列长度
  • fair: 表示是否为公平的阻塞队列, 默认false, 表示基于非公平锁实现的阻塞队列

2.1 添加操作

add(e)

//1. 添加元素, 返回boolean结果
public boolean add(E e) {
  return super.add(e); // 调用父类add方法
}

//2. AbstractQueue添加元素的方法
public boolean add(E e) {
  if (offer(e)) // 添加成功返回true
    return true;
  else // 添加失败, 抛出异常
    throw new IllegalStateException("Queue full");
}

​ 这里使用了模版设计模式, 以ArrayBlockingQueue的add方法作为入口, 实际调用的是父类的add方法, 这样做的目的是解决通用型问题。

​ add内部调用了offer方法, 用于判断队列是否满了, 如果offer返回true表示添加成功, 如果返回false,表示队列满了,会抛出IllegalStateException异常

offer(e)

public boolean offer(E e) {
  checkNotNull(e); // 检查元素e是否为空
  final ReentrantLock lock = this.lock; // 添加重入锁
  lock.lock();
  try {
    if (count == items.length) // 列表满了, 返回false, 不用等待, 注意和put区别
      return false;
    else { // 队列没满, 进行入队操作
      enqueue(e);
      return true;
    }
  } finally {
    lock.unlock(); // 释放锁
  }
}

​ offer方法根据是否添加成功返回对应的逻辑值, true – 成功, false – 失败

  • 校验添加的元素是否为空
  • 添加重入锁, 进行加锁、解锁操作
  • 校验阻塞队列是否已经满了,
    • 如果满了返回false
    • 如果没满进行入队操作(enqueue)
  • 释放获取的锁

enqueue(e)

/**
	* Inserts element at current put position, advances, and signals.
  * Call only when holding lock.
  */
private void enqueue(E x) {
  // assert lock.getHoldCount() == 1;
  // assert items[putIndex] == null;
  final Object[] items = this.items; 
  items[putIndex] = x; // 通过putIndex索引对数据赋值
  if (++putIndex == items.length) // 当putIndex等于数组长度时, 将putIndex重置为0, 避免数组下标越界
    putIndex = 0;
  count++; // 记录队列中元素个数
  notEmpty.signal(); // 元素入队列成功, 发出signal通知
}

​ add、offer方法添加元素到阻塞队列中的核心处理方法, 线程持有锁后, 通过putIndex索引直接将元素添加到数组items, 这里有两个核心地方

  • putIndex == item.length时, 需要将putIndex重置为0, 避免下标越界
  • 添加元素后需要通过 notEmpty.signal发送通知, 让等待的消费线程可以继续执行, 因为获取(take)元素时, 如果队列为空会执行notEmpty.await进行等待

put(e)

public void put(E e) throws InterruptedException {
  checkNotNull(e); // 检查元素是否为空
  final ReentrantLock lock = this.lock; // 获得锁
  /**
  	* 注意和lock.lock()方法的区别
  	* lockInterruptibly方法允许在等待时, 由其它线程调用interrupt进行中断
  	* lock方法是尝试获得锁成功后才响应中断
  	*/
  lock.lockInterruptibly();
  try {
    while (count == items.length) // 检查队列是否已经满了, 如果满了notFull进行等待
      notFull.await();
    enqueue(e); // 队列未满, 进行入队操作
  } finally {
    lock.unlock(); // 释放锁
  }
}

​ put方法和offer、add方法一样都是添加元素的方法, 注意它们之间的区别

  • add方法, 内部调用的是offer, 如果队列满了, 抛出IllegalStateException异常, 即offer返回false
  • offer方法, if检测, 如果队列满了, 会直接返回false; 如果是offer(e, time, unit)队列满时需等待
  • put方法, while循环检测, 如果队列满了, notFull.await()等待, 当队列未满时继续执行入队操作

2.2 删除操作

remove

//AbstractQueue.remove
public E remove() {
  E x = poll(); // 内部调用poll获取队列第一个有效元素
  if (x != null) // 元素x不为空, 直接返回这个元素
    return x;
  else // x==null,抛出异常
    throw new NoSuchElementException();
}

​ 内部调用poll方法, 如果获取到元素直接返回这个元素, 如果没有获取到元素抛出NoSuchElementException异常

poll

//1. 获取元素
public E poll() {
  final ReentrantLock lock = this.lock; // 获取锁对象
  lock.lock();
  try {
    return (count == 0) ? null : dequeue(); // 如果队列不为空, dequeue返回元素, 否则返回null
  } finally {
    lock.unlock(); // 释放占有的锁
  }
}

//2. 带超时时间的获取元素
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
  long nanos = unit.toNanos(timeout);
  final ReentrantLock lock = this.lock; // 获取重入锁
  /**
  	* 注意和lock.lock()方法的区别
  	* lockInterruptibly方法允许在等待时, 由其它线程调用interrupt进行中断
  	* lock方法是尝试获得锁成功后才响应中断
  	*/
  lock.lockInterruptibly();
  try {
    while (count == 0) {
      if (nanos <= 0)
        return null;
      nanos = notEmpty.awaitNanos(nanos); // 超时等待, notEmpty等待nanos时间
    }
    return dequeue(); // 入队操作
  } finally {
    lock.unlock(); // 释放持有的锁
  }
}
  • 获取重入锁, 进行加锁、解锁操作
  • 校验阻塞队列是否为空
    • 如果为空, 返回null
    • 如果不为空, 进行dequeue操作, 返回对应元素
  • 释放获取的锁

NOTE: 分析上面代码, 需要注意poll()和poll(time, unit)的区别, poll(time, unit)会进行超时等待, 而poll会直接返回。

take

public E take() throws InterruptedException {
  final ReentrantLock lock = this.lock; // 获取锁
  /**
  	* 注意和lock.lock()方法的区别
  	* lockInterruptibly方法允许在等待时, 由其它线程调用interrupt进行中断
  	* lock方法是尝试获得锁成功后才响应中断
  	*/
  lock.lockInterruptibly();
  try {
    while (count == 0) // 队列为空, 进行等待操作
      notEmpty.await();
    return dequeue(); // 队列非空, dequeue获取元素
  } finally {
    lock.unlock(); // 释放持有的锁
  }
}

​ 阻塞式获取队列中元素的方法, 注意和poll()、poll(time, unit)的区别, 这个阻塞是可中断的, 这点和poll(time, unit)一样, 如果队列没有元素会notEmpty.await阻塞, 当队列中有元素时会继续执行; 这里可以和入队操作相对应, 在入队时会调用enqueue, 如果入队成功会调用notEmpty.signal进行通知,唤醒take阻塞的线程继续执行。

dequeue

/**
 * Extracts element at current take position, advances, and signals.
 * Call only when holding lock.
 */
private E dequeue() {
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex]; // 默认获取下标为0的数据
    items[takeIndex] = null; // 将该位置的元素设置为null
    if (++takeIndex == items.length) // 当takeIndex == 数组长度是, 重置takeIndex, 避免数组越界
        takeIndex = 0;
    count--; // 数组数量减1
    if (itrs != null)
        itrs.elementDequeued(); // 更新迭代器中的元数据
    notFull.signal(); // 发送数组未满的信号, 让因为数组满而阻塞的线程可以继续执行添加操作
    return x;
}

​ dequeue是出队列的核心方法, 主要是删除队列头部元素,并返回给调用者, takeIndex记录获取数据的索引值,可以和putIndex进行比较。

​ itrs.elementDequeued, 更新迭代器中的元数据, 从前面阻塞方法类结构图可以看出, ArrayBlockingQueue的父类AbstractCollection含有抽象方法iterator, ArrayBlockingQueue对这个方法进行了实现, 所以实现了迭代器的功能。

三、原子操作类

​ 操作的原子性, 是指一个操作或者一系列操作要么同时成功, 要么同时失败, 不允许部分成功,部分失败,比如我们常见的 i++, 是一个非原子操作。

3.1 原子类的分类

  • 原子更新基本类型
    • AtomicBoolean、AtomicInteger、AtomicLong
  • 原子更新数组
    • AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
  • 原子更新引用
    • AtomicReference、AtomicReferenceFieldUpdater、AtomicMarkableReference
  • 原子更新字段
    • AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference

3.2 原理分析

unsafe

​ unsafe在很多场景中都有使用, 比如:

  • 多线程同步(MonitorEnter)
  • CAS操作(compareAndSwap)
  • 线程的挂起和恢复(park、unpark)
  • 内存屏障(loadFence、storeFence)
  • 原子类操作(getAndIncrement、getAndAdd)

​ 这里以AtomicInteger为例说明原子操作的实现原理, 原子操作的实现是基于unsafe来实现基本操作的, 具体查看下面内容

getAndIncrement

public final int getAndIncrement() {
  return unsafe.getAndAddInt(this, valueOffset, 1);
}

getAndAdd

public final int getAndAdd(int delta) {
  return unsafe.getAndAddInt(this, valueOffset, delta);
}

compareAndSet

public final boolean compareAndSet(int expect, int update) {
  return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章