Java并发容器——阻塞队列源码分析、生产者消费者模式、CopyOnWriteArrayList、ConcurrentLinkedQueue类总结

版权声明:个人博客:blog.suyeq.com 欢迎访问 https://blog.csdn.net/hackersuye/article/details/85071647

    Java在jdk1.6引入了并发容器阻塞队列,阻塞队列是一个统称,凡是实现了BlockingQueue接口的容器都可以称的上阻塞队列,它实现了生产者-消费者模式,在阻塞队列中出队列与入队列都有两种形式,阻塞的put与take方法以及定时获取的offer与poll方法,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)
    int remainingCapacity();
    boolean remove(Object o);
    public boolean contains(Object o);
   //......
}

    主要的阻塞队列实现类如图:
在这里插入图片描述

    J.U.C中的并发容器用来替代Collections类中创建的同步容器,相较于同步队列,并发容器有着更好的锁粒度以及更好的在多线程环境下的并发策略,所以并发容器的并发性能都会比同步容器要好很多。

ArrayBlockingQueue

    ArrayBlockingQueue类是阻塞队列的Array实现,它是有界的,它在并发情况下的策略是持有一个ReetrantLock锁,并用Condition持有两个等待队列来实现阻塞,一个负责管理空的情况,一个负责管理满的情况:

	final ReentrantLock lock;
    private final Condition notEmpty;
    private final Condition notFull;

    之所以称它是一个有界的队列,是因为它的大小在创建时已经固定,不能改变了:

 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 void put(E e) throws InterruptedException {
		//检查值部位空
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        //获取全局锁
        lock.lockInterruptibly();
        try {
        	//当队列已满,调用await方法陷入等待
            while (count == items.length)
                notFull.await();
            //没有满,则入队列
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

    从源码中看出,当队列中已满的情况下,会调用await方法让当前线程陷入等待,并释放锁,只有其它线程取出了队列中的元素,并唤醒因队列满情况而陷入等待的线程,才会让元素入队列。阻塞的take出队方法与入队刚好相反,源码如下:

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

    当队列为空的时候会阻塞当前线程,dequeue与enqueue方法里会调用signal方法唤醒对应的阻塞线程。还提供了一组入队以及出队的方法来灵活的处理入队以及出队:

public boolean offer(E e) {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }
    
public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return (count == 0) ? null : dequeue();
        } finally {
            lock.unlock();
        }
    }

    当队列为空或者已满的情况下,都会立马返回,而不会陷入等待,同时这两个方法还有等待时间的重载方法,当等待一定时间后,如果没有得到想要的结果,那么就会返回失败的结果。

LinkedBlockingQueue

    LinkedBlockingQueue是阻塞队列的链表实现,它在内部持有两个锁,一个入队锁,一个出队锁,并使用原子变量来记录队列的长度,它也是个有界队列:

private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();

    虽然是用链表实现的,但是在创建LinkedBlockingQueue类时,必须规定队列的大小,而且不能更改:

扫描二维码关注公众号,回复: 5761249 查看本文章
public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

    入队以及出队的原理与ArrayBlockingQueue类原理是一样的,但是由于入队锁以及出队锁的分离,可以实现入队和出队的同时执进行,这样就提高了性能效率。因为出队以及入队的操作并不是在同一个地方,入队在队尾操作,出队在队头进行,所以两者是影响不大的。所以一般推荐使用LinkedBlockingQueue类,因为并发效率高。

其它

    PriorityBlockingQueue类是优先级队列,如果你不想按照先进先去的方式排列,你可以使用这个类,当然进入该队列的要实现Comparable接口或者Comparator接口。

    LinkedBlockingDeque类则是双端LinkedBlockingQueue类的实现,可以在两头入队和出队操作,实现的原理是一样的。

    CopyOnWriteArrayList类与CopyOnWriteArraySet类保持同步的操作是,发布一个不变的对象,意思是说,如果有插入的操作,那么利用锁来同步重新构建一个内部数组,只要内部数组不变那么在并发的情况下访问到的数据就是正确的。但是缺点就是每次改变数据都要在内部构建一个新的数组,降低了效率,所以这两个容器适用于读操作特别多,但是写入操作很少的场景。

    ConcurrentLinkedQueue与ConcurrentLinkedDeque类内部采用CAS原子操作来保证原子性。

猜你喜欢

转载自blog.csdn.net/hackersuye/article/details/85071647
今日推荐