前言
今天不学习,明天变垃圾
本文的主要内容:多线程案例中的【阻塞队列 + 生产者消费者模型】。
阻塞队列
1. 阻塞队列
- (了解)编译器优化:不仅仅是编译器在做优化,也可能是操作系统和CPU在做优化
- 队列:先进先出;然而,并不是所有的队列都是“先进先出”,“先进先出”针对的是普通的队列,复杂队列就不一定“先进先出”。
如:非“先进先出”:
① 优先级队列PriorityQueue
② 消息队列(在队列元素中引入一个“类型”,此时的“类型”是指业务上的类型):入队列的时候没啥,但是出队列的时候会指定某个类型的元素先出。
- 【常用,所以工作中经常把消息队列这样的数据结构单独实现成一个小程序,并且部署在一组服务器上,称为“消息队列服务器”,也就是平时常说的“MQ”,其实也就是一种常用的“中间件”
- “中间件”其实就是一类“通用”服务器的统称,与业务无关,如MySQL】
-
阻塞队列是一个特殊的队列,但是其确实是 “先进先出” 的。
-
阻塞队列特点:
① 线程安全
② 带有阻塞功能:
A)如果队列满了还继续入队列,此时入队操作就会阻塞;直到队列不满,入队列才能成功
B)如果队列空了还继续出队列,此时出队操作就会阻塞;直到队列不空,出队列才能成功 -
阻塞队列的典型应用场景:生产者-消费者模型(描述的是多线程协同工作的一种方式),该模型能够较好地解决锁冲突问题。 (举例:包饺子)
2. 生产者消费者模型
- 生产者-消费者模型 / 阻塞队列的其他好处:
① 使用阻塞队列,有利于代码 “解耦合”
耦合:两个模块之间的关联关系,关系越紧密则耦合性越高。
【如:两个服务器直接通信则关联性强,互相影响;
但是如果在两个服务器之间加上一个阻塞队列就有效降低了两个服务器的关联性,耦合性降低,并且这样的话增加/删除服务器也较为方便】
- 也就是说: 生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,此时的阻塞队列就类似于“缓冲区”
② 削峰填谷:(举例:三峡水库)
如果按照没有 生产者消费者模型的写法,外面流量过来的压力就会直接压在每个服务器上,如果某个服务器抗压能力不太行就容易挂。
- 为什么一个服务器同一时刻收到很多请求就挂了?
理由:服务器每处理一个请求都是需要消耗一定的硬件资源,这些硬件资源包括但不限于CPU、内存、硬盘、宽带等,同一时刻请求越多则消耗的资源越多;而一台主机的硬件资源是有限的,一旦某个硬件资源耗尽了,此时机器也就挂了。
【而所谓的分布式系统,本质上就是加入了更多的硬件资源】 - 如果使用阻塞队列,当流量骤增的时候,生产者和阻塞队列就承受了压力,而其余消费者还是按照原来的节奏来消费数据,即对消费者的冲击就不大。
3. 阻塞队列的具体使用
(队列有三个基本操作:入队列、出队列、取 队首元素)
-
阻塞队列提供给了带有阻塞的入队列和出队列方法,但是没有提供带有阻塞的取队首元素方法。
-
标准库的阻塞队列
Demo1标准库
1)BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性。而put、take方法带有阻塞。
2) BlockingQueue是接口。 -
自己实现一个阻塞队列
1) 先实现一个普通队列
(队列的实现有两个版本:基于链表、基于数组):这里写基于数组的版本(循环队列)
① 循环队列:下标head/tail 如果head == tail就是null/满,如果存入数据则tail++,并且队列中有效元素范围是[head,tail); 而如果到达末尾就又回到开头,达到循环
② 如何区分该队列是空还是满呢?空和满都是head==tail,即指针重合,区分方法就是:要么加一个记录个数的变量,要么浪费一个空间
(具体参考博客:队列实现(循环队列))
2)加上线程安全 (线程安全就是加锁synchronized)
3)加上阻塞实现(队列为空则出队列阻塞,队列满则入队列阻塞)
这里注意稳妥的写法,wait不一定是另一个线程中的notify来唤醒的,也可能是interrupt来唤醒的,如果是interrupt唤醒可能条件就还不成熟,所以需要循环再判断。
(wait被唤醒之后也是要去竞争锁的)
Demo2 生产者消费者模型+自己实现阻塞队列
(尽可能看注释+每次提交)
THINK
- 阻塞队列的经典应用场景:生产者消费者模型
- 阻塞队列的实现(尤其注意模拟实现:普通队列的实现+加锁+实现阻塞安全)
- 标准库中的阻塞队列(接口)
- 阻塞队列的实现注意唤醒与等待!!