非阻塞算法的实现ConcurrentLinkedQueue安全队列

非阻塞算法的实现ConcurrentLinkedQueue安全队列,也说明了阻塞算法实现的两种方式,使用一把锁(出队和入队同一把锁ArrayBlockingQueue)和两把锁(出队和入队各一把锁LinkedBlockingQueue)来实现,今天来探究下ArrayBlockingQueue。

  ArrayBlockingQueue是一个阻塞队列,底层使用数组结构实现,按照先进先出(FIFO)的原则对元素进行排序。

  ArrayBlockingQueue是一个线程安全的集合,通过ReentrantLock锁来实现,在并发情况下可以保证数据的一致性。

  此外,ArrayBlockingQueue的容量是有限的,数组的大小在初始化时就固定了,不会随着队列元素的增加而出现扩容的情况,也就是说ArrayBlockingQueue是一个“有界缓存区”。

  从下图可以看出,ArrayBlockingQueue是使用一个数组存储元素的,当向队列插入元素时,首先会插入到数组下标索引为6的位置,再有新元素进来时插入到索引为7的位置,依次类推,如果满了就不会再插入。

  当元素出队时,先移除索引为2的元素3,与入队一样,依次类推,移除索引3、4、5...上的元素。这也形成了“先进先出”。

二.源码解析

  1. 构造方法

    复制代码
    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:
        •  在公平锁中,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个锁,那么新发出的请求的线程将被放入到队列中;
        • 非公平锁中, 根据抢占机制,拥有锁的线程在释放锁资源的时候, 新发出请求的线程可以和等待队列中的第一个线程竞争锁资源, 新线程竞争失败才放入队列中,但是已经进入等待队列的线程, 依然是按照先进先出的顺序获取锁资源;
  2. 入队:有阻塞式和非阻塞式

    1. 阻塞式:当队列中的元素已满时,则会将此线程停止,让其处于等待状态,直到队列中有空余位置产生

      复制代码
      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)其他某个线程中断当前线程
    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开始)与数组长度相等时,下次我们就需要从数组头部开始添加了
    3. 阻塞式和非阻塞式的结合:offer(E e, long timeout, TimeUnit unit),向队列尾部添加元素,可以设置线程等待时间,如果超过指定时间队列还是满的,则返回false;

猜你喜欢

转载自www.cnblogs.com/kakarot/p/13191899.html
今日推荐