队列基础使用示例与通过队列实现线程通信

一. 队列基础解释

  1. 什么是队列: 一种用来存储数据的数据结构,有先进先出的特点
  2. 队列的分类: 非阻塞队列ConcurrentLinkedQueue, 阻塞队列 BlockingQueue
  3. 阻塞队列与非阻塞队列的区别:
  • 阻塞队列: 入列时如果超过了指定范围,会阻塞等待,在获取数据时如果队列中没有也会阻塞等待,可以有效防止队列容器溢出,数据丢失,是线程安全的
  • 非阻塞队列: 数据入列时如果超出了指定范围,不会等待,直接报错,在出列时,如果为空不会等待直接报错
  1. 根据队列中存放的数据个数是否是指定有限的,分为有界队列,与无界队列

  2. 常用队列:

    • ArrayDeque, (数组双端队列)
    • PriorityQueue, (优先级队列)
    • ConcurrentLinkedQueue, (基于链表的并发队列)
    • DelayQueue, (延期阻塞队列,阻塞队列实现了BlockingQueue接口)
    • ArrayBlockingQueue, (基于数组的并发阻塞队列)
    • LinkedBlockingQueue, (基于链表的FIFO阻塞队列)
    • LinkedBlockingDeque, (基于链表的FIFO双端阻塞队列)
    • PriorityBlockingQueue, (带优先级的无界阻塞队列)
    • SynchronousQueue (并发同步阻塞队列)

二. ConcurrentLinkedDeque 并发非阻塞式队列

  • 优点: 适用于高并发场景下的,利用CAS无锁机制,基于链接节点的非阻塞无界线程安全队列,该队列遵循先进先出原则,不允许向该队列中添加null值,
  • 添加方法: add 和offer() 都是加入元素的方法(在ConcurrentLinkedQueue中这俩个方法没有任何区别)
  • 删除方法: poll() 和peek() 都是取头元素节点,区别在于前者会删除元素,后者不会
  1. 使用示例
/*** 非阻塞式队列的特征: 当向该队列中添加数据时,如果添加的数据超出了
 * 队列的总数,会直接抛出异常,如果获取队列中的数据时,如队列为空会返回null
 * 注意获取队列中数据后要使用可以自动删除被获取的数据的方式,不然,下次
 * 获取数据获取到的还是原来的*/
public class TestConcurrentLinkedDeque {

    public static void main(String[]args){

        //创建非阻塞式队列ConcurrentLinkedDeque对象,根据存放到队列的数据
        //类型,设置泛型类型
        ConcurrentLinkedDeque<String> q = new ConcurrentLinkedDeque();
        //向队列中添加数据
        q.offer("AA");
        q.offer("BB");
        q.offer("FF");
        q.offer("RR");
        q.offer("CC");
        //从头获取元素,被获取到的队列元素会自动删除(支持此方式)
        System.out.println(q.poll());//多次执行poll会获取下一个
        System.out.println(q.poll());
        //从头获取元素,被获取后不会被删除(一般不会使用此方式)
        System.out.println(q.peek());
        //获取当前队列元素的总个数(如果前面被获取了个数会删除)
        System.out.println(q.size());//上面执行了一次poll方法,会删除掉一个,打印为4
    }
}

三. BlockingQueue 阻塞队列

  • 阻塞队列的特点: 获取元素时,如果队列为空,获取元素的线程会等待队列变为非空,然后执行。当队列满时,存储元素的线程会等待队列可用。
  • 阻塞队列常用于多线程数据共享,线程通讯,生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素
  • “生产者”和“消费者”模型中,通过队列可以实现两者之间的数据共享。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然,通过阻塞式队列解决这些问题,当需要生产数据时如果不能生产成功,当前线程等待生成成功后,其他线程才允许执行,当需要消费时如果消费不成功,当前线程在消费处等待消除成功后,其他线程允许执行
    在这里插入图片描述
    在这里插入图片描述

