# Queue接口
Queue 隊列,也是Collection的一個重要分支,是一個先進先出的結構,其一個重要的子接口為BlockingQueue(阻塞隊列),阻塞隊列在多線程的場景中有著廣泛的應用,在這里主要學習BlockingQueue的實現類。
阻塞隊列中的阻塞意為等待,與非阻塞隊列相比的話有如下的不同點:
1. 出隊:
* 非阻塞隊列:當隊列滿的時候,放入新的元素時,數據丟失。
* 阻塞隊列:當隊列滿的時候,放入新的元素時,線程阻塞等待,等待隊列中有出隊的元素,再繼續運行放進去。
2. 出隊:
* 非阻塞隊列:當隊列沒有元素的時候,取數據時得到的是null。
* 阻塞隊列:當隊列沒有元素的時候,取數據時,線程阻塞等待,什么時候有元素入隊了,才可以繼續運行取出元素。
常用API:
1. 添加元素
~~~
?public boolean add(E, e); // 不能添加null,成功時返回true,不成功時拋出Queue Full異常
?public boolean offer(E, e); // 不能添加null,成功時返回true,不成功時返回false
?public void put(E e); // 阻塞操作
~~~
2. 查詢
~~~
?public E take(); // 獲取并移除此隊列的頭部,在元素變得可用之前一直等待
?public E poll(long time, TimeUnit unit); // 獲取并移除此隊列的頭部,在指定時間內等待可用的元素
?public E peek(); // 獲取隊列的頭部,不移除 ?
~~~
*****
BlockingQueue的常見實現類:
## ArrayBlockingQueue
ArrayBlockingQueue底層是一個基于數組的有邊界的阻塞隊列,其只用了一把鎖來同時阻塞讀寫操作,讀寫不分離。
源碼分析:
~~~
?public class ArrayBlockingQueue<E> extends AbstractQueue<E>
? ? ? ? ?implements BlockingQueue<E>, java.io.Serializable {
? ? ?// 底層使用數組保存數據
? ? ?final Object[] items;
? ? ?// 取元素時用到的索引,初始為0
? ? ?int takeIndex;
? ? ?// 放元素時用到的索引,初始為0
? ? ?int putIndex;
? ? ?// 數組中元素的個數
? ? ?int count;
? ? ?// 可重入鎖,用于出隊和入隊等操作中
? ? ?final ReentrantLock lock;
? ? ?// lock伴隨的一個不為空的等待池,當隊列有數據時會喚醒在該等待隊列中的線程獲取數據
? ? ?private final Condition notEmpty;
? ? ?// lock伴隨的一個隊列不滿的等待池,當隊列中還沒有滿時會喚醒在該等待隊列中的線程往隊列添加數據
? ? ?private final Condition notFull;
? ? ?
? ? ?
? ? ?// -----------------------構造方法-------------------------
? ? ?public ArrayBlockingQueue(int capacity) { // 必須指定初始化容量
? ? ? ? ?this(capacity, false); // 默認為非公平鎖
? ? }
? ? ?public ArrayBlockingQueue(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();
? ? }
? ? ?
? ? ?//------------------兩個關鍵的操作-------------------------
? ? ?// 入隊
? ? ?private void enqueue(E x) {
? ? ? ? ?final Object[] items = this.items;
? ? ? ? ?// 將x放入putIndex索引指向的位置
? ? ? ? ?items[putIndex] = x;
? ? ? ? ?if (++putIndex == items.length)
? ? ? ? ? ? ?// 循環數組的作用,可以不斷的利用底層數組存放數據
? ? ? ? ? ? ?putIndex = 0;
? ? ? ? ?// 元素個數+1
? ? ? ? ?count++;
? ? ? ? ?// 添加一個元素則證明底層數組有數據了,喚醒在空隊列的等待池中阻塞的線程取出數據,即喚醒take方法的阻塞
? ? ? ? ?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--; // 元素個數-1
? ? ? ? ?if (itrs != null)
? ? ? ? ? ? ?itrs.elementDequeued();
? ? ? ? ?// 取出一個元素則證明隊列中又有空間可以存放數據了,喚醒在滿隊列的等待池中阻塞的線程,可以往隊列中繼續添加數據,即喚醒put方法的阻塞
? ? ? ? ?notFull.signal();
? ? ? ? ?return x;
? ? }
? ? ?
? ? ?// -------------------阻塞操作-------------------------
? ? ?// 添加元素
? ? ?public void put(E e) throws InterruptedException {
? ? ? ? ?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();
? ? ? ? ? ? ?return dequeue();
? ? ? ? } finally {
? ? ? ? ? ? ?lock.unlock();
? ? ? ? }
? ? }
? ? ?
?}
~~~
【更正:Condition中等待池這個詞匯換成等待隊列可能會更合適一點。】
總結:
1. 兩個Conditional變量的作用,notEmpty用來阻塞獲取元素操作,當隊列非空時(enqueue中喚醒)會喚醒該等待池中的線程;notFull用來阻塞添加元素操作,當隊列未滿時(dequeue中喚醒)會喚醒該等待池中的線程。
2. 讀寫操作只用到一把鎖,讀寫不分離。
3. 必須指定初始化容量。對于put操作當隊列已滿的時候會阻塞線程,對于take操作當隊列為空時也會阻塞線程。
> 面試:能否將while(count == items.length)和while(count == 0)中的while換成if?
不能,因為notFull中的線程被喚醒的瞬間,有其他線程放入元素,此時隊列又滿了,如果采用if的話只會判斷一次,線程被喚醒之后就會繼續執行enqueue操作,但是此時隊列是滿的,執行enqueue就會造成數據的丟失了。因此需要不斷的判斷,直到喚醒的線程判斷出當前隊列真的有位置了才能繼續運行。
## LinkedBlockingQueue
底層基于鏈表結構,支持讀寫同時操作,并發情況下,效率比ArrayBlockingQueue高。是一個`可選擇`的有界隊列,可以指定鏈表的容量,當不指定時默認為整型的最大值。
其API的使用與ArrayBlockingQueue一樣。
源碼分析:
~~~
?public class LinkedBlockingQueue<E> extends AbstractQueue<E>
? ? ? ? ?implements BlockingQueue<E>, java.io.Serializable {
? ? ?
? ? ?// 鏈表的節點結構
? ? ?static class Node<E> {
? ? ? ? ?E item; // 數據域
? ? ? ? ?Node<E> next; // 下個節點的指針域
? ? ? ? ?Node(E x) { item = x; }
? ? }
? ? ?// 鏈表的最大容量,不指定時默認為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 takeLock = new ReentrantLock();
? ? ?// 隊列非空等待池
? ? ?private final Condition notEmpty = takeLock.newCondition();
? ? ?// 添加數組的鎖
? ? ?private final ReentrantLock putLock = new ReentrantLock();
? ? ?// 隊列不滿時的等待池
? ? ?private final Condition notFull = putLock.newCondition();
? ? ?
? ? ?//---------------------構造方法----------------------
? ? ?public LinkedBlockingQueue() {
? ? ? ? ?// 默認容量為整型最大值
? ? ? ? ?this(Integer.MAX_VALUE);
? ? }
? ? ?public LinkedBlockingQueue(int capacity) {
? ? ? ? ?if (capacity <= 0) throw new IllegalArgumentException();
? ? ? ? ?this.capacity = capacity;
? ? ? ? ?last = head = new Node<E>(null);
? ? }
? ? ?
? ? ?
? ? ?//--------------------關鍵操作----------------------------
? ? ?// 入隊,
? ? ?private void enqueue(Node<E> node) {
? ? ? ? ?// 尾插法,直接添加到最后一個元素,并改變last的指向
? ? ? ? ?last = last.next = node;
? ? }
? ? ?// 出隊
? ? ?private E dequeue() {
? ? ? ? ?Node<E> h = head;
? ? ? ? ?// 第一個節點,會成為新的頭節點,
? ? ? ? ?Node<E> first = h.next;
? ? ? ? ?h.next = h; // help GC,這樣操作沒有別的引用h這個節點,垃圾回收會進行回收
? ? ? ? ?head = first;
? ? ? ? ?// 返回第一個節點的數據
? ? ? ? ?E x = first.item;
? ? ? ? ?// head的數據域為空
? ? ? ? ?first.item = null;
? ? ? ? ?return x;
? ? }
? ? ?
? ? ?// ----------------put和take操作-------------------------
? ? ?// 添加元素
? ? ?public void put(E e) throws InterruptedException {
? ? ? ? ?if (e == null) throw new NullPointerException();
? ? ? ? ?int c = -1;
? ? ? ? ?// 封裝成新的節點結構
? ? ? ? ?Node<E> node = new Node<E>(e);
? ? ? ? ?final ReentrantLock putLock = this.putLock;
? ? ? ? ?final AtomicInteger count = this.count;
? ? ? ? ?putLock.lockInterruptibly();
? ? ? ? ?try {
? ? ? ? ? ? ?while (count.get() == capacity) {
? ? ? ? ? ? ? ? ?// 隊列已滿,則阻塞當期線程,采用while的原因與ArrayBlockingQueue相同
? ? ? ? ? ? ? ? ?notFull.await();
? ? ? ? ? ? }
? ? ? ? ? ? ?enqueue(node);
? ? ? ? ? ? ?// 遞增元素個數
? ? ? ? ? ? ?c = count.getAndIncrement();
? ? ? ? ? ? ?if (c + 1 < capacity)
? ? ? ? ? ? ? ? ?// 再次判斷隊列是否滿的,因為count可能同時被讀線程操作
? ? ? ? ? ? ? ? ?notFull.signal();
? ? ? ? } finally {
? ? ? ? ? ? ?putLock.unlock();
? ? ? ? }
? ? ? ? ?if (c == 0)
? ? ? ? ? ? ?// 隊列中有元素了,喚醒take阻塞的線程,這里的c是舊值,count=1
? ? ? ? ? ? ?signalNotEmpty();
? ? }
? ? ?// 取出元素
? ? ?public E take() throws InterruptedException {
? ? ? ? ?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.getAndDecrement();
? ? ? ? ? ? ?if (c > 1)
? ? ? ? ? ? ? ? ?// 再次判斷是否有元素
? ? ? ? ? ? ? ? ?notEmpty.signal();
? ? ? ? } finally {
? ? ? ? ? ? ?takeLock.unlock();
? ? ? ? }
? ? ? ? ?if (c == capacity)
? ? ? ? ? ? ?// c是count的舊值,c==capacity就證明隊列有空余出來的位置了,喚醒put阻塞的線程
? ? ? ? ? ? ?signalNotFull();
? ? ? ? ?return x;
? ? }
?}
~~~
> 在FixedThreadPool和SingleThreadExecutor中被使用。
> 疑問,為什么使用了鎖還要使用while進行排隊隊列是否為空或者隊列是否滿了?
> 這是因為底層使用的是ReetrantLock,支持可重復入的。
**ArrayBlockingQueue和LinkedBlockingQueue的對比**
假如有10000個線程,分別有5000個線程進行讀操作,有5000個線程進行寫操作,其性能對比如下:
~~~
?public static void main(String[] args) throws InterruptedException {
? ? ?ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(10000);
? ? ?// LinkedBlockingQueue<String> linkedBlockingQueue = new LinkedBlockingQueue<>(10000);
? ? ?List<String> dataList = new Vector<>();
? ? ?// 創建10000個線程,分別進行500次的讀寫操作
? ? ?long start = System.currentTimeMillis();
? ? ?for (int i = 0; i < 10000; i++) {
? ? ? ? ?if (i % 2 == 0) {
? ? ? ? ? ? ?new Thread(() -> {
? ? ? ? ? ? ? ? ?for (int j = 0; j < 500; j++) {
? ? ? ? ? ? ? ? ? ? ?try {
? ? ? ? ? ? ? ? ? ? ? ? ?blockingQueue.put(Thread.currentThread().getName() + "j");
? ? ? ? ? ? ? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? ? ? ? ? ? ? ?e.printStackTrace();
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }, "Thread-" + i).start();
? ? ? ? } else {
? ? ? ? ? ? ?new Thread(() -> {
? ? ? ? ? ? ? ? ?for (int j = 0; j < 500; j++) {
? ? ? ? ? ? ? ? ? ? ?try {
? ? ? ? ? ? ? ? ? ? ? ? ?dataList.add(blockingQueue.take());
? ? ? ? ? ? ? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? ? ? ? ? ? ? ?e.printStackTrace();
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }, "Thread-" + i).start();
? ? ? ? }
??
? ? }
? ? ?long end = System.currentTimeMillis();
? ? ?System.out.println("用時:" + (end - start) + "ms");
? ? ?System.out.println(dataList.size());
?}
~~~
結果:
~~~
?ArrayBlockingQueue用時:2331ms
?LinkedBlockingQueue用時:2319ms
~~~
兩者性能差不多,有時LinkedBlockingQueue的性能還比不上ArrayBlockingQueue。
## SynchronousQueue
使用這個隊列必須先從隊列中取出元素(即先調用take方法),才可以向隊列中加入元素(再調用put方法),SynchronousQueue隊列中沒有任何容量,甚至一個容量都沒有。可以理解一個標記,當一個線程從隊列獲取數據的時候就會打上一個標記,之后如果有另外一個線程放入數據,數據就會直接傳送給獲取數據的線程。
如果先向隊列中添加元素則會拋出異常`Queue Full`,因為隊列是沒有容量的。使用put方法的話阻塞,因為一開始隊列就是滿的。注意取出元素的方法不能用peek,因為peek不會將元素從隊列中拿走,只是查看的作用。
優點:方便高效的進行線程間的數據傳送,效率高,不會產生隊列中數據爭搶問題。
~~~
?public static void main(String[] args) throws InterruptedException {
? ? ? ? ?SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();
? ? ? ? ?new Thread(() -> {
? ? ? ? ? ? ?try {
? ? ? ? ? ? ? ? ?System.out.println(Thread.currentThread().getName() + ":" + synchronousQueue.take());
? ? ? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? ? ? ?e.printStackTrace();
? ? ? ? ? ? }
? ? ? ? }, "Thread-take").start();
? ? ? ? ?new Thread(() -> {
? ? ? ? ? ? ?try {
? ? ? ? ? ? ? ? ?synchronousQueue.put("abc");
? ? ? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? ? ? ?e.printStackTrace();
? ? ? ? ? ? }
? ? ? ? }, "Thread-put").start();
??
? ? }
~~~
要多個線程配合使用才行。
> 在CachedThreadPool中被使用
## PriorityBlockingQueue
帶有優先級的阻塞隊列,隊列中的元素有不同的優先級;沒有界限限制(整型最大值),但是可以指定初始化的長度,不指定時默認為11,底層會進行擴容操作。放入的元素必須實現內部比較器,或者在創建PriorityBlockingQueue時設置外部比較器,元素的優先級根據比較器來決定。
~~~
?public static void main(String[] args) throws InterruptedException {
? ? ?PriorityBlockingQueue<String> blockingQueue = new PriorityBlockingQueue<>(11, new Comparator<String>() {
? ? ? ? ?@Override
? ? ? ? ?public int compare(String o1, String o2) {
? ? ? ? ? ? ?return o1.compareTo(o2);
? ? ? ? }
? ? });
??
? ? ?blockingQueue.put("bbb");
? ? ?blockingQueue.put("aaa");
? ? ?blockingQueue.put("ccc");
? ? ?System.out.println(blockingQueue);
? ? ?System.out.println("------------------------------------------------------");
? ? ?System.out.println(blockingQueue.take());
? ? ?System.out.println(blockingQueue.take());
? ? ?System.out.println(blockingQueue.take());
??
?}
~~~
## DelayQueue
DelayQueue是一個無界的BlockingQueue,用于放置實現了Delayed接口的對象,存放在DelayQueue隊列中的對象只能在其到期時才能從隊列中取走。
當生產者線程調用put方法添加元素時,會觸發Delayed接口中的compareTo方法進行排序,也就是隊列中的元素的順序是按到期時間排序的,而非進入隊列的順序。排在隊列頭部的元素最早到期,越往后到期時間越晚。
消費者線程查看隊列頭部的元素,并不是取出操作。然后調用元素的getDelay方法,如果此方法的返回值≤0,則消費者線程會從隊列中取出此元素處理。如果getDealy方法返回值大于0,則消費者線程wait返回的時間值后,再次從隊列頭部取出元素,此時元素到期可以取出。
注意:不能將null元素放置到這種隊列中。
使用場景:
1. 淘寶訂單業務:下單之后如果30分鐘之內沒有付款就自動取消訂單。
2. 餓了么訂餐通知:下單成功后60S之內給用戶發送短信通知。
3. 關閉空閑連接:服務器中客戶端的連接空閑一段時間后會自動關閉。
4. 緩存:緩存中的對象超過空閑時間限制,需要從緩存中移出。
5. 任務超時處理:在tcp協議的滑動窗口中,用來處理超時未響應的請求。
~~~
?public class TestDelayQueue {
? ? ?public static void main(String[] args) throws InterruptedException {
? ? ? ? ?DelayQueue<Student> delayQueue = new DelayQueue<>();
? ? ? ? ?delayQueue.put(new Student("zhangsan", 20, System.currentTimeMillis() + 5000));
? ? ? ? ?delayQueue.put(new Student("lisi", 19, System.currentTimeMillis() + 2000));
? ? ? ? ?delayQueue.put(new Student("wangwu", 18, System.currentTimeMillis() + 1000));
? ? ? ? ?System.out.println(delayQueue);
? ? ? ? ?System.out.println(delayQueue.take());
? ? ? ? ?System.out.println(delayQueue.take());
? ? ? ? ?System.out.println(delayQueue.take());
? ? }
?}
??
?// Student.java
?public class Student implements Delayed {
??
? ? ?private String name;
? ? ?private int age;
? ? ?private long endTime;
??
? ? ?public Student() {}
??
? ? ?public Student(String name, int age, long endTime) {
? ? ? ? ?this.name = name;
? ? ? ? ?this.age = age;
? ? ? ? ?this.endTime = endTime;
? ? }
??
? ? ?@Override
? ? ?public long getDelay(TimeUnit unit) {
? ? ? ? ?return endTime - System.currentTimeMillis();
? ? }
??
? ? ?@Override
? ? ?public int compareTo(Delayed o) {
? ? ? ? ?Student other = (Student) o;
? ? ? ? ?return age - other.getAge();
? ? }
??
? ? ?public String getName() {
? ? ? ? ?return name;
? ? }
??
? ? ?public void setName(String name) {
? ? ? ? ?this.name = name;
? ? }
??
? ? ?public int getAge() {
? ? ? ? ?return age;
? ? }
??
? ? ?public void setAge(int age) {
? ? ? ? ?this.age = age;
? ? }
??
? ? ?@Override
? ? ?public String toString() {
? ? ? ? ?return "Student{" +
? ? ? ? ? ? ? ? ?"name='" + name + '\'' +
? ? ? ? ? ? ? ? ?", age=" + age +
? ? ? ? ? ? ? ? ?", endTime=" + endTime +
? ? ? ? ? ? ? ? ?'}';
? ? }
?}
~~~
實現Delayed接口的實現類需要重寫getDelay和compareTo方法。
- 第一章 Java基礎
- ThreadLocal
- Java異常體系
- Java集合框架
- List接口及其實現類
- Queue接口及其實現類
- Set接口及其實現類
- Map接口及其實現類
- JDK1.8新特性
- Lambda表達式
- 常用函數式接口
- stream流
- 面試
- 第二章 Java虛擬機
- 第一節、運行時數據區
- 第二節、垃圾回收
- 第三節、類加載機制
- 第四節、類文件與字節碼指令
- 第五節、語法糖
- 第六節、運行期優化
- 面試常見問題
- 第三章 并發編程
- 第一節、Java中的線程
- 第二節、Java中的鎖
- 第三節、線程池
- 第四節、并發工具類
- AQS
- 第四章 網絡編程
- WebSocket協議
- Netty
- Netty入門
- Netty-自定義協議
- 面試題
- IO
- 網絡IO模型
- 第五章 操作系統
- IO
- 文件系統的相關概念
- Java幾種文件讀寫方式性能對比
- Socket
- 內存管理
- 進程、線程、協程
- IO模型的演化過程
- 第六章 計算機網絡
- 第七章 消息隊列
- RabbitMQ
- 第八章 開發框架
- Spring
- Spring事務
- Spring MVC
- Spring Boot
- Mybatis
- Mybatis-Plus
- Shiro
- 第九章 數據庫
- Mysql
- Mysql中的索引
- Mysql中的鎖
- 面試常見問題
- Mysql中的日志
- InnoDB存儲引擎
- 事務
- Redis
- redis的數據類型
- redis數據結構
- Redis主從復制
- 哨兵模式
- 面試題
- Spring Boot整合Lettuce+Redisson實現布隆過濾器
- 集群
- Redis網絡IO模型
- 第十章 設計模式
- 設計模式-七大原則
- 設計模式-單例模式
- 設計模式-備忘錄模式
- 設計模式-原型模式
- 設計模式-責任鏈模式
- 設計模式-過濾模式
- 設計模式-觀察者模式
- 設計模式-工廠方法模式
- 設計模式-抽象工廠模式
- 設計模式-代理模式
- 第十一章 后端開發常用工具、庫
- Docker
- Docker安裝Mysql
- 第十二章 中間件
- ZooKeeper