concurrent包下的blockingQueue的学习

来源:互联网 发布:心动网络有名吗 编辑:程序博客网 时间:2024/05/16 15:19

一、什么是BlockingQueue

BlockingQueue即阻塞队列,从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种:

1. 当队列满了的时候进行入队列操作2. 当队列空了的时候进行出队列操作

因此,当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞,除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空队列进行出队列操作时,它将会被阻塞,除非有另一个线程进行了入队列操作。

Queue(队列) :用于保存一组元素,不过在存取元素的时候必须遵循先进先出原则。队列是一种特殊的线性表,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。在队列这种数据结构中,最先插入的元素将是最先被删除的元素;反之最后插入的元素将是最后被删除的元素,因此队列又称为“先进先出”(FIFO—first in first out)的线性表。

Deque(双端队列): 两端都可以进出的队列。当我们约束从队列的一端进出队时,就形成了另外一种存取模式,它遵循先进后出原则,这就是栈结构。双端队列主要是用于栈操作。使用站结构让操作有可追溯性(如windows窗口地址栏内的路径前进栈、后退栈)。

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


在Java中,BlockingQueue的接口位于java.util.concurrent 包中(在Java5版本开始提供),由上面介绍的阻塞队列的特性可知,阻塞队列是线程安全的。

二、BlockingQueue的用法

阻塞队列主要用在生产者/消费者的场景,下面这幅图展示了一个线程生产、一个线程消费的场景:

这里写图片描述

负责生产的线程不断的制造新对象并插入到阻塞队列中,直到达到这个队列的上限值。队列达到上限值之后生产线程将会被阻塞,直到消费的线程对这个队列进行消费。同理,负责消费的线程不断的从队列中消费对象,直到这个队列为空,当队列为空时,消费线程将会被阻塞,除非队列中有新的对象被插入。

三、BlockingQueue接口中的方法

阻塞队列一共有四套方法分别用来进行insertremoveexamine,当每套方法对应的操作不能马上执行时会有不同的反应,下面这个表格就分类列出了这些方法: 
asdf

-Throws ExceptionSpecial ValueBlocksTimes OutInsertadd(o)offer(o)put(o)offer(o, timeout, timeunit)Removeremove(o)poll()take()poll(timeout, timeunit)Examineelement()peek()  

这四套方法对应的特点分别是:

1. ThrowsException:如果操作不能马上进行,则抛出异常2. SpecialValue:如果操作不能马上进行,将会返回一个特殊的值,一般是true或者false3. Blocks:如果操作不能马上进行,操作会被阻塞4. TimesOut:如果操作不能马上进行,操作会被阻塞指定的时间,如果指定时间没执行,则返回一个特殊值,一般是true或者false

需要注意的是,我们不能向BlockingQueue中插入null,否则会报NullPointerException

四、BlockingQueue的实现类

BlockingQueue只是java.util.concurrent包中的一个接口,而在具体使用时,我们用到的是它的实现类,当然这些实现类也位于java.util.concurrent包中。在Java6中,BlockingQueue的实现类主要有以下几种:

1. ArrayBlockingQueue2. DelayQueue3. LinkedBlockingQueue4. PriorityBlockingQueue5. SynchronousQueue

下面我们就分别介绍这几个实现类。

4.1 ArrayBlockingQueue

ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。

ArrayBlockingQueue是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。下面是一个初始化和使用ArrayBlockingQueue的例子:

BlockingQueue queue = new ArrayBlockingQueue(1024);queue.put("1");Object object = queue.take();

先看一下ArrayBlockingQueue的部分源码:理解一下ArrayBlockingQueue的实现原理和机制

public class ArrayBlockingQueue <E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
 
    //数组的储存结构
    final Object[] items;    
   //锁采用的机制
    final ReentrantLock lock;
    public ArrayBlockingQueue( int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        //通过将公平性 (fairness) 设置为 true 而构造的队列允许按照 FIFO 顺序访问线程
        lock = new ReentrantLock(fair);
        notEmpty = lock .newCondition();
        notFull  lock .newCondition();
    }
    public boolean offer(E e) {
        checkNotNull(e);
        //使用ReentrantLock 锁机制
        final ReentrantLock lock = this.lock;
        lock.lock();//加锁
        try {
            if (count == items.length)
                return false ;
            else {
                enqueue(e);
                return true ;
            }
        } finally {
            lock.unlock();//释放锁
        }
    }
    private void enqueue(E x) {
        final Object[] items = this.items;
        items[ putIndex] = x;//通过数组进行储存
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal();
    }
