非阻塞算法的实现ConcurrentLinkedQueue安全队列,也说明了阻塞算法实现的两种方式,使用一把锁(出队和入队同一把锁ArrayBlockingQueue)和两把锁(出队和入队各一把锁LinkedBlockingQueue)来实现,今天来探究下ArrayBlockingQueue。
ArrayBlockingQueue是一个阻塞队列,底层使用数组结构实现,按照先进先出(FIFO)的原则对元素进行排序。
ArrayBlockingQueue是一个线程安全的集合,通过ReentrantLock锁来实现,在并发情况下可以保证数据的一致性。
此外,ArrayBlockingQueue的容量是有限的,数组的大小在初始化时就固定了,不会随着队列元素的增加而出现扩容的情况,也就是说ArrayBlockingQueue是一个“有界缓存区”。
从下图可以看出,ArrayBlockingQueue是使用一个数组存储元素的,当向队列插入元素时,首先会插入到数组下标索引为6的位置,再有新元素进来时插入到索引为7的位置,依次类推,如果满了就不会再插入。
当元素出队时,先移除索引为2的元素3,与入队一样,依次类推,移除索引3、4、5...上的元素。这也形成了“先进先出”。
二.源码解析
-
构造方法
public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable { //队列实现:数组 final Object[] items; //当读取元素时数组的下标(下一个被取出元素的索引) int takeIndex; //添加元素时数组的下标 (下一个被添加元素的索引) int putIndex; //队列中元素个数: int count; //可重入锁: final ReentrantLock lock; //入队操作时是否让线程等待 private final Condition notEmpty; //出队操作时是否让线程等待 private final Condition notFull; /** * 初始化队列容量构造:由于公平锁会降低队列的性能,因而使用非公平锁(默认)。 */ public ArrayBlockingQueue(int capacity) { this(capacity, false); } //带初始容量大小和公平锁队列(公平锁通过ReentrantLock实现): 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(); } }
-
在多线程中,默认不保证线程公平的访问队列;
-
在ArrayBlockingQueue中为了保证数据的安全,使用了ReentrantLock锁。由于锁的引入,导致了线程之间的竞争。当有一个线程获取到锁时,其余线程处于等待状态。当锁被释放时,所有等待线程为夺锁而竞争;
-
锁有公平锁和非公平锁:
-
公平锁:等待的线程在获取锁而竞争时,按照等待的先后顺序FIFO进行获取操作;公平锁可以应用在比如并发下的日志输出队列中,保证了日志输出的顺序完整性;
-
优点:等待锁的线程不会饿死,和非公平锁相比,在获得锁和保证锁分配的均衡性差异较小;
-
缺点:使用公平锁的程序在多线程访问时表现为很低的吞吐量(即速度很慢),等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁的大;公平锁不能保证线程调度的公平性,因此,使用公平锁的众多线程中的一员可能获得多倍的成功机会,这种情况发生在其他活动线程没有被处理并且目前并未持有锁时【ReentrantLock源码对公平锁的定义】;
Note however, that fairness of locks does not guarantee fairness of thread scheduling. Thus, one of many threads using a fair lock may obtain it multiple times in succession while other active threads are not progressing and not currently holding the lock.
-
上面这句话有重入锁的概念,一个线程可以在已经获取锁的情况下再次进入获取到锁,不需要竞争;同时,如果一个线程获取到了锁,然后释放,在其他线程来获取之前再次是可以获取到锁的。
A: Request Lock -> Release Lock -> Request Lock Again (Succeeds) B: Request Lock (Denied)... ----------------------- Time --------------------------------->
-
-
-
非公平锁:在获取锁时,无论是先等待还是后等待的线程,均有可能获取到锁。即根据抢占机制,是随机获取锁的,和公平锁不一样的是先来的不一定能获取到锁,有可能一直拿不到锁,这样会造成“饥饿”现象;
-
优点:非公平锁性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟,而且,非公平锁更能充分的利用CPU的时间片,尽量减少CPU空闲的状态时间;即可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获取到锁,CPU不必唤醒其他所有线程;
-
缺点:处于等待队列中的线程可能会饿死或者等很久才会获得锁;
-
-
产生“饥饿”的原因:
-
高优先级吞噬所有低优先级的CPU时间片,优先级越高,就会获得越高的CPU执行机会; ---> 使用默认的优先级;
-
线程被永久阻塞在一个等待进入同步块synchronized的状态(长时间执行) ,同时synchronized并不保障等待线程的顺序(锁释放后,随机竞争,由OS调度),这会存在一个可能是某个线程总是抢锁抢不到导致一直等待状态 ---> 避免持有锁的线程长时间执行、使用显示lock来代替synchronized;
synchronized(obj) { while (true) { // .... infinite loop }
-
等待的线程永远不被唤醒:如果多个线程处在wait方法执行上,而对其调用notify方法不会保证哪一个线程会获得唤醒,唤醒是无序的,跟VM/OS调度有关,甚至底层是随机选取一个或是队列中的第一个,任何线程都有可能处于继续等待的状态,因此存在这样一个风险,即一个等待线程从来得不到唤醒,因为其他等待线程总是能被获得唤醒 ---> 使用显示lock来代替synchronized;
-
-
比如ReentrantLock:
-
在公平锁中,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个锁,那么新发出的请求的线程将被放入到队列中;
-
非公平锁中, 根据抢占机制,拥有锁的线程在释放锁资源的时候, 新发出请求的线程可以和等待队列中的第一个线程竞争锁资源, 新线程竞争失败才放入队列中,但是已经进入等待队列的线程, 依然是按照先进先出的顺序获取锁资源;
-
-
-
-
入队:有阻塞式和非阻塞式
-
阻塞式:当队列中的元素已满时,则会将此线程停止,让其处于等待状态,直到队列中有空余位置产生
public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly();//获取锁 try { //队列中元素 == 数组长度(队列满了),则线程等待 while (count == items.length) notFull.await(); enqueue(e);//元素加入队列 } finally { lock.unlock();//释放锁 } }
-
lockInterruptibly:
-
如果当前线程未被中断,则获取锁。
-
如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。
-
如果当前线程已经保持此锁,则将保持计数加 1,并且该方法立即返回。
-
如果锁被另一个线程保持,则出于线程调度目的,禁用当前线程,并且在发生以下两种情况之一以前,该线程将一直处于休眠状态:1)锁由当前线程获得;2)其他某个线程中断当前线程
-
-
-
非阻塞式:当队列中的元素已满时,并不会阻塞此线程的操作,而是让其返回又或者是抛出异常
public boolean add(E e) { return super.add(e);// AbstractQueue.add } public boolean add(E e) { 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 { //如果队列满了,返回false if (count == items.length) return false; else { //元素加入队列 enqueue(e); return true; } } finally { lock.unlock();//释放锁 } } 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();//唤醒出队上的等待线程,表示有元素可以消费了 }
-
enqueue中++putIndex == items.length,putIndex=0:这是因为当前队列执行元素出队时总是从队列头部获取,而添加元素的索引从队列尾部获取所以当队列索引(从0开始)与数组长度相等时,下次我们就需要从数组头部开始添加了
-
-
阻塞式和非阻塞式的结合:offer(E e, long timeout, TimeUnit unit),向队列尾部添加元素,可以设置线程等待时间,如果超过指定时间队列还是满的,则返回false;
-