【J.U.C-Collections】并发集合类与实现原理——BlockingQueue

阻塞队列通过Lock锁来实现:不满足条件时线程被阻塞,满足条件自动唤醒。
接口常见的两个实现类与原理:
ArrayBlockingQueue数组类型:一个lock锁+两个Condition条件+环形数组结构
LinkedBlockingQeque链表类型:两个lock锁+两个Condition条件+单向链表

ArrayBlockingQueue中Condition的判断条件依赖于三个int变量:节点数量count+下一个操作的位置标记putIndex、takeIndex。
可以把这个数组看做一个环形,最大index的下一个又回到0;
但是因为只有一把lock锁,入队与出队不能同时进行。

LinkedBlockingQeque因为入队出队可以同步进行,所以有两个不同点:
记录节点数的变量count是AtomicInteger类型的;
入队后先尝试唤起其他的入队线程,再唤醒出队线程。因为直接唤醒出队的话,出队的这段时间,入队锁就是空闲的。

BlockingQueue阻塞队列

  • 当队列为空时,取元素的操作将被阻塞;
  • 当队列为满时,添加元素的操作将被阻塞;
  • 也就是说,在不满足条件的情况下,当前线程会 被阻塞,直到满足条件时会被自动唤起。通常使用 来实现。

上篇中也列出了阻塞队列所提供的几类接口,对比于一般的队列,增加了阻塞线程的接口

  • put、take:这两个方法会一直 阻塞调用线程 ,直到线程被中断或者队列状态可用;
  • offer、poll:这两个方法会限时阻塞调用线程,直到线程被中断或者队列状态可用或者超时;
操作抛出异常返回特数值一直阻塞超时退出
插入add(e)offer(e)put(e)offer(e,time,unit)
删除remove()polltake()poll(time,unit)
检查element()peek--

BlockingQueue接口

public interface BlockingQueue<E> extends Queue<E> {

	boolean add(E e);

	boolean offer(E e);
 
 	void put(E e) throws InterruptedException;

    boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

    E take() throws InterruptedException;

    E poll(long timeout, TimeUnit unit)
        throws InterruptedException;

    int remainingCapacity();

    boolean remove(Object o);

    public boolean contains(Object o);

    int drainTo(Collection<? super E> c);
    
    int drainTo(Collection<? super E> c, int maxElements);
}

BlockingQueue实现生产者-消费者模式

public class TestBlockingQueue {

    public static void main(String[] args) throws Exception {

        BlockingQueue<String> queue = new ArrayBlockingQueue(10);

        Producer producer = new Producer(queue);
        Consumer consumer = new Consumer(queue);

        new Thread(producer).start();
        new Thread(consumer).start();

    }

}

class Producer implements Runnable {

    protected BlockingQueue<String> queue;

    public Producer(BlockingQueue queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            queue.put("1");
            Thread.sleep(1000);
            queue.put("2");
            Thread.sleep(1000);
            queue.put("3");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


class Consumer implements Runnable {

    protected BlockingQueue<String> queue;

    public Consumer(BlockingQueue queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            System.out.println(queue.take());
            System.out.println(System.currentTimeMillis());

            System.out.println(queue.take());
            System.out.println(System.currentTimeMillis());

            System.out.println(queue.take());
            System.out.println(System.currentTimeMillis());

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果如下,可见Consumer线程被阻塞

1
1608090320376
2
1608090321380
3
1608090322384

【ArrayBlockingQueue】实现类

1.存储结构与构造函数

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
	/** 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;
    /** Main lock guarding all access */
    final ReentrantLock lock;
    /** Condition for waiting takes */
    private final Condition notEmpty;
    /** Condition for waiting puts */
    private final Condition notFull;
}
  • ArrayBlockingQueue是通过数组来实现元素的存储,实际存储结构为Object[] items;
public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }
    
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();
    }
public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection<? extends E> c) {
        this(capacity, fair);

        final ReentrantLock lock = this.lock;
        lock.lock(); // Lock only for visibility, not mutual exclusion
        try {
            int i = 0;
            try {
                for (E e : c) {
                    checkNotNull(e);
                    items[i++] = e;
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                throw new IllegalArgumentException();
            }
            count = i;
            putIndex = (i == capacity) ? 0 : i;
        } finally {
            lock.unlock();
        }
    }

指定队列初始化大小:

  • 创建一个指定大小的Object数组;
  • 创建两个锁:notEmpty、notFull

指定队列大小、和初始集合:

  • 创建一个指定大小的Object数组;
  • 在循环给数组赋值之前先加锁;
  • 这种形式会抛出异常的两种情况:capacity<c.size、集合c中有null元素;

所以BlockQueue中不能含有null元素。

2.【功能实现】——入队

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();
    }
  1. 加锁;
  2. 如果队列已满,也就是while循环中count == items.length,notFull.await()一直等待;
  3. 队列未满,直接入队;
    3.1 putIndex记录了 下一个入队的index,将数组的此位置赋值;
    3.2 putIndex+1、count+1;
    3.3 如果此时队列已经满了,那么将putIndex重置为0;
    3.4 notEmpty.signal()唤醒一个notEmpty的等待线程;
  4. 释放锁;

