java.util.concurrent 包 的LinkedBlockingQueue学习

LinkedBlockingQueue是一个用于并发环境下的阻塞队列集合类,它可以用于生产-消费者模型。

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

阻塞队列提供了四种处理方法:

方法\处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入 add(e) offer(e) put(e) offer(e,time,unit)
移除 remove() poll() take() poll(time,unit)
检查 element() peek() 不可用 不可用
  • 出异常:是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException("Queue full")异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException异常 。
  • 返回特殊值:插入方法会返回是否成功,成功则返回true。移除方法,则是从队列里拿出一个元素,如果没有则返回null
  • 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列可用。
  • 超时退出:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出

多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue。(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒)

java.util.concurrent 包 的LinkedBlockingQueue学习_第1张图片

ArrayBlockingQueue

基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行。

LinkedBlockingQueue

基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

.
.
.
/** 通过 take 取出进行加锁、取出 */
private final ReentrantLock takeLock = new ReentrantLock();
/** 等待中的队列等待取出 */
private final Condition notEmpty = takeLock.newCondition();
/*通过 put 放置进行加锁、放置*/
private final ReentrantLock putLock = new ReentrantLock();
/** 等待中的队列等待放置 */
private final Condition notFull = putLock.newCondition();
/* 记录集合中的个数(计数器) */
private final AtomicInteger count = new AtomicInteger(0);
.
.
.
//队列初始容量,Integer 最大值
public static final int MAX_VALUE = 0x7fffffff;
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}
/**
 * 默认队列容量为 0x7fffffff;用户也可以自己指定容量。
 */
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    //初始化首尾节点
    last = head = new Node(null);
}

Offer 操作-生产者

public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException {
    //空元素抛空指针异常
    if (e == null) throw new NullPointerException();
    //换算时间
    long nanos = unit.toNanos(timeout);
    int c = -1;
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    //获取可被中断锁,只有一个线程可获取
    putLock.lockInterruptibly();
    try {
        //如果队列满则进入循环
        while (count.get() == capacity) {
            //nanos<=0 直接返回
            if (nanos <= 0)
                return false;
            //否者调用 await 进行等待,超时则返回<=0
            nanos = notFull.awaitNanos(nanos);
        }
        //await 在超时时间内返回则添加元素
        enqueue(new Node(e));
        //getAndIncrement方法返回值是改动前的值,与incrementAndGet方法不同(返回改动后的值)
        c = count.getAndIncrement();
        //c+1是元素添加后总个数;队列不满则激活其他等待入队线程
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        //释放锁
        putLock.unlock();
    }
    //当 c==0 说明队列里面有一个元素,这时候唤醒出队线程
    if (c == 0)
        signalNotEmpty();
    return true;
}
//添加元素到末尾
private void enqueue(Node node) {
    last = last.next = node;
}
//唤醒出队线程
private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
}

 poll 操作-消费者

获取并移除队首元素,在指定的时间内去轮询队列看有没有首元素有则返回,否者超时后返回 null。

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    E x = null;
    int c = -1;
    long nanos = unit.toNanos(timeout);
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    //出队线程获取独占锁
    takeLock.lockInterruptibly();
    try {
        //循环直到队列不为空
        while (count.get() == 0) {
            //超时直接返回 null
            if (nanos <= 0)
                return null;
            nanos = notEmpty.awaitNanos(nanos);
        }
        //出队,计数器减一
        x = dequeue();
        c = count.getAndDecrement();
        //如果出队前队列不为空则发送信号,激活其他阻塞的出队线程
        if (c > 1)
            notEmpty.signal();
    } finally {
        //释放锁
        takeLock.unlock();
    }
    //当前队列容量为最大值-1 则激活入队线程。
    if (c == capacity)
        signalNotFull();
    return x;
}
 
//激活入队线程
private void signalNotFull() {
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        notFull.signal();
    } finally {
        putLock.unlock();
    }
}

首先获取独享锁,然后进入循环当当前队列有元素才会退出循环,或者超时了,直接返回 null。

超时前退出循环后,就从队列移除元素,然后计数器减去一,如果减去 1 前队列元素大于 1 则说明当前移除后队 列还有元素,那么就发信号激活其他可能阻塞到当前条件信号的线程

最后如果减去 1 前队列元素个数=最大值,那么移除一个后会腾出一个空间来,这时候可以激活可能存在的入队阻塞线程

 

put 操作-生产者

与带超时时间的 poll 类似不同在于 put 时候如果当前队列满了,线程会进入线程等待状态挂起(notFull.await();)它会一直等待其他线程调用 notFull.signal 才会被 唤醒

take 操作-消费者

与带超时时间的 poll 类似不同在于 take 时候如果当前队列空了,线程会进入线程等待挂起状态(notEmpty.await()),它会一直等待其他线程调用 notEmpty.signal()才 会被唤醒