…….
}
使用实例是:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/*
* 现有的程序代码模拟产生了16个日志对象,并且需要运行16秒才能打印完这些日志,
* 请在程序中增加4个线程去调用parseLog()方法来分头打印这16个日志对象,
* 程序只需要运行4秒即可打印完这些日志对象。
*/
public class BlockingQueueTest {
       public static void main(String[] args) throws Exception {
             // 新建一个等待队列
             final BlockingQueue<String> bq = new ArrayBlockingQueue<String>(16);
             // 四个线程
             for (int i = 0; i < 4; i++) {
                   new Thread(new Runnable() {
                         @Override
                         public void run() {
                               while (true ) {
                                     try {
                                          String log = (String) bq.take();
                                           parseLog(log);
                                    } catch (Exception e) {
                                    }
                              }
                        }
                  }).start();
            }
             for (int i = 0; i < 16; i++) {
                  String log = (i + 1) + ” –>  “;
                  bq.put(log); // 将数据存到队列里!
            }
      }
       // parseLog方法内部的代码不能改动
       public static void parseLog(String log) {
            System. out.println(log + System.currentTimeMillis());
             try {
                  Thread. sleep(1000);
            } catch (InterruptedException e) {
                  e.printStackTrace();
            }
      }
}

4.2 DelayQueue

DelayQueue阻塞的是其内部元素,DelayQueue中的元素必须实现 java.util.concurrent.Delayed接口,这个接口的定义非常简单:

public interface Delayed extends Comparable<Delayed> {long getDelay(TimeUnit unit);}

getDelay()方法的返回值就是队列元素被释放前的保持时间,如果返回0或者一个负值,就意味着该元素已经到期需要被释放,此时DelayedQueue会通过其take()方法释放此对象。

从上面Delayed 接口定义可以看到,它还继承了Comparable接口,这是因为DelayedQueue中的元素需要进行排序,一般情况,我们都是按元素过期时间的优先级进行排序。

例1:为一个对象指定过期时间

首先,我们先定义一个元素,这个元素要实现Delayed接口

public class DelayedElement implements Delayed {  private long expired;  private long delay;  private String name;  DelayedElement(String elementName, long delay) {         this. name = elementName;         this. delay= delay;         expired = ( delay + System. currentTimeMillis());  }  @Override  public int compareTo(Delayed o) {        DelayedElement cached=(DelayedElement) o;         return cached.getExpired()> expired?1:-1;  }  @Override  public long getDelay(TimeUnit unit) {         return ( expired - System. currentTimeMillis());  }  @Override  public String toString() {         return "DelayedElement [delay=" + delay + ", name=" + name + "]";  }  public long getExpired() {         return expired;  }}

设置这个元素的过期时间为3s

public class DelayQueueExample {  public static void main(String[] args) throws InterruptedException {        DelayQueue<DelayedElement> queue= new DelayQueue<>();        DelayedElement ele= new DelayedElement( "cache 3 seconds",3000);         queue.put( ele);        System. out.println( queue.take());  }

}

运行这个main函数,我们可以发现,我们需要等待3s之后才会打印这个对象。

其实DelayQueue应用场景很多,比如定时关闭连接、缓存对象,超时处理等各种场景,下面我们就拿学生考试为例让大家更深入的理解DelayQueue的使用。

例2:把所有考试的学生看做是一个DelayQueue,谁先做完题目释放谁

首先,我们构造一个学生对象