ArrayBlockingQueue

  1. 有边界的阻塞队列(可以不阻塞,阻塞设置在向队列中添加或获取数据时设置添加与获取的等待时间,如果添加或获取不成功,指定等待多长时间,如果在这段时间内还是没有添加或获取成功,则返回结果),内部实现是一个数组。有边界的意思是它的容量是有限的,必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。ArrayBlockingQueue遵循先进先出规则

  2. 抛出异常示例,当相对列中添加输入,如果队列已满,或获取数据的队列为空时抛出异常,

		//创建ArrayBlockingQueue,根据传入列队元素类型设置泛型,并设置列队元素的个数
        ArrayBlockingQueue <String> arrays = new ArrayBlockingQueue<String>(3);
        //抛出异常式添加
        arrays.add("aaa");
        arrays.add("bbb");
        arrays.add("ccc");
        //当队列已满时再向队列中添加元素抛出 IllegalStateException
        //arrays.add("ddd");

        //抛出异常式删除队列中都一个元素并返回(遵守先进先出原则)
        //当队列为空时,会抛出NoSuchElementException
        String val = arrays.remove();
        
        //抛出异常式检查,队列为空时抛出异常
        arrays.element();
        System.out.println(val);
  1. 阻塞等待方式
		//创建ArrayBlockingQueue,根据传入列队元素类型设置泛型,并设置列队元素的个数
        ArrayBlockingQueue <String> arrays = new ArrayBlockingQueue<String>(3);
        //阻塞等待式添加
        arrays.put("aaa");
        arrays.put("bbb");
        arrays.put("ccc");
        //当队列已满时再向队列中添加元素,会一直阻塞等待
        //等待队列放出一个空间后,才会添加成功
        //arrays.put("ddd");

        //阻塞等待式获取
        String val = arrays.take();
        arrays.take();
        arrays.take();
        //当队列为空时,会一致阻塞等待
        //队列中有数据后获取成功才会执行完毕
        //arrays.take();

        System.out.println(val);
  1. 特殊值方式添加获取示例,不会抛出异常,添加时返回成功或失败的boolean值,获取时失败返回null,并且可以指定阻塞等待时间
	//创建ArrayBlockingQueue,根据传入列队元素类型设置泛型,并设置列队元素的个数
	ArrayBlockingQueue <String> arrays = new ArrayBlockingQueue<String>(3);
	//非阻塞式添加,此方式没有设置当添加数据不成功时的等待时间
	arrays.offer("ttt");
	arrays.offer("vvv");
	arrays.offer("rr");
	// 阻塞式添加,如果添加不成功设置等待时间,该时间内如果还是没有添加成功
	//则不添加,可以通过返回的boolea值进行判断(此处指定等待时间为1,后面时时间单位为秒)
	arrays.offer("qq", 1, TimeUnit.SECONDS);
	
	//非阻塞获取
	arrays.poll();
	//阻塞获取,如果获取不到,指定等待,在指定等待时间内
	//获取不到,最后返回null;
	arrays.poll(3,TimeUnit.SECONDS);

LinkedBlockingQueue

阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE(21亿非常大的一个值)的容量 。它的内部实现是一个链表。遵循先进先出,规则

	//创建LinkedBlockingQueue列队对象,并制定元素个数(可以不指定那么就是无边界的)
	LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue(3);
	linkedBlockingQueue.offer("AAA");//非阻塞式添加
	linkedBlockingQueue.offer("DDD",3,TimeUnit.SECONDS);//阻塞式添加
	linkedBlockingQueue.add("BBB");
	System.out.println(linkedBlockingQueue.size());
	linkedBlockingQueue.poll();//非阻塞式获取
	linkedBlockingQueue.poll(3,TimeUnit.SECONDS);//阻塞式获取

PriorityBlockingQueue

是一个没有边界的队列,它的排序规则和 java.util.PriorityQueue一样。需要注意,PriorityBlockingQueue中允许插入null对象。所有插入PriorityBlockingQueue的对象必须实现 java.lang.Comparable接口,队列按照Comparable来设置元素的先后顺序。可以通过PriorityBlockingQueue获得一个迭代器Iterator,但这个迭代器并不保证按照优先级顺

