zoukankan      html  css  js  c++  java
  • Java阻塞队列

    一、阻塞队列(BlockingQueue):用于保存等待执行的任务。在阻塞队列中,线程阻塞的两种情况:

    1、当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。

      

    2、当队列中填满数据的情况下, 生产者端的所有线程都会被自动阻塞,直到队列中有空位置,线程被自动唤醒。

      

    二、阻塞队列的主要方法

           

      抛出异常:抛出一个异常;

      特殊值:返回一个特殊值(null或false,视情况而定)

      阻塞:在成功操作之前,一直阻塞线程

      超时:放弃前只在最大的时间内阻塞

    插入操作

      1)public abstract boolean add(E paramE):将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功是返回true,如果当前没有可用空间,则抛出异常。如果钙元素是null,则会抛出NullPointerException异常。

      2)public abstract boolean offer(E paramE):将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回true,如果当前没有可用的空间,则返回false。

      3)public abstract void put(E paramE)throws InterruptedExcaption:将指定元素插入队列中,将等待可用的空间(如果有必要)。

     1 public void put(E paramE) throws InterruptedException {
     2      checkNotNull(paramE);
     3      ReentrantLock  localReentrantLock = this.lock;
     4      localReentrantLock.lockInterruptibly();
     5   try {
     6          while (this.count == this.items.length)
     7          this.notFull.await();//如果队列满了,则线程阻塞等待
     8          enqueue(paramE);
     9          localReentrantLock.unlock();
    10      } finally {
    11          localReentrantLock.unlock();
    12      }
    13  }

      4)offer(E o,long timeout, TimeUnit unit):可以设定等待的时间,如果指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。

    获取数据操作

      1)poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;

      2)poll(long timeout,TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定的时间内,队列一旦有数据可取,则立即返回队列中的数据。否则直到时间超时还没有数据可取,返回失败;

      3)take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;

      4)drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率,不需要多次分批加锁或释放锁。

    三、Java中的阻塞队列

      1. ArrayBlockingQueue:由数组结构实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓的公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞队列:

        ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,ture);

      在读写操作上都需要锁住整个容器,适合于实现“生产者消费者”模式。ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。这个类是线程安全的。生产者和消费者共用一把锁。

      源码:

    final Object[] items;
    int takeIndex;
    int putIndex;
    int count;
    final ReentrantLock lock;
    private final Condition notEmpty;
    private final Condition notFull;
    
    public ArrayBlockintQueue(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 enqueue(E x){
        final Object[] items = this.items;
        items[putIndex] = x;
        if(++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal(); 
    }
    
    private E dequeue(){
        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;
    }
    
    public void put(E e) throws InterruptedExcepion{
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try{
            while(count == items.length)
                notFull.await();
            enqueue(e);
        }finally{
            lock.unlock();
        }
    }
    
    public E take() throws InterruptedException{
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try{
            while(count ==0)
                notEmpty.await()l
            return dequeue();
        }finally{
            lock.unlock();
        }
    }
    
    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();
        }
    }

      2. LinkedBlockingQueue:由链表结构组成的有界阻塞队列,内部维持着一个数据缓冲队列(该队列由链表构成)。同ArrayBlockingQueue类似,此队列按照先进先出(FIFO)的原则对元素进行排序。

      只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者线程,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。

      而LinkedBlockingQueue之所以能够高效地处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。吞吐量通常要高于ArrayBlockingQueue。

        LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE)。

      源码:

    private final int capacity;
    private final AtomicInteger count = new AtomicInteger();
    transient Node<E> head;
    private transient Node<E> last;
    private final ReentrantLock tackLock = new ReentrantLock();
    private final Condition notEmpty = takeLock.newCondition();
    private final ReentrantLock putLock = new ReentrantLock();
    private final Condition notFull = putLock.newCondtion();
    
    private void enqueue(Node<E> node){
        last = last.next = node;
    }
    
    private E dequeue(){
        Node<E> h =head;
        Node<E> first = h.next;
        h.next=h;
        head=first;
        E x =first.item;
        first.item=null;
        return x;
    }
    
    public void put(E e) throws InterruptedException{
        if(e == null) throw new NullPointerExcepion();
        int c =-1;
        Node<E>node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try{
        //当队列满时,调用notFull.await()方法释放锁,陷入等待状态。
        //有两种情况会激活该线程
        //第一,某个put线程添加元素后,发现队列有空余,就调用notFull.signal()方法激活阻塞线程
        //第二,take线程取元素时,发现队列已满。则其取出元素后,也会调用    notFull.signal()f方法激活阻塞线程
            while(count.get() == capacity){
                notFull.await();
            }
            enqueue(node);
            c =count.getAndIncrement();
            //发现队列未满,调用notFull.signal()激活阻塞的put线程(可能存在)
            if(c+1<capacity)
                notFull.signal();
        }finally{
            putLock.unlock();
        }
        if(c == 0)
            signalNotEmpty();
    } 
    
    public E take() throws InterruptedExcepion{
        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.getAndDectement();
            if(c>1)
                notEmpty.signal();
        }finally{
            takeLock.unlock();
        }
        if(c == capacity)
            signalNotFull();
        return x;
    }
    
    public boolean offer(E e){
        if(e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        if(count.get() == capacity)
            return false;
        int c=-1;
        Node<E>node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock();
        putLock.lock();
        try{
            if(count.get() < capacity){
                enqueue(node);
                c = count.getAndInrement();
                if(c+1 < capacity)
                    notFull.signal;
            }
        }finally{
            putLock.unlock();
        }
        if(c==0)
            signalNotEmpty();
        return c>=0;
    }
    
    public E poll(){
        final AtomicInteger count = this.count;
        if(count.get() == 0)
            return null;
        E x = null;
        int c= -1;
        final ReentrantLock takeLock = this.takeLock();
        takeLock.lock();
        try{
            if(count.get()>0){
                x = dequeue();
                c = count.getAndDecrement();
                if(c>1)
                    notEmpty.signal();
            }
        }finally{
            takeLock.unlock();
        }
        if(c == capacity)
            signalNotFull();
        return x;
    }

      ArrayBlockingQueue和LinkedBlockingQueue的区别:

      1)队列大小的初始化方式不同

        ArrayBlockingQueue是有界的,必须指定队列的大小;

        LinkedBlockingQueue是分情况的,指定队列的大小时,就是有界的;不指定队列的大小时,默认是Integer.MAX_VALUE,看成无界队列,但当生产速度大于消费速度时候,有可能会内存溢出。

      2)队列中锁的实现不同

        ArrayBlockingQueue实现的队列中的锁是没有分离的,即生产和消费用的是同一个锁;进行put和take操作,共用同一个锁对象。也就是说,put和take无法并行执行!

        LinkedBlockingQueue实现的队列中的锁是分离的,即生产用的是putLock,消费是takeLock。也就是说,生成端和消费端各自独立拥有一把锁,避免了读(take)和写(put)时互相竞争锁的情况,可并行执行。

      3)在生产或消费时操作不同

        ArrayBlockingQueue基于数组,在插入或删除元素时,是直接将枚举对象插入或移除的,不会产生或销毁任何额外的对象实例;

        LinkedBlockingQueue基于链表,在插入或删除元素时,需要把枚举对象转换为Node<E>进行插入或删除,会生成一个额外的Node对象,这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别,会影响性能。

      Put()和take()方法

        都可以实现阻塞的功能。

        Put()方法:把元素加入到阻塞队列中,如果阻塞队列没有空间,则调用此方法的线程被阻塞,直到有空间的时候再继续。

        take()方法:取出排在阻塞队列首位的对象,若阻塞队列为空,则调用此方法的线程被阻塞,知道有新的对象被加入的时候再继续。

      offer()和poll()方法

        不具有阻塞的功能。

        offer()方法:把元素加入到阻塞队列中,如果可以容纳,则返回true。如果不可以容纳,则返回false。

        poll()方法:取出排在阻塞队列首位的对象,若阻塞队列为空,则返回null,如果不为空,则返回取出来的那个元素。

      3. PriorityBlockingQueue:基于数组的且支持优先级排序的无界阻塞队列。默认情况下元素采取升序排列。可以自定义实现compareTo() 方法来指定元素进行排序规则,或初始化PriorityBlockingQueue时,制定构造函数Comparator来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。

      它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。注意,不会阻塞生产者,但会阻塞消费者。PriorityBlockingQueue里面存储的对象必须是实现Compareable接口,队列通过这个接口的Compare方法确定对象的priority。

      队列的元素并不是全部按优先级排序的,但是对头的优先级肯定是最高的。每取一个头元素时候,都会对剩余的元素做一次调整,这样就能保证每次队头的元素都是优先级最高的元素。

      4. DelayQueue:使用优先级队列实现的支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素是可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。

      该队列的头部是延迟期满后保存时间最长的Delayed元素。这个对列里面所存储的对象都带有一个时间参数,采用take获取数据的时候,如果时间没有到,取不出来任何数据。而加入数据的时候,是不会阻塞的(不会阻塞生产者,但会阻塞消费者)。DelayQueue内部使用PriorityQueue实现的。DelayQueue是一个使用PriorityQueue实现的BlockingQueue,优先队列的比较基准是时间。本质上即:DelayQueue = BolckingQueue + PriorityQueue + Delayed。

      优势:

      如果不使用DelayQueue,那么常规的解决方法就是:使用一个后台线程,遍历所有对象,挨个检查。这种笨的办法简单好用,但是对象数量过多时,可能存在性能问题,检查间隔时间不好设置,间隔时间过大,影响精确度,国小则存在效率问题。而且做不到按超时的时间顺序处理。 

      我们可以将DelayQueue运用在以下场景中:

        1)缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。

        2)定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。

    class Wanging implements Delayed{
        private String name;
        private String id;
        private long endTime;
    
        public Wanging(String name,String id,long endTime){
            this.name=name;
            this.id=id;
            this.endTime=endTime;
        }
    
        pubic String getName(){
            return this.name;
        }
    
        public String getId(){
            return this.id;
        }
    
        //用来判断是否到了截止时间
        public long getDelay(TimeUnit unit){
            return endTime - System.currentTimeMillis();
        }
    
        //相互比较排序用
        public int compareTo(Delayed o){
            Wanging jia = (Wanging) o;
            return endTime - jia.endTime > 0? 1:0;
        }
    }
    
    public class WangBa implements Runnable{
        private DelayQueue<Wanging> queue = new DelayQueue<Wanging>();
        public boolean yinye = true;
    
        public void shangji(String name,String id,int money){
            Wanging man = new Wanging(name,id,1000*60*money+System.currentTimeMillis());
            System.out.println("网名"+man.getName()+"身份证"+man.getId()+"交钱"+money+"块,开始上机...");
            this.queue.add(man);
        }
    
        public void xiaji(Wanging man){
            System.out.println("网名"+man.getName()+"身份证"+man.getId()+"时间到下机...");
        }
    
        public void run(){
            while(yinye){
                try{
                    System.out.println("检查ing");
                    Wanging man = queue.take();
                    xiaji(man);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
        public static void main(String[] args){
            try{
                System.out.println("网吧开始营业");
                WangBa siyu = new WangBa();
                Thread shangwang = new Thread(siyu);
                shangwang.start();
    
               siyu.shangji("路人甲","123",1);
                siyu.shangji("路人乙","234",2);
                siyu.shangji("路人丙","345",3);
            }catch(Exception e){
    
            }
        }
    }

      5. SynchronousQueue:是一个不存储元素的阻塞队列,它的size()方法总是返回0。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。可以认为SynchronousQueue是一个缓存值为1的阻塞队列。SynchronousQueue负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另外一个线程使用,SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue。

      6. LinkedTransferQueue:由链表结构组成的无界阻塞队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。

        1)transfer方法:如果当前有消费者正在等待接收元素(消费者使用take方法或带时间限制的poll方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等待该元素被消费者消费了才返回。

        2)tryTransfer方法:是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。tryTransfer方法与transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回,而transfer方法是必须等到消费者消费了才返回。对于带有时间限制的tryTransfer(E o,long timeout,TimeUnit unit)方法,则是试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回false,如果在超市时间内消费了元素,则返回true。

      7.LinkedBlockingDeque:由链表结构组成的双向阻塞队列。所谓双向队列指可以从队列的两端插入和移除元素。双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque多了addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast等方法,以First单词结尾的方法,表示插入、获取(peek)或移除双端队列第一个元素。以Last单词结尾的方法,表示插入、获取或移除双端队列的最后一个元素。另外插入方法add等同于addLast,移除方法remove等同于removeFirst。但是take方法却等同于takeFirst,使用时还是用带有First和Last后缀的方法更清楚。在初始化LinkedBlockingQueue时可以设置容量防止其过度膨胀。另外双向阻塞队列可以运用在“工作窃取”模式中。

     

    编程实现一个最大元素为100的阻塞队列。

    Lock lock = new ReentrantLock();
    Condition notFull = lock.newCondition();
    Condition notEmpty = lock.newCondition();
    
    Object[] items = new Object[100];
    int putptr,takeptr,count;
    
    public void put(Object x) throws InterruptedException{
        lock.lock();
        try{
            while(count == items.length)
                notFull.await();
            items[putptr]=x;
            if(++putptr == items.length)
                putptr = 0;
            ++count;
            notEmpty.signal(); 
        }finally{
            lock.unlock();
        }
    }
    
    public Object take() throws InterruptedException{
        lock.lock();
        try{
            while(count == 0)
                notEmpty.await();
            Object x =items[takeptr];
            if(++takeptr == items.length)
                takeptr = 0;
            --count;
            notFull.signal();
            return x;
        }finally{
            lock.unlock();
        }
    }

    设计一个双缓冲阻塞队列

      在服务器开发中,通常的做法是把逻辑处理线程和I/O处理线程分离。

      逻辑处理线程:对接收的包进行逻辑处理。

      I/O处理线程:网络数据的发送和接收,连接的建立和维护。

      通常逻辑处理线程和I/O处理线程是通过数据队列来交换数据,就是生产者--消费者模型。

      这个数据队列是多个线程在共享,每次访问都需要加锁,因此如何减少互斥/同步的开销就显得尤为重要。解决方案:双缓冲队列。

      两个队列,将读写分离,一个给逻辑线程读,一个给IO线程用来写,当逻辑线程读完队列后会将自己的队列与IO线程的队列相调换。这里需要加锁的地方有两个,一个是IO线程每次写队列时都要加锁,另一个是逻辑线程在调换队列时也需要加锁,但逻辑线程在读队列时是不需要加锁的。如果是一块缓冲区,读、写操作是不分离的,双缓冲区起码节省了单缓冲区时读部分操作互斥/同步的开销。本质是采用空间换时间的优化思路。

  • 相关阅读:
    Windows Server 2008 R2 服务器系统安装及配置全过程图文详解
    c#多线程
    DevExpress之TreeList控件用作导航使用说明
    Windows操作系统下搭建Git服务器和客户端。
    D^3CTF 2021 8-bit-pub
    shiro-1.2.4反序列化分析踩坑
    *CTF2021 oh-my-bet
    无线网络密码破解初体验
    瑞昱rtl819x-SDK-v3.4.14b的watchdog分析
    RC4加密算法
  • 原文地址:https://www.cnblogs.com/HuiH/p/11823215.html
Copyright © 2011-2022 走看看