阻塞队列
阻塞队列在生产者消费者场景中用的比较多。在java8中,JUC提供了7个阻塞队列。
类名 | 作用 |
ArrayBlockingQueue | 数组实现的有界阻塞队列, 此队列按照先进先出(FIFO)的原则对元素进行排序。 |
LinkedBlockingQueue | 链表实现的有界阻塞队列, 此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序 |
PriorityBlockingQueue | 支持优先级排序的无界阻塞队列, 默认情况下元素采取自然顺序升序排列。也可以自定义类实现 compareTo()方法来指定元素排规则,或者初始化 PriorityBlockingQueue 时,指定构造参数 Comparator 来对元素进行排序。 |
DelayQueue | 优先级队列实现的无界阻塞队列 |
SynchronousQueue | 不存储元素的阻塞队列, 每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。 |
LinkedTransferQueue | 链表实现的无界阻塞队列 |
LinkedBlockingDeque | 链表实现的双向阻塞队列 |
阻塞队列的操作方法
插入操作
add(e) :添加元素到队列中,如果队列满了,继续插入元素会报错,IllegalStateException。
offer(e) : 添加元素到队列,同时会返回元素是否插入成功的状态,如果成功则返回 true
put(e) :当阻塞队列满了以后,生产者继续通过 put添加元素,队列会一直阻塞生产者线程,直到队列可用
offer(e,time,unit) :当阻塞队列满了以后继续添加元素,生产者线程会被阻塞指定时间,如果超时,则线程直接退出
移除操作
remove():当队列为空时,调用 remove 会返回 false,如果元素移除成功,则返回 true
poll(): 当队列中存在元素,则从队列中取出一个元素,如果队列为空,则直接返回 null
take():基于阻塞的方式获取队列中的元素,如果队列为空,则 take 方法会一直阻塞,直到队列中有新的数据可以消费
poll(time,unit):带超时机制的获取数据,如果队列为空,则会等待指定的时间再去获取元素返回
ArrayBlockingQueue的原理分析
构造方法
ArrayBlockingQueue一共有三个构造函数。
capacity:表示数组的长度,也就是队列的长度。
fair:表示是否为公平的阻塞队列,默认情况下构造的是非公平的阻塞队列
c:表示给定数据初始化。
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
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();
}
public ArrayBlockingQueue(int capacity, boolean fair,
Collection<? extends E> c) {
this(capacity, fair);
final ReentrantLock lock = this.lock;
lock.lock(); // Lock only for visibility, not mutual exclusion
try {
int i = 0;
try {
for (E e : c) {
checkNotNull(e);
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
add方法
public boolean add(E e) {
//调用父类的add方法,也就是AbstractQuene
return super.add(e);
}
//AbstractQuene类的方法
public boolean add(E e) {
//调用off方法,最后回调ArrayBlockingQueue里的offer方法
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
public boolean offer(E e) {
checkNotNull(e); //判断添加的数据是否为空
final ReentrantLock lock = this.lock;
lock.lock(); //加锁
try {
if (count == items.length) // 判断队列长度,如果队列长度等于数组长度,表示满了直接返回 false
return false;
else {
enqueue(e); //调用 enqueue 将元素添加到队列中
return true;
}
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
// 当putIndex 等于数组长度时,将 putIndex 重置为 0
//因为是先进先出,所以队列中元素满了时,需要再从头往开始
if (++putIndex == items.length)
putIndex = 0;
count++; //记录队列元素的个数
notEmpty.signal();//唤醒处于等待状态下的线程,表示当前队列中的元素不为空,如果存在消费者线程阻塞,就可以开始取出元素
}
put方法
put 方法和 add 方法功能一样,差异是 put 方法如果队列满了,会阻塞。
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
//这个也是获得锁,但是和 lock 的区别是,这个方法优先允许在等待时由其他线程调用等待线程的 interrupt 方法来中断等待直接
//返回。而 lock方法是尝试获得锁成功后才响应中断
lock.lockInterruptibly();
try {
while (count == items.length)
//队列满了的情况下,当前线程将会被 notFull 条件对象挂起加到等待队列中
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
take方法
take方法是一种阻塞获取队列中元素的方法,它的实现原理很简单,有就删除没有就阻塞。这个阻塞是可以中断的,如果队列没有数据那么就加入 notEmpty条件队列等待(有数据就直接取走,方法结束),如果有新的put 线程添加了数据,那么 put 操作将会唤醒 take 线程,执行 take 操作
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
//如果队列为空的情况下,直接通过 await 方法阻塞,当有线程add或者put,则会通过notEmpty.signal()这个线程
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null; //将该位置的元素设置为空
//如果拿到数组的最后一个元素,那么重置为 0,继续从头部位置开始获取数据
if (++takeIndex == items.length)
takeIndex = 0;
count--; //队列元素个数递减
if (itrs != null)
itrs.elementDequeued(); //迭代器不为空,则更新迭代器中的数据
//唤醒因为队列满了,而被阻塞的写线程
notFull.signal();
return x;
}
remove方法
remove 方法是移除一个指定元素。
public boolean remove(Object o) {
if (o == null) return false;
final Object[] items = this.items;
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count > 0) {
final int putIndex = this.putIndex; //获取下一个要添加元素时的索引
int i = takeIndex;
do {
if (o.equals(items[i])) { //从takeIndex 下标开始,找到要被删除的元素
removeAt(i); //移除指定元素
return true;
}
if (++i == items.length)
i = 0;
} while (i != putIndex);
}
return false;
} finally {
lock.unlock();
}
}
ArrayBlockingQueue原理图解
当线程执行add或put方法时,会把当前元素放到items[putIndex]中,然后putIndex+1,count+1。当线程执行take方法时,会获取items[takeIndex],然后takeIndex+1,count-1.
原子操作类
所谓的原子性表示一个或者多个操作,要么全部执行完,要么一个也不执行。不能出现成功一部分失败一部分的情况。比如,i=0;执行i++一个分为三个步骤。1、拿到i的值、2、i+1,3、将结果赋给i。当10个线程并行执行i++时,结果可能小于10。这是因为,在执行步骤1时,可能有多个线程同时拿到的i值是1。这就是一个典型的原子性问题。可以通过给它加锁解决。而从JDK1.5开始,JUC提供了Atomic包,提供了对于常用数据结构的原子操作。它提供了简单、高效、以及线程安全的更新一个变量的方式。
由于变量类型的关系,在 JUC中提供了 12 个原子操作的类。这 12 个类可以分为四大类
1. 原子更新基本类型
AtomicBoolean、AtomicInteger、AtomicLong
2. 原子更新数组
AtomicIntegerArray 、 AtomicLongArray 、AtomicReferenceArray
3. 原子更新引用
AtomicReference 、 AtomicReferenceFieldUpdater 、AtomicMarkableReference(更新带有标记位的引用类型)
5. 原子更新字段
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference