队列BlockingQueue
线程池中主要有几大队列
一.LinkedBlockingQueue
基于链表实现的可选容量的阻塞队列,元素从队尾插入,初始化时可以传入队列容量,不传就是Interger的最大值,也就是无界队列,内部应该是单链表
和一般的队列区别是一定至少有一个节点,头节点是不存元素的,尾节点是可以存数据的,注意,初始化的时候,头节点和尾节点是同一个,内部维持了两把锁,一把入队锁,一把出队锁用来保证线程安全,同一时刻只能有一个线程执行入队,同一时刻只能有一个线程执行出队,但是入队和出队是可以同时执行的,在内部用AtomicInterger类型变量标识当前队列中的元素个数,确保两个线程操作底层队列是线程安全的
主要变量:
/** The capacity bound, or Integer.MAX_VALUE if none */
private final int capacity; //队列容量
/** Current number of elements */ //队列大小
private final AtomicInteger count = new AtomicInteger();
/**
* Head of linked list.
* Invariant: head.item == null
*/
transient Node head; //队列头元素,为空,初始的时候,头节点和尾节点是一个
/**
* Tail of linked list.
* Invariant: last.next == null
*/
private transient Node last; //队列尾元素
/** 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(); //插入队列条件,通知进队插元素的线程
put(T)插入元素
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 node = new Node(e); //创建节点
final ReentrantLock putLock = this.putLock; //获取到插入锁
final AtomicInteger count = this.count; //当前队列的元素个数
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
while (count.get() == capacity) { //判断队列长度满了没
notFull.await(); //当前队列满了,加入条件等待队列中,等待入队
}
enqueue(node); //插入元素
c = count.getAndIncrement(); //将count+1,但是返回的是+1之前的数据
if (c + 1 < capacity) //允许插入
notFull.signal(); //唤醒等待的入队线程
} finally {
putLock.unlock(); //解锁
}
if (c == 0) //这个是插入之前的元素个数
signalNotEmpty(); //通知出队线程,队列非空,唤醒等待取元素的线程
}
插入的流程:
1、线程T先获取到入队锁,获取当前队列中元素的个数
如果当前队列满了,调用notFull.await将线程T加入到Condition的等待队列中,并且await会释放掉线程T获得的入队锁,此时如果还有其他的线程也要入队,那么其他的线程会获取到这个入队锁,当然此时仍然可能队列中的元素是满的,所以可能会重复线程T的操作进入Condition等待队列
如果队列还有空,执行元素插入,并获取此次插入之前队列的元素个数,如果本次插入之后还是可以插入,那么就唤醒Condition中等待的线程队列,我插完了,还能插
释放入队锁,方便让其他入队线程获得入队锁进行数据插入
如果在本次插入之前队列中的元素个数为0,那么可能会存在因为数据为空而在等待的出队线程,得唤醒他们
通过Condition的唤醒,是有序唤醒,也就是说唤醒的是第一个等待的线程
T take()从队列中获取元素
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(); //去Condition等待吧,并且释放出队锁
}
x = dequeue(); //拿到队列中的第一个元素
c = count.getAndDecrement(); //将count-1,也就是取了一个元素,但是这里返回的是-1之前的数据
if (c > 1) //取完这个数据后,队列中还有元素
notEmpty.signal(); //那不得唤醒其他在等待中的出队线程,一起来拿啊
} finally {
takeLock.unlock(); //释放锁
}
if (c == capacity) //如果在本次取值之前队列中的元素个数是满的,那也可能存在有入队的线程因为队列满了而进入等待
signalNotFull(); //那不得唤醒入队的好兄弟,赶紧插啊
return x;
}
取数据流程:
线程T先获取队列中得元素个数,再拿到出队锁
如果队列中没有元素,那就就T放入Condition队列中去等待唤醒
如果队列中还有数据,那取吧,然后获取取完后队列长度,如果还有数据,唤醒其他Condition中等待取数据的线程,来拿数据
释放出队锁,给别的好兄弟
出队前的队列中数据的个数如果和最大值是相等的,那么可能存在因为队列满了而进入等待的入队线程,唤醒他
remove从队列中删除数据
public boolean remove(Object o) {
if (o == null) return false; //元素不存在
fullyLock(); //删除必须同时获取入队锁和出队锁
try {
for (Node trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
if (o.equals(p.item)) {
unlink(p, trail); //解除此节点的前后联系,因为是单链表,所以只需要将preNode.next->p.next就可以了
return true;
}
}
return false;
} finally {
fullyUnlock(); //释放锁
}
}
删除数据一定要两把锁都拿到
LinkedBlockingQueue总结:
同时段允许两个线程在两端进行入队或出队操作,但是一端同时刻只能有一个线程进行操作,为了维护底部数据的统一,使用AtomicInterger类型的count变量,表示队列中元素的个数,count只能在两个地方变化,入队的时候+1,出队的时候-1,AtomicInterger是原子安全的,所以确保了底层队列的数据同步
二 .ArrayBlockingQueue
底层基于数组实现的队列,构造时必须指定容量,一旦指定容量不能改变,是一个容量限制的阻塞队列
主要变量
/** The queued items */
final Object[] items; //数组数据
/** items index for next take, poll, peek or remove */
int takeIndex; //取数据的位置
/** items index for next put, offer, or add */
int putIndex; //存数据位置
/** Number of elements in the queue */
int count; //下标
/*
* Concurrency control uses the classic two-condition algorithm
* found in any textbook.
*/
/** Main lock guarding all access */
final ReentrantLock lock; //锁,只有一把锁
/** Condition for waiting takes */
private final Condition notEmpty; //取数据的等待条件
/** Condition for waiting puts */
private final Condition notFull; //存数据的等待条件
put(T)插入元素
public void put(E e) throws InterruptedException {
Objects.requireNonNull(e); //不允许插入空元素,直接抛出遗产
final ReentrantLock lock = this.lock; //获取锁,整个队列只有一把锁
lock.lockInterruptibly(); //上锁
try {
while (count == items.length) //元素的长度等于队列的总长度,那就是存满了
notFull.await(); //当前线程阻塞,直到有位置插入元素
enqueue(e); //队列没有满,进行进栈
} finally {
lock.unlock(); //解锁
}
}
插入数据时元素不能为空,为空直接抛出异常,先获取队列锁,只有一把锁,判断队列是否满了,满了就调用await()释放锁,并进入阻塞状态,没满就插入元素
插入元素
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items; //当前队列存数据的数组
items[putIndex] = x; //插入数据的下标
if (++putIndex == items.length) putIndex = 0; //判断数据是否插满
count++; //元素个数+1
notEmpty.signal(); //唤醒可能存在因存数据而等待的取数据线程
}
T take()取数据
take取走队头的元素,当队列为空时,就会阻塞,直到队列中有元素时被取出来
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock; //获取锁
lock.lockInterruptibly(); //上锁
try {
while (count == 0) //队列中没有元素
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]; //从takeIndex下标取数据,一般是0,因为取数据是从头开始取
items[takeIndex] = null; //将取过数据的位置null
if (++takeIndex == items.length) takeIndex = 0; //取完当前数据后,判断是否还有数据
count--; //元素个数 -1
if (itrs != null)
itrs.elementDequeued();
notFull.signal(); //唤醒元素满了而阻塞的插入数据线程
return x;
}
ArrayBlockingQueue总结:
ArrayBlockingQueue内部是通过ReentrantLock和Condition来实现,内部只有一把锁,意味着同一时刻只有一个线程能进行入队或出队的操作
总结:
ArrayBlockingQueue,内部是数据+一把锁+两个条件,入队和出队都是同一把锁,在出队高并发或入队高并发的情况下,使用此阻塞队列,操作数据,且不需要扩容,性能高
LinkedBlockingQueue,内部是单链表+两把锁+两个条件,在出队和入队同时高并发的情况下,使用此阻塞队列,避免入队和出队竞争锁,且链表的长度为最大,容量无限