目录
一. 队列基础解释
- 什么是队列: 一种用来存储数据的数据结构,有先进先出的特点
- 队列的分类: 非阻塞队列ConcurrentLinkedQueue, 阻塞队列 BlockingQueue
- 阻塞队列与非阻塞队列的区别:
- 阻塞队列: 入列时如果超过了指定范围,会阻塞等待,在获取数据时如果队列中没有也会阻塞等待,可以有效防止队列容器溢出,数据丢失,是线程安全的
- 非阻塞队列: 数据入列时如果超出了指定范围,不会等待,直接报错,在出列时,如果为空不会等待直接报错
-
根据队列中存放的数据个数是否是指定有限的,分为有界队列,与无界队列
-
常用队列:
- ArrayDeque, (数组双端队列)
- PriorityQueue, (优先级队列)
- ConcurrentLinkedQueue, (基于链表的并发队列)
- DelayQueue, (延期阻塞队列,阻塞队列实现了BlockingQueue接口)
- ArrayBlockingQueue, (基于数组的并发阻塞队列)
- LinkedBlockingQueue, (基于链表的FIFO阻塞队列)
- LinkedBlockingDeque, (基于链表的FIFO双端阻塞队列)
- PriorityBlockingQueue, (带优先级的无界阻塞队列)
- SynchronousQueue (并发同步阻塞队列)
二. ConcurrentLinkedDeque 并发非阻塞式队列
- 优点: 适用于高并发场景下的,利用CAS无锁机制,基于链接节点的非阻塞无界线程安全队列,该队列遵循先进先出原则,不允许向该队列中添加null值,
- 添加方法: add 和offer() 都是加入元素的方法(在ConcurrentLinkedQueue中这俩个方法没有任何区别)
- 删除方法: poll() 和peek() 都是取头元素节点,区别在于前者会删除元素,后者不会
- 使用示例
/*** 非阻塞式队列的特征: 当向该队列中添加数据时,如果添加的数据超出了
* 队列的总数,会直接抛出异常,如果获取队列中的数据时,如队列为空会返回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
-
有边界的阻塞队列(可以不阻塞,阻塞设置在向队列中添加或获取数据时设置添加与获取的等待时间,如果添加或获取不成功,指定等待多长时间,如果在这段时间内还是没有添加或获取成功,则返回结果),内部实现是一个数组。有边界的意思是它的容量是有限的,必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。ArrayBlockingQueue遵循先进先出规则
-
抛出异常示例,当相对列中添加输入,如果队列已满,或获取数据的队列为空时抛出异常,
//创建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);
- 阻塞等待方式
//创建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);
- 特殊值方式添加获取示例,不会抛出异常,添加时返回成功或失败的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
- 可以看为是同步列队,队列内部仅允许容纳一个元素。当一个线程插入一个元素,其他插入线程进入阻塞状态,只有插入的元素被消费后,才可以再次执行插入操作,消费也是相同。
- 代码示例,创建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();
}
四. 通过队列实现线程通讯
- 创建生产者,持有一个阻塞队列,将生产的数据存入队列中
//实现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来修饰
* */
- 创建消费者,也持有一个队列,在初始化时需要对这个队列进行赋值,通过持有的队列,取出要消费的数据进行消费
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
}
}
}
- 生产消费测试,根据生产消费的特性,选择指定的队列存储数据,生产时将队列赋值给生产线程中的属性,消费时在消费线程的队列属性中获取数据进行消费
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
}
}