SynchronousQueue

  1. 可以看为是同步列队,队列内部仅允许容纳一个元素。当一个线程插入一个元素,其他插入线程进入阻塞状态,只有插入的元素被消费后,才可以再次执行插入操作,消费也是相同。
  2. 代码示例,创建A,B两个线程,启动向SynchronousQueue中添加数据,虽然A,B两个线程的执行顺序不确定,但在执行时,向队列中添加数据与获取数据时配合执行的,假设A线程先执行,在向队列中添加完第一个元素aaaa后,会阻塞等待,B线程获取了第一个数据以后,A线程才会被唤醒继续添加第二个元素,假设B线程先执行也是如此,如果队列中没数据,会阻塞等待A线程添加了一个数据,B线程才会获取继续执行
	public static void main(String[] args) throws InterruptedException {
        //同步队列
        SynchronousQueue<String> queue = new SynchronousQueue<String>();

        new Thread(()->{
            try {
            	//添加第一个元素
                queue.put("aaaa");
                System.out.println("添加第一个元素完毕aaaa");
                //添加第二个元素,会在第一个元素被B线程消费后才会执行
                queue.put("bbbb");
                System.out.println("添加第二个元素完毕bbbb");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();


        new Thread(()->{
            try {
            	//获取第一个元素
                String val1 = queue.take();
                System.out.println("消费第一个元素"+val1);
				
				//获取第二个元素
                String val2 = queue.take();
                System.out.println("消费第二个元素"+val2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"b").start();
    }

四. 通过队列实现线程通讯

  1. 创建生产者,持有一个阻塞队列,将生产的数据存入队列中
//实现Runnable接口,重写run方法方式创建生产线程类
class ProducerThread implements Runnable {
   private BlockingQueue<String> blockingQueue;//队列对象属性
   //原子类AtomicInteger,通过这个原子类的的方法,对数据所运算,可以保证线程安全
   private AtomicInteger count = new AtomicInteger();
   //标识符,用来标识下一步的操作,是该生成还是消费,并使用volatile设置可见性
   //flag这个标识符是多个线程的共享数据,共享数据应该使用synchronized来同步
   //防止冲突,但是,flag这个数据并不参关于结果的真正运算,只是用来判断进行标识
   //并不会涉及到原子性的问题,所以设置volatile可见性,禁止重排序
   private volatile boolean FLAG = false;

   public ProducerThread(BlockingQueue<String> blockingQueue) {
      this.blockingQueue = blockingQueue;
   }
   /*生产者run方法,方法中通过判断flag来判断解释了是否应该进行生产操作
   * 如果flag不是true,则通过创建生产线程对象时传递进来操作队列元素对象
   * 阻塞式生成队列元素,生产消息,filg不是true是循环为死循环,只要列队中
   * 设置的边界不满(在创建列队对象时设置为3),会不停的生产消息(不停生产,
   * 后面消费线程会不停消费,只要消费线程不停止读取,永远都存不满)*/
   @Override
   public void run() {
      System.out.println(Thread.currentThread().getName() + "生产者开始启动....");
      while (!FLAG) {
         //通过原子类调用incrementAndGet方法,做累计+1运算
         // (当前默认为第一次调用返回1,保证线程安全,返回字符串)
         String data = count.incrementAndGet() + "";
         try {
            //向队列中阻塞添加数据,并设置等待时间为2,返回添加结果offer
            boolean offer = blockingQueue.offer(data, 1, TimeUnit.SECONDS);
            if (offer) {//如果添加成功
               System.out.println(Thread.currentThread().getName() + ",生产队列" + data + "成功..");
               //stop();不使用
            } else {//如果添加失败
               System.out.println(Thread.currentThread().getName() + ",生产队列" + data + "失败..");
            }
            Thread.sleep(1000);//线程休眠
         } catch (Exception e) {

         }
      }
      System.out.println(Thread.currentThread().getName() + ",生产者线程停止...");
   }
   public void stop() {
      this.FLAG = false;
   }
}

/*注意,
* 1.此处的filg标识与线程通讯中使用的filg是有一定区别的,线程通讯中的filg是在共享数据中
* 是两个线程同时操作一个filg, 此处的filg各个线程里面都有,各自是各自的,互不影响的,
* 此处的filg标识只是用来标识是否允许开启while循环,进行生产消息,或者消费消息,
* 2.由于设置的生产消息线程执行会没睡眠1秒执行一次(while,与sleep,)当队列数据存放满了以后
* 会等待1秒,合起来时每两秒生产一个消息,
* 消费消息是阻塞式消费,如果获取不到消息会阻塞等待2秒,两个线程同时执行时根据设置的时间配合
* 就是每生产一个,就消费一个
* 3.在main方法中注意子线程的开启方式,创建子线程对象,将子线程对象传入Thread构造器中,
* 创建Thread对象,通过Thread对象开启线程
* 4.没有使用同步,线程页不需要等待,filg也不是共享数据,再好好考虑一下还是否需要volatile来修饰
* */

  1. 创建消费者,也持有一个队列,在初始化时需要对这个队列进行赋值,通过持有的队列,取出要消费的数据进行消费
class ConsumerThread implements Runnable {
   private volatile boolean FLAG = true;
   private BlockingQueue<String> blockingQueue;

   public ConsumerThread(BlockingQueue<String> blockingQueue) {
      this.blockingQueue = blockingQueue;
   }

   /*消费在run方法中,通过判断flag标识,来验whil循环是否开启,
   * 如果flag为true,则是消费消息,通过初始化消费线程对象时传入的队列对象
   * 调用poll,阻塞式获取该队列中的元素,并设置阻塞等待时间
   * 判断获取到的数据是否为null,如果能获取到数据,循环不停执行,获取消息
   * 否则设置filg,return跳出while循环结束代码执行
   * 每消费一个消息停止执行消费,让生产者生产消息
   * ,接下来应该进行 生成消息的操作,生成线程与消费线是并发执行的,都在运行中,不用设置线程阻塞与等待*/
   @Override
   public void run() {
      System.out.println(Thread.currentThread().getName() + "消费者开始启动....");
      while (FLAG) {//通过flag判断如果是true则进行消费,如果不是
         try {
            String data = blockingQueue.poll(2, TimeUnit.SECONDS);

            //不用进行""验证,引入如果为""说明取到了值,值时""而已,但是列队中不允许存""
            if (data == null ) {
               FLAG = false;//
               System.out.println("消费者超过2秒时间未获取到消息.");
               return;
            }
            System.out.println("消费者获取到队列信息成功,data:" + data);

         } catch (Exception e) {
            // TODO: handle exception
         }
      }
   }
  1. 生产消费测试,根据生产消费的特性,选择指定的队列存储数据,生产时将队列赋值给生产线程中的属性,消费时在消费线程的队列属性中获取数据进行消费
 	public static void main(String[] args) {
      /*创建消费线程类与生产线程类时需要传递操作列队数据的对象,通过这个
      * 对象,生产线程生产消息,消费线程消费消息*/
      //创建操作列队数据对象LinkedBlockingQueue,阻塞列队,并设置列队元素个数
      BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>(3);
      //创建生产者线程(传入队列对象,该对象要与消费者中是同一个对象)
      ProducerThread producerThread = new ProducerThread(blockingQueue);
      //创建消费者线程(传入队列对象)
      ConsumerThread consumerThread = new ConsumerThread(blockingQueue);
      Thread t1 = new Thread(producerThread);//通过Thread方式启动线程
      Thread t2 = new Thread(consumerThread);
      t1.start();//启动生产者线程,生产消息
      t2.start();//启动消费者线程,消费消息
      //10秒后 停止线程..
      try {
         Thread.sleep(10*1000);
         producerThread.stop();
      } catch (Exception e) {
         // TODO: handle exception
      }
   }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值