![]()
这是我的第 58 篇原创文章
作者 | 悟空聊架构
来源 | 悟空聊架构(ID:PassJava666)
在讲《21张图讲解集合的线程不安全》那一篇,我留了一个彩蛋,就是Queue(队列)还没有讲,这次我们重点来看看Java中的Queue家族,总共涉及到18种Queue。这篇恐怕是市面上最全最细讲解Queue的。
本篇主要内容如下:
本篇主要内容
帮你总结好的阻塞队列:
18种Queue总结
一、Queue自我介绍
队列原理图
1.1 Queue自我介绍
hi,大家好,我的英文名叫Queue,中文名叫队列,无论现实生活中还是计算机的世界中,我都是一个很重要的角色哦~
我是一种数据结构,大家可以把我想象成一个数组,元素从我的一头进入、从另外一头出去,称为FIFO原则(先进先出原则)。
我还有两个亲兄弟:List(列表)、Set(集),他们都是Collection的儿子,我还有一个远房亲戚:Map(映射)。他们都是java.util包这个大家庭的成员哦~
1.2 现实生活中的场景
-
海底捞排号等位(先排号的优先进餐厅)
-
邮政员寄送信件(信箱是队列)
1.3 计算机世界中的场景
二、高屋建瓴,纵览全局
18种队列分为三大类: 接口、抽象类、普通类。
弄清楚下面的继承实现关系对后面理解18种队列有很大帮助。
18个Queue的继承实现关系图
-
Queue接口 继承 Collection接口, Collection接口 继承 Iterable接口
-
BlockingQueue接口、 Deque接口 继承 Queue接口
-
AbstractQueue抽象类 实现 Queue接口
-
BlockingDeque接口、 TransferQueue接口 继承 BlockingQueue接口
-
BlockingDeque接口继承 Deque接口
-
LinkedBlockingDeque类 实现 BlockingDeque接口
-
LinkedTransferQueue类接口 实现 TransferQueue接口
-
LinkedList类、 ArrayDeque类、 ConcurrentLinkedDeque类 实现 了 Deque接口
-
ArrayBlockingQueue类、 LinkendBlockingQueue类、 LinkedBlockingDeque类、 LinkedTransferQueue类、 SynchronousQueue类、 PriorityBlockQueue类、 DelayQueue类 继承 了 AbstractQueue抽象类和 实现了BlockingQueue接口
-
PriorityQueue类和 ConcurrentLinkedQueue类 继承 了 AbstractQueue抽象类
注意:
三、万物归宗Queue接口
2.1 深入理解Queue接口的本质
-
Queue接口是一种Collection,被设计用于处理之前临时保存在某处的元素。
-
除了基本的Collection操作之外,队列还提供了额外的插入、提取和检查操作。每一种操作都有两种形式:如果操作失败,则抛出一个异常;如果操作失败,则返回一个特殊值(null或false,取决于是什么操作)。
-
队列通常是以FIFO(先进先出)的方式排序元素,但是这不是必须的。
-
只有优先级队列可以根据提供的比较器对元素进行排序或者是采用正常的排序。无论怎么排序,队列的头将通过调用remove()或poll()方法进行移除。在FIFO队列种,所有新的元素被插入到队尾。其他种类的队列可能使用不同的布局来存放元素。
-
每个Queue必须指定排序属性。
2.2 Queue接口的核心方法
总共有3组方法,每一组方法对应两个方法。如下图所示:
Queue的核心方法
| 动作 |
抛异常 |
返回特殊值 |
| Insert |
add(e) |
offer(e) |
| Remove |
remove() |
poll |
| Examine |
element() |
peek() |
-
(1)比如添加(Insert)元素的动作,会有两种方式:add(e)和offer(e)。如果调用add(e)方法时,添加失败,则会抛异常,而如果调用的是offer(e)方法失败时,则会返回false。offer方法用于异常是正常的情况下使用,比如在有界队列中,优先使用offer方法。假如队列满了,不能添加元素,offer方法返回false,这样我们就知道是队列满了,而不是去handle运行时抛出的异常。
-
(2)同理,移除(Remove)元素的动作,队列为空时,remove方法抛异常,而poll返回null。如果移除头部的元素成功,则返回移除的元素。
-
(3)同理,检测(Examine)元素的动作,返回头部元素(最开始加入的元素),但不删除元素, 如果队列为空,则element()方法抛异常,而peek()返回false。
-
(4)Queue接口没有定义阻塞队列的方法,这些方法在BlockQueue接口中定义了。
-
(5)Queue实现类通常不允许插入null元素,尽管一些实现类比如LinkedList不禁止插入null,但是还是不建议插入null,因为null也被用在poll方法的特殊返回值,以说明队列不包含元素。
四、双端可用Deque接口
4.1 深入理解Deque接口的原理
双端队列Deque
(1)Deque概念: 支持两端元素插入和移除的线性集合。名称deque是双端队列的缩写,通常发音为deck。大多数实现Deque的类,对它们包含的元素的数量没有固定的限制的,支持有界和无界。
(2)Deque方法说明:
Deque方法
说明:
-
该列表包含包含访问deque两端元素的方法,提供了插入,移除和检查元素的方法。
-
这些方法种的每一种都存在两种形式:如果操作失败,则会抛出异常,另一种方法返回一个特殊值(null或false,取决于具体操作)。
-
插入操作的后一种形式专门设计用于容量限制的Deque实现,大多数实现中,插入操作不能失败,所以可以用插入操作的后一种形式。
-
Deque接口扩展了Queue接口,当使用deque作为队列时,作为FIFO。元素将添加到deque的末尾,并从头开始删除。
-
作为FIFO时等价于Queue的方法如下表所示:
比如Queue的add方法和Deque的addLast方法等价。
注意:peek方法不论是作为栈还是队列,都是从队列的检测队列的头,返回最先加入的元素。比如第一次put 100,第二次put 200,则peek返回的是100。如下图所示:
示例代码
4.1 哪些类实现了Deque接口
-
LinkedList类
-
ArrayDeque类
-
ConcurrentLinkedDeque类
-
LinkedBlockingDeque类
4.2 哪些类继承了Deque接口
五、队列骨架AbstractQueue抽象类
5.1 深入理解AbstractQueue抽象类
AbstractQueue是一个抽象类,继承了Queue接口,提供了一些Queue操作的骨架实现。
AbstractQueue的方法
方法add、remove、element方法基于offer、poll和peek。也就是说如果不能正常操作,则抛出异常。我们来看下AbstactQueue是怎么做到的。
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public E element() {
E x = peek();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
注意:
5.2 哪些类继承了AbstractQueue抽象类
-
ArrayBlockingQueue类、 LinkendBlockingQueue类、 LinkedBlockingDeque类、 LinkedTransferQueue类、 SynchronousQueue类、 PriorityBlockQueue类、 DelayQueue类 继承 了 AbstractQueue抽象类
-
PriorityQueue类和 ConcurrentLinkedQueue类 继承 了 AbstractQueue抽象类
六、阻塞缓冲BlockingQueue接口
6.1 宏观来看BlockingQueue(阻塞队列)
阻塞队列满了的情况
阻塞队列为空的情况
(1)BlockingQueue(阻塞队列)也是一种队列,支持阻塞的插入和移除方法。
(3)阻塞的插入:当队列满时,队列会阻塞插入元素的线程,直到队列不满。
(4)阻塞的移除:当队列为空,获取元素的线程会等待队列变为非空。
(5)应用场景:生产者和消费者,生产者线程向队列里添加元素,消费者线程从队列里移除元素,阻塞队列时获取和存放元素的容器。
(6)为什么要用阻塞队列:生产者生产和消费者消费的速率不一样,需要用队列来解决速率差问题,当队列满了或空的时候,则需要阻塞生产或消费动作来解决队列满或空的问题。
6.2 案例解析
线程A往阻塞队列(Blocking Queue)中添加元素,而线程B从阻塞队列中移除元素。
6.3 操刀BlockingQueue接口
BlockingQueue接口的10个核心方法:
继承的方法
10个核心方法总结如下:
BlockingQueue接口的10个核心方法
有三大类操作:插入、移除、检查。
6.4 BlockingQueue通过什么来阻塞插入和移除的?
6.5 哪些类继承了BlockingQueue接口?
-
BlockingDeque接口 - 双端阻塞队列
-
TransferQueue接口 - 传输队列
6.6 哪些类实现了BlockingQueue接口?
-
ArrayBlockingQueue类 - 由数组构成的有界阻塞队列
-
LinkedBlockingQueue类 - 由链表构成的有界阻塞队列,界限默认大小为Integer.MAX_Value(2^31-1),值非常大,相当于无界。
-
LinkedBlockingDeque类 - 由链表构成的双向阻塞队列
-
LinkedTransferQueue类 - 由链表构成的无界阻塞队列
-
SynchronousQueue类 - 不存储元素的阻塞队列,只有一个元素进行数据传递。
-
LinkedTransferQueue类 - 由链表构成的无界阻塞TransferQueue队列
-
DelayQueue类 - 使用优先级队列实现的延迟无界阻塞队列
6.6 BlockingQueue接口继承了哪些接口
七、双端阻塞BlockingDeque接口
7.1 从原理图上理解BlockDeque
BlockingDeque满了
BlockQueue为空
7.2 BlockingDeque接口方法
是阻塞队列BlockingQueue和双向队列Deque接口的结合。有如下方法:
BlockingDeque接口方法
示例:
尝试执行以下方法:
LinkedBlockingDeque queue = new LinkedBlockingDeque();
queue.addFirst("test1");
queue.addFirst(300);
queue.addLast("400");
最后队列中的元素顺序如下:
300, test1, 400。
先添加了test1放到队列的头部,然后在头部的前面放入300,所以300在最前面,成为头部,然后将400放入队列的尾部,所以最后的结果是300, test1, 400。
队列种的元素
7.3 BlockDeque和BlockQueue的对等方法
BlockDeque和BlockQueue的对等方法
7.4 BlockingDeque的特点
-
线程安全。
-
不允许使用null元素。
-
无界和有界都可以。
7.5 BlockingDeque接口继承了哪些接口?
7.6 哪些类实现了BlockDeque接口?
八、使命必达TransferQueue接口
8.1 Transfer怎么理解?
如果有消费者正在获取元素,则将队列中的元素传递给消费者。如果没有消费者,则等待消费者消费。我把它称作使命必达队列,必须将任务完成才能返回。
8.2 生活中的案例
-
针对TransferQueue的transfer方法
-
针对TransferQueue的tryTransfer方法
-
针对TransferQueue的tryTransfer超时方法
8.3 TransferQueue的原理解析
-
transfer(E e)
原理如下图所示:
transfer方法的原理
-
原理图解释:生产者线程Producer Thread尝试将元素B传给消费者线程,如果没有消费者线程,则将元素B放到尾节点。并且生产者线程等待元素B被消费。当元素B被消费后,生产者线程返回。
-
如果当前有消费者正在等待接收元素(消费者通过take方法或超时限制的poll方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。
-
如果没有消费者等待接收元素,transfer方法会将元素放在队列的tail(尾)节点,并等到该元素被消费者消费了才返回。
-
tryTransfer(E e)
-
tryTransfer(E e, long timeout, TimeUnit unit)
-
带有时间限制的tryTransfer方法。
-
试图把生产者传入的元素直接传给消费者。
-
如果没有消费者消费该元素则等待指定的时间再返回。
-
如果超时了还没有消费元素,则返回false。
-
如果在超时时间内消费了元素,则返回true。
-
getWaitingConsumerCount()
-
hasWaitingConsumer()
8.3 TransferQueue接口继承了哪些接口?
8.4 哪些类实现了TransferQueue接口?
九、优先由你PriorityQueue类
9.1 理解PriorityQueue类
本应该按照升序排序
按照自定义优先级排序
public PriorityQueue(Comparator<? super E> comparator) {
this(DEFAULT_INITIAL_CAPACITY, comparator);
}
public Comparator<? super E> comparator() {
return comparator;
}
-
不允许插入null元素。
-
实现PriorityQueue接口的类,不保证线程安全,除非是PriorityBlockingQueue。
-
PriorityQueue的迭代器不能保证以任何特定顺序遍历元素,如果需要有序遍历,请考虑使用 Arrays.sort(pq.toArray)。
-
进列( offer、 add)和出列( poll、 remove())的时间复杂度O(log(n))。
-
remove(Object) 和 contains(Object)的算法时间复杂度O(n)。
-
peek、element、size的算法时间复杂度为O(1)。
9.2 PriorityQueue类继承了哪些类?
9.2 PriorityQueue类实现了哪些接口?
十、双向链表LinkedList类
10.1 LinkedList的结构
LinkedList的结构
我们来看下节点类Node
private static class Node<E> {
E item; //元素
Node<E> next; //向后的节点链接
Node<E> prev; //向前的节点链接
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
10.2 与ArrayList的区别
10.3 LinkedList不是线程安全的
LinkedList不是线程安全的,所以可以使用如下方式保证线程安全。
List list = Collections.synchronizedList(new LinkedList<>());
10.4 LinkedList的家庭成员关系
-
LinkedList 继承了 AbstractSequentialList 类。
-
LinkedList 实现了 Queue 接口,可作为队列使用。
-
LinkedList 继承了 AbstractQueue抽象类,具有队列的功能。
-
LinkedList 实现了 List 接口,可进行列表的相关操作。
-
LinkedList 实现了 Deque 接口,可作为双向队列使用。
-
LinkedList 实现了 Cloneable 接口,可实现克隆。
-
LinkedList 实现了 java.io.Serializable 接口,即可支持序列化,能通过序列化去传输。
十一、并发安全ConcurrentLinkedQueue类
11.1 理解ConcurrentLinkedQueue
ConcurrentLinkedQueue原理
-
ConcurrentLinked是由链表结构组成的线程安全的先进先出无界队列。
-
当多线程要共享访问集合时,ConcurrentLinkedQueue是一个比较好的选择。
-
不允许插入null元素
-
支持非阻塞地访问并发安全的队列,不会抛出ConcurrentModifiationException异常。
-
size方法不是准确的,因为在统计集合的时候,队列可能正在添加元素,导致统计不准。
-
批量操作addAll、removeAll、retainAll、containsAll、equals和toArray不保证原子性(操作不可分割)
-
添加元素happen-before其他线程移除元素。
-
用法如下:
ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();
BuildingBlockWithName buildingBlock = new BuildingBlockWithName("三角形", "A");
concurrentLinkedQueue.add(buildingBlock);
11.2 ConcurrentLinkedQueue类继承了哪些类?
11.3 ConcurrentLinkedQueue类实现了哪些接口?
十二、双向数组ArrayDeque类
ArrayDeque原理图
12.1 理解ArrayDeque
12.2 使用方法
创建一个ArrayDeque,往arrayDeque队尾添加元素。
ArrayDeque arrayDeque = new ArrayDeque();
for (int i = 0; i < 50; i++) {
arrayDeque.add(buildingBlock); // add方法等价于addLast方法
}
12.3 ArrayDeque实现了哪些接口
十三、双向并发ConcurrentLinkedDeque类
13.1 理解ConcurrentLinkedDeque类
ConcurrentLinkedDeque原理图
13.2 ConcurrentLinkedDeque使用示例
创建两个积木:三角形、四边形,然后添加到队列:
BuildingBlockWithName buildingBlock1 = new BuildingBlockWithName("三角形", "A");
BuildingBlockWithName buildingBlock2 = new BuildingBlockWithName("四边形", "B");
ConcurrentLinkedDeque concurrentLinkedDeque = new ConcurrentLinkedDeque();
concurrentLinkedDeque.addFirst(buildingBlock1);
concurrentLinkedDeque.addLast(buildingBlock2);
//结果:顺序:三角形、四边形
13.3 ConcurrentLinkedDeque实现了哪些接口
十四、数组阻塞ArrayBlockingQueue类
14.1 理解ArrayBlockingQueue
ArrayBlockingQueuey原理图
-
ArrayBlockingQueue是一个用数组实现的有界阻塞队列。
-
队列慢时插入操作被阻塞,队列空时,移除操作被阻塞。
-
按照先进先出(FIFO)原则对元素进行排序。
-
默认不保证线程公平的访问队列。
-
公平访问队列:按照阻塞的先后顺序访问队列,即先阻塞的线程先访问队列。
-
非公平性是对先等待的线程是非公平的,当队列可用时,阻塞的线程都可以争夺访问队列的资格。有可能先阻塞的线程最后才访问访问队列。
-
公平性会降低吞吐量。
14.2 ArrayBlockingQueue使用示例
创建两个积木:三角形、四边形,然后添加到队列:
BuildingBlockWithName buildingBlock1 = new BuildingBlockWithName("三角形", "A");
BuildingBlockWithName buildingBlock2 = new BuildingBlockWithName("四边形", "B");
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(100, true);
arrayBlockingQueue.add(buildingBlock1);
arrayBlockingQueue.add(buildingBlock2);
14.3 ArrayBlockQueue实现了哪些接口
十五、链表阻塞LinkedBlockingQueue类
15.1 理解LinkedBlockingQueue
LinkedBlockingQueue原理
-
LinkedBlockingQueue具有单链表和有界阻塞队列的功能。
-
队列慢时插入操作被阻塞,队列空时,移除操作被阻塞。
-
默认和最大长度为Integer.MAX_VALUE,相当于无界(值非常大:2^31-1)。
15.2 LinkedBlockingQueue使用示例
LinkedList linkedList1 = new LinkedList();
linkedList1.add("A");
linkedList1.add("B");
linkedList1.add("C");
15.3 LinkedBlockingQueue的应用场景
15.4 LinkedBlockingQueue实现了哪些接口
-
LinkedBlockingQueue继承了 BlockingQueue类,可作为阻塞队列使用
-
LinkedBlockingQueue继承了 AbstractQueue抽象类,具有队列的功能。
-
LinkedBlockingQueue实现了 java.io.Serializable 接口,即可支持序列化,能通过序列化去传输。
十六、双向阻塞LinkedBlockingDeque类
16.1 理解LinkedBlockingDeque类
LinkedBlockingDeque原理图
-
由链LinkedBlockingDeque = 阻塞队列+链表+双端访问
-
线程安全。
-
多线程同时入队时,因多了一端访问入口,所以减少了一半的竞争。
-
默认容量大小为Integer.MAX_VALUE。可指定容量大小。
16.2 LinkedBlockingDeque的应用场景
LinkedBlockingDeque可以用在“工作窃取“模式中。
工作窃取算法:某个线程比较空闲,从其他线程的工作队列中的队尾窃取任务来帮忙执行。
16.3 LinkedBlockingDeque实现了哪些接口
-
LinkedBlockingDeque继承了 BlockingDeque类,可作为阻塞队列使用
-
LinkedBlockingDeque继承了 AbstractQueue抽象类,具有队列的功能。
-
LinkedBlockingDeque实现了 java.io.Serializable 接口,即可支持序列化,能通过序列化去传输。
十七、链表阻塞LinkedTransferQueue类
17.1 理解LinkedTransferQueue类
LinkedTransferQueue原理图
LinkedTransferQueue = 阻塞队列+链表结构+TransferQueue
之前我们讲“使命必达TransferQueue接口时已经介绍过了TransferQueue接口 ,所以LinkedTransferQueue接口跟它相似,只是加入了阻塞插入和移除的功能,以及结构是链表结构。
之前的TransferQueue也讲到了3个案例来说明TransferQueue的原理,大家可以回看TransferQueue。
17.2 LinkedTransferQueue接口比其他阻塞队列多了5个方法
-
transfer(E e)
-
tryTransfer(E e)
-
tryTransfer(E e, long timeout, TimeUnit unit)
-
getWaitingConsumerCount()
-
hasWaitingConsumer()
17.3 LinkedTransferQueue代码示例
生产者1 依次往队列中添加 A、B、C
生产者2 依次往队列中添加 D、E、F
消费者消费元素
生产者1 transfer A
生产者2 transfer D
2s后...
消费者 take A
生产者1 put B
2s后...
消费者 take D
生产者2 transfer E
2s后...
消费者 take B
生产者1 transfer C
(1)首先生产者线程1和2 调用transfer方法来传输A和D,发现没有消费者线程接收,所以被阻塞。
(2)消费者线程过了2s后将A拿走了,然后生产者1 被释放继续执行,传输元素B,发现又没有消费者消费,所以进行了等待。
(3)消费者线程过了2s后,将排在队列首部的D元素拿走,生产者2继续往下执行,传输元素E,发现没有消费者,所以进行了等待。
(4)消费者线程过了2s后,将排在队列首部的B元素拿走,生产者1传输C元素,等待消费者拿走。
(5)消费者不再消费了,所以生产者1和生产者2都被阻塞了,元素C和,元素E都没有被拿走,而且生产者2的F元素还没有开始传输,因为在等待元素D被拿走。
(6)看下队列里面确实有C和E元素,而且E排在队列的首部。
队列里面的元素
17.4 LinkedTransferQueue实现了哪些接口
十八、传球好手SynchronousQueue类
18.1 理解SynchronousQueue类
SynchronousQueue原理图
-
我称SynchronousQueue为”传球好手“。想象一下这个场景:小明抱着一个篮球想传给小花,如果小花没有将球拿走,则小明是不能再拿其他球的。
-
SynchronousQueue负责把生产者产生的数据传递给消费者线程。
-
SynchronousQueue本身不存储数据,调用了put方法后,队列里面也是空的。
-
每一个put操作必须等待一个take操作完成,否则不能添加元素。
-
适合传递性场景。
-
性能高于ArrayBlockingQueue和LinkedBlockingQueue。
18.2 SynchronousQueue示例
我们创建了两个线程,一个线程用于生产,一个线程用于消费
生产的线程依次put A、B、C三个值
消费线程每隔5s调用take方法
t1 put A
t2 take A
5秒后...
t1 put B
t2 take B
5秒后...
t1 put C
t2 take C
小结:说明生产线程执行put第一个元素"A" 操作后,需要等待消费者线程take完“A”后,才能继续往下执行代码。
18.1 SynchronousQueue应用场景
18.2 SynchronousQueue和LinkedTransferQueue的区别
-
SynchronousQueue 不存储元素,而LinkedTransferQueue存储元素。
-
SynchronousQueue 队列里面没有元素,而LinkedTransferQueue可以有多个存储在队列等待传输。
-
LinkedTransferQueue还支持若传输不了,则丢到队列里面去。
-
LinkedTransferQueue还支持若超过一定时间传输不了,则丢到队列里面去。
十九、优先级阻塞PriorityBlockingQueue类
19.1 理解PriorityBlockQueue类
PriorityBlockQueue的原理图
-
PriorityBlockQueue = PriorityQueue + BlockingQueue
-
之前我们也讲到了PriorityQueue的原理,支持对元素排序。
-
元素默认自然排序。
-
可以自定义CompareTo()方法来指定元素排序规则。
-
可以通过构造函数构造参数Comparator来对元素进行排序。
19.2 PriorityBlockQueue实现了哪些接口
-
LinkedBlockingQueue继承了 BlockingQueue接口,可作为阻塞队列使用
-
LinkedBlockingQueue继承了 AbstractQueue抽象类,具有队列的功能。
-
LinkedBlockingQueue实现了 java.io.Serializable 接口,即可支持序列化,能通过序列化去传输。
二十、延时阻塞DelayQueue类
20.1 理解DelayQueue
DelayQueue原理图
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
20.2 源码解析
public boolean offer(E e, long timeout, TimeUnit unit) {
return offer(e);
}
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
return q.poll();
poll方法
20.3 应用场景
20.4 DelayQueue实现了哪些接口
这一篇花了很多心思在上面,看官方英文文档、画原理图、写demo代码,排版。这恐怕是市面上最全最细讲解Queue的。
- END -
❝
你好,我是悟空哥,「7年项目开发经验,全栈工程师,开发组长,超喜欢图解编程底层原理」。正在编写两本PDF,分别是 1、Spring Cloud实战项目(佳必过),2、Java并发必知必会。我还手写了2个小程序,Java刷题小程序,PMP刷题小程序,点击我的公众号菜单打开!另外有111本架构师资料以及1000道Java面试题,都整理成了PDF,可以关注公众号 「悟空聊架构」 回复 悟空 领取优质资料。
❞
「转发->在看->点赞->收藏->评论!!!」 是对我最大的支持!