public class Student implements Runnable,Delayed{  private String name;  //姓名  private long costTime;//做试题的时间  private long finishedTime;//完成时间  public Student(String name, long costTime) {         this. name = name;         this. costTime= costTime;         finishedTime = costTime + System. currentTimeMillis();  }  @Override  public void run() {        System. out.println( name + " 交卷,用时" + costTime /1000);  }  @Override  public long getDelay(TimeUnit unit) {         return ( finishedTime - System. currentTimeMillis());  }  @Override  public int compareTo(Delayed o) {        Student other = (Student) o;         return costTime >= other. costTime?1:-1;  }}

然后在构造一个教师对象对学生进行考试

public class Teacher {  static final int STUDENT_SIZE = 30;  public static void main(String[] args) throws InterruptedException {        Random r = new Random();        //把所有学生看做一个延迟队列        DelayQueue<Student> students = new DelayQueue<Student>();        //构造一个线程池用来让学生们“做作业”        ExecutorService exec = Executors.newFixedThreadPool(STUDENT_SIZE);         for ( int i = 0; i < STUDENT_SIZE; i++) {               //初始化学生的姓名和做题时间               students.put( new Student( "学生" + (i + 1), 3000 + r.nextInt(10000)));        }        //开始做题        while(! students.isEmpty()){               exec.execute( students.take());        }         exec.shutdown();  }}

我们看一下运行结果:

学生2 交卷,用时3学生1 交卷,用时5学生5 交卷,用时7学生4 交卷,用时8学生3 交卷,用时11

通过运行结果我们可以发现,每个学生在指定开始时间到达之后就会“交卷”(取决于getDelay()方法),并且是先做完的先交卷(取决于compareTo()方法)。

通过查看其源码可以看到,DelayQueue内部实现用的是PriorityQueue和一个Lock:

这里写图片描述

4.3 LinkedBlockingQueue

LinkedBlockingQueue : 基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列 中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时 (LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反 之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别 采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大 小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于 消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。


LinkedBlockingQueue阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。

和ArrayBlockingQueue一样,LinkedBlockingQueue 也是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。下面是一个初始化和使LinkedBlockingQueue的例子:

BlockingQueue<String> unbounded = new LinkedBlockingQueue<String>();BlockingQueue<String> bounded   = new LinkedBlockingQueue<String>(1024);bounded.put("Value");String value = bounded.take();
先看一下LinkedBlockingDeque的部分源码:理解一下ArrayBlockingQueue的实现原理和机制
public class LinkedBlockingDeque <E>
    extends AbstractQueue<E>
    implements BlockingDeque<E>, java.io.Serializable {
    final ReentrantLock lock = new ReentrantLock();//线程安全
    /**
     * @throws NullPointerException {@inheritDoc}
     */
    public boolean offerLast(E e) {
        if (e == null) throw new NullPointerException();
        Node<E> node = new Node<E>(e);//每次插入后都将动态地创建链接节点
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return linkLast(node);
        } finally {
            lock.unlock();
        }
    }
    public boolean offer(E e) {
        return offerLast(e);
    }
    public boolean add(E e) {
        addLast(e);
        return true ;
    }
    public void addLast(E e) {
        if (!offerLast(e))
            throw new IllegalStateException(“Deque full”);
    }
    public E removeFirst() {
        E x = pollFirst();
        if (x == null) throw new NoSuchElementException();
        return x;
    }
    public E pollFirst() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return unlinkFirst();
        } finally {
            lock.unlock();
        }
    }
 
……
}
使用实例是:
将ArrayBlockingQueue的例子换成LinkedBlockingQueue即可:
 // 新建一个等待队列
 final BlockingQueue<String> bq = new ArrayBlockingQueue<String>(16);
换成:
final BlockingQueue<String> bq = new LinkedBlockingQueue<String>(16);

4.4 PriorityBlockingQueue

PriorityBlockingQueue是一个没有边界的队列,它的排序规则和 java.util.PriorityQueue一样。需要注意,PriorityBlockingQueue中允许插入null对象。

PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。

所有插入PriorityBlockingQueue的对象必须实现 java.lang.Comparable接口,队列优先级的排序规则就是按照我们对这个接口的实现来定义的。

另外,我们可以从PriorityBlockingQueue获得一个迭代器Iterator,但这个迭代器并不保证按照优先级顺序进行迭代。

下面我们举个例子来说明一下,首先我们定义一个对象类型,这个对象需要实现Comparable接口:

public class PriorityElement implements Comparable<PriorityElement> {private int priority;//定义优先级PriorityElement(int priority) {    //初始化优先级    this.priority = priority;}@Overridepublic int compareTo(PriorityElement o) {    //按照优先级大小进行排序    return priority >= o.getPriority() ? 1 : -1;}public int getPriority() {    return priority;}public void setPriority(int priority) {    this.priority = priority;}@Overridepublic String toString() {    return "PriorityElement [priority=" + priority + "]";}}

然后我们把这些元素随机设置优先级放入队列中

public class PriorityBlockingQueueExample {public static void main(String[] args) throws InterruptedException {    PriorityBlockingQueue<PriorityElement> queue = new PriorityBlockingQueue<>();    for (int i = 0; i < 5; i++) {        Random random=new Random();        PriorityElement ele = new PriorityElement(random.nextInt(10));        queue.put(ele);    }    while(!queue.isEmpty()){        System.out.println(queue.take());    }}}

看一下运行结果:

PriorityElement [priority=3]PriorityElement [priority=4]PriorityElement [priority=5]PriorityElement [priority=8]PriorityElement [priority=9]

4.5 SynchronousQueue

SynchronousQueue队列内部仅允许容纳一个元素。当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费。

阅读全文
'); })();
0 0
原创粉丝点击
热门IT博客
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 维普中文期刊网 维普科技期刊 维普外文科技期刊数据库 维普中文科技期刊全文数据库 维普中文科技期刊数据库简介 维普网论文查重 维普科技期刊数据库 维柴发电机组厂家 维柴发电机价格 二手维柴发动机 潍柴英致汽车 维棠 维棠flv下载 维棠下载 维棠怎么用 维棠下载器手机安卓版 下载维棠软件 维棠软件下载器 维棠flv下载器安卓版 维森车蜡 维森啤酒 维益植脂乳 维正 维正集团 维氏 瑞士维氏手表 维汉 维汉学习通 维汉tarjiman 维汉学习通免费下载 维汉双语词典 维沃 维沃x27 维沃x6 维沃手机怎么样 维沃x9手机价格 维泰 东莞卓越维港 维记港式茶餐厅 维他港式奶茶 维港 酒店