Executor、future学习(四)

队列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先获取到入队锁,获取当前队列中元素的个数

  1. 如果当前队列满了,调用notFull.await将线程T加入到Condition的等待队列中,并且await会释放掉线程T获得的入队锁,此时如果还有其他的线程也要入队,那么其他的线程会获取到这个入队锁,当然此时仍然可能队列中的元素是满的,所以可能会重复线程T的操作进入Condition等待队列

  1. 如果队列还有空,执行元素插入,并获取此次插入之前队列的元素个数,如果本次插入之后还是可以插入,那么就唤醒Condition中等待的线程队列,我插完了,还能插

  1. 释放入队锁,方便让其他入队线程获得入队锁进行数据插入

  1. 如果在本次插入之前队列中的元素个数为0,那么可能会存在因为数据为空而在等待的出队线程,得唤醒他们

  1. 通过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;
    }

取数据流程:

  1. 线程T先获取队列中得元素个数,再拿到出队锁

  1. 如果队列中没有元素,那就就T放入Condition队列中去等待唤醒

  1. 如果队列中还有数据,那取吧,然后获取取完后队列长度,如果还有数据,唤醒其他Condition中等待取数据的线程,来拿数据

  1. 释放出队锁,给别的好兄弟

  1. 出队前的队列中数据的个数如果和最大值是相等的,那么可能存在因为队列满了而进入等待的入队线程,唤醒他

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来实现,内部只有一把锁,意味着同一时刻只有一个线程能进行入队或出队的操作

总结:

  1. ArrayBlockingQueue,内部是数据+一把锁+两个条件,入队和出队都是同一把锁,在出队高并发或入队高并发的情况下,使用此阻塞队列,操作数据,且不需要扩容,性能高

  1. LinkedBlockingQueue,内部是单链表+两把锁+两个条件,在出队和入队同时高并发的情况下,使用此阻塞队列,避免入队和出队竞争锁,且链表的长度为最大,容量无限

你可能感兴趣的:(学习记录,学习)