阻塞队列BlockingQueue(ArrayBlockingQueue)源码分析

阻塞队列有多种实现,这里我们就先分析一下ArrayBlockingQueue,该队列获取数据、插入数据、检测数据的方式有多种,但是不同的方法会有不同的效果,比如插入数据时,如果队列已经满了,调用add方法插入会抛异常,但是是有put插入数据就会阻塞当前线程。具体的效果查看下面的图。
队列的基本方法
这里我们只是重点查看阻塞的方法,put和take两个方法,看他是怎么做到阻塞的。

1. 创建一个有界阻塞队列,长度为2

BlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(2);

2. 分析队列的构造函数

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();
}

capacity:队列长度
fair:是否为公平锁,默认为非公平锁
items:用来保存数据的数组,使用其它队列有其它的实现方式,比如链表
lock:锁,用来操作当前队列的锁
notEmpty:用来存储队列中没有数据时阻塞的线程
notFull:用来存储队列没有空间时阻塞的线程

lock.newCondition()是创建一个队列,对于不可进行争夺锁的线程阻塞放入到该队列中,后续可将该队列所有线程全部唤醒,放入到AQS队列,详细说明可查看ReentrantLock源码分析。

这里创建了两个等待队列,就是用来存放没有空间的阻塞队列和没有数据的阻塞队列

3.put方法插入数据

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();
    }
}

判断当前队列数据是否已经满了,如果满了则将当前线程进行阻塞,如果没满则进行设置值

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++;
    notEmpty.signal();
}

这里有个细节就是如果添加数据的指针到达数组的最后,则从数组的开头重新开始,数据插入的位置是由putIndex指针来标记的
当插入数据之后,唤醒等待获取队列数据的线程组,notEmpty.signal();

4.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];
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    notFull.signal();
    return x;
}

这里也有一个细节,当获取值是数组最后一位,将获取值的指针takeIndex设置到数组开头。最后会唤醒等待添加数据的线程组:notFull.signal();

5.总结

1.在队列中插入数据的位置和获取数据的位置都是通过两个指针下标来标记的,当指针到了数组的最后一位会将指针重置为0,也就是从头开始反复利用。

2. 每当插入数据后都会触发唤醒等待获取数据的线程组,相反的每当获取数据后都会触发唤醒等待插入数据的线程组。

猜你喜欢

转载自blog.csdn.net/weixin_33613947/article/details/116743066