3.【功能实现】——出队

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;
    }
  1. 加锁;
  2. 如果队列为空,也就是while循环中count == 0,notEmpty.await()一直等待;
  3. 队列不为空,直接出队;
    3.1 takeIndex记录了下一个出队的index,返回数组的此位置元素,并将数组的index位置设为null;
    3.2 takeIndex+1、count-1;
    3.3 如果此时++takeIndex == items.length,表示下一个要拿出的元素是数组中最后一个位置,那么将takeIndex重置为0;
    3.4 notFull.signal()唤醒一个notFull的等待线程;
  4. 释放锁;

总结1.环形数组结构

Q:putIndex、takeIndex为什么需要重置为0?

总结2.入队出队不能同时进行

  • ArrayBlockQueue内部只维护了一把全局锁,意味着生产者和消费者的活动不能同时进行。

【LinkedBlockingQeque】实现类

1.存储结构与构造函数

public class LinkedBlockingQeque<E>
    extends AbstractQueue<E>
    implements BlockinQqeque<E>, java.io.Serializable {
    /** 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<E> head;
    /**
     * Tail of linked list.
     * Invariant: last.next == null
     */
    private transient Node<E> 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();
}
static class Node<E> {
        E item;
        
        Node<E> next;

        Node(E x) { item = x; }
    }
  • 因为读写操作可以同时进行,两把锁的情况下并不能保证count的原子性,所以队列存储count使用的是原子类AtomicInteger;
  • LinkedBlockingQueue是通过Node节点来存储元素,实际结构是一个 单向链表
public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }
public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
    }
 public LinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
        final ReentrantLock putLock = this.putLock;
        putLock.lock(); // Never contended, but necessary for visibility
        try {
            int n = 0;
            for (E e : c) {
                if (e == null)
                    throw new NullPointerException();
                if (n == capacity)
                    throw new IllegalStateException("Queue full");
                enqueue(new Node<E>(e));
                ++n;
            }
            count.set(n);
        } finally {
            putLock.unlock();
        }
    }

不指定队列的大小:

  • 将队列容量设为Integer.MAX_VALUE

指定了队列的大小:

  • 赋值capacity容量值;

通过已有集合赋值:

  • 设置容量为Integer.MAX_VALUE
  • 在循环在链表的最后加元素之前加锁;
  • 这种形式会抛出异常的两种情况:capacity<c.size、集合c中有null元素;

2.【功能实现】——入队

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<E> node = new Node<E>(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();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }
  1. 创建一个Node节点;
  2. 获取putLock,加锁;
  3. 如果队列已满,也就是while循环中count.get() == capacity,notFull.await()一直等待;
  4. 队列未满,直接入队, count+1
  5. 如果入队以后队列未满, 唤起一个入队线程
  6. 释放锁
  7. 判断入队之前的count是否为0,表示队列是空的,那么可能存在出队线程正在等待, 唤起一个出队线程

3.【功能实现】——出队

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();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }
  1. 创建一个Node节点;
  2. 获取takeLock,加锁;
  3. 如果队列为空,也就是while循环中count.get() == 0,notEmpty.await()一直等待;
  4. 队列不为空,直接出队, count-1
  5. 如果入队以后队列不为空, 唤起一个出队线程
  6. 释放锁
  7. 判断出队之前的队列是否已满,那么可能存在入队线程正在等待, 唤起一个入队线程

总结1.近似有界阻塞队列

  • 队列在初始化时,既可以指定大小,也可以不指定。如果不指定的话,默认为Integer.MAX_VALUE,近似于无界;

总结2.入队锁出队锁分离

  • 队列维护了两把锁,一把入队时获取,一把出队时获取,使得两个操作可以同时进行;

总结3.为什么入队后先唤起入队线程,再唤起出队线程?

  • 根据代码的流程来看,当一个元素入队之后:
  1. 先判断是否还满足入队的条件,如果满足,唤起一个入队线程;
  2. 判断是否满足出队的条件,如果满足,唤起一个出队线程;
  • 这个过程与ArrayBlockingQueue相对比:
  1. 入队之后直接唤起一个出队线程
  • 为什么LinkedBlockingQueue入队后,不直接唤起一个出队线程,然后等出队线程结束的时候,自动的唤起下一个入队线程呢?
    因为LinkedBlockingQueue的入队和出队操作是可以同时进行的,如果按照上述描述,那么出队线程执行过程这段时间,入队锁就是空闲的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值