size 操作-消费者

当前队列元素个数,如代码直接使用原子变量 count 获取。

public int size() {
    return count.get();
}

peek 操作

获取但是不移除当前队列的头元素,没有则返回 null。

public E peek() {
    //队列空,则返回 null
    if (count.get() == 0)
        return null;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        Node first = head.next;
        if (first == null)
            return null;
        else
            return first.item;
    } finally {
        takeLock.unlock();
    }
}

 remove 操作

删除队列里面的一个元素,有则删除返回 true,没有则返回 false,在删除操作时候由于要遍历队列所以加了双重 锁,也就是在删除过程中不允许入队也不允许出队操作。

public boolean remove(Object o) {
    if (o == null) return false;
        //双重加锁
        fullyLock();
    try {
        //遍历队列找则删除返回 true
        for (Node trail = head, p = trail.next; p != null;trail = p, p = p.next) {
            if (o.equals(p.item)) {
                unlink(p, trail);
                return true;
            }
        }
        //找不到返回 false
        return false;
    } finally {
        //解锁
        fullyUnlock();
    }
}
//获取锁
void fullyLock() {
    putLock.lock();
    takeLock.lock();
}
//释放锁
void fullyUnlock() {
    takeLock.unlock();
    putLock.unlock();
}
//删除元素
void unlink(Node p, Node trail) {
    p.item = null;
    trail.next = p.next;
    if (last == p)
        last = trail;
    //如果当前队列满,删除后,也不忘记最快的唤醒等待的线程
    if (count.getAndDecrement() == capacity)
        notFull.signal();
}
 

并发库中的 BlockingQueue 是一个比较好玩的类,顾名思义,就是阻塞队列。该类主要提供了两个方法 put()和 take(),前者将一个对象放到队列中,如果队列已经满了,就等待直到有空闲节点;后者从 head 取一个对象,如果没 有对象,就等待直到有可取的对象。

下面的例子比较简单,一个读线程,用于将要处理的文件对象添加到阻塞队列中, 另外四个写线程用于取出文件 对象,为了模拟写操作耗时长的特点,特让线程睡眠一段随机长度的时间。另外,该 Demo 也使用到了线程池和原子 整型 (AtomicInteger),AtomicInteger 可以在并发情况下达到原子化更新,避免使用了 synchronized,而且性能 非常高。由 于阻塞队列的 put 和 take 操作会阻塞,为了使线程退出,特在队列中添加了一个“标识”,算法中也叫 “哨兵”,当发现这个哨兵后,写线程就退出。

package com.sendbp.eduz.concurrent.linkedBlockingQueue;
 
 
import java.io.File;
import java.io.FileFilter;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
 
/**
 * @author: JC.Lin
 * @data: 2019/2/23 15:28
 */
public class LinkedBlockingQueueTest {
    /**
     * 随机数
     */
    static long randomTime() {
        return (long) (Math.random() * 1000);
    }
 
    public static void main(String[] args) {
        // 能容纳 100 个文件
        final BlockingQueue queue = new LinkedBlockingQueue(100);
        // 线程池
        final ExecutorService exec = Executors.newFixedThreadPool(5);
        final File root = new File("D:\\war");
        // 完成标志
        final File exitFile = new File("");
        // 读个数
        final AtomicInteger rc = new AtomicInteger();
        // 写个数
        final AtomicInteger wc = new AtomicInteger();
        // 读线程
        Runnable read = new Runnable() {
            public void run() {
                scanFile(root);
                scanFile(exitFile);
            }
 
            public void scanFile(File file) {
                if (file.isDirectory()) {
                    File[] files = file.listFiles(new FileFilter() {
                        public boolean accept(File pathName) {
                            return pathName.isDirectory() || pathName.getPath().endsWith(".java");
                        }
                    });
                    assert files != null;
                    for (File one : files)
                        scanFile(one);
                } else {
                    try {
                        int index = rc.incrementAndGet();
                        System.out.println("Read0: " + index + " " + file.getPath());
                        queue.put(file);//读取到的文件放到队列中
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
 
 
        exec.submit(read);
        // 四个写线程
        for (int index = 0; index < 4; index++) {
            // 写线程
            final int NO = index;
            Runnable write = new Runnable() {
                String threadName = "Write" + NO;
 
                public void run() {
                    while (true) {
                        try {
                            Thread.sleep(randomTime());
                            int index = wc.incrementAndGet();
                            File file = queue.take();//从队列中拿元素
                            // 队列已经无对象
                            if (file == exitFile) {
                                // 再次添加"标志",以让其他线程正常退出
                                queue.put(exitFile);
                                break;
                            }
                            System.out.println(threadName + ": " + index + " " + file.getPath());
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            };
            exec.submit(write);
        }
        exec.shutdown();
    }
 
}

 

你可能感兴趣的:(Java,多线程,JVM)