本文介紹了Threadlocal、volatile、condition、Semaphore、CountDownLatch、unsafe 等關鍵字
目錄如下:
- Threadlocal 本地執行緒
- volatile
- condition
- CountDownLatch 閂鎖
- CyclicBarrier 籬柵
- Semaphore 信號燈
- unsafe 魔法類
- StampedLock 新讀寫鎖
1. Threadlocal
從名字我們就可以看到ThreadLocal叫做本地執行緒,意思是ThreadLocal中填充的變數屬于當前執行緒,該變數對其他執行緒而言是隔離的,ThreadLocal為變數在每個執行緒中都創建了一個副本,那么每個執行緒可以訪問自己內部的副本變數,
Java就是通過ThreadLocal來實作執行緒本地存盤的,
使用場景:
- 在進行物件跨層傳遞的時候,使用ThreadLocal可以避免多次傳遞,打破層次間的約束,
- 執行緒間資料隔離
- 進行事務操作,用于存盤執行緒事務資訊,
- 資料庫連接,Session會話管理,**
1.1 ThreadLocal 結構分析
ThreadLocalMap里面有個Entry陣列,只有陣列沒有像HashMap那樣有鏈表,因此當hash沖突的之后,ThreadLocalMap是采用線性探測的方式解決hash沖突,
線性探測,就是先根據初始key的hashcode值確定元素在table陣列中的位置,如果這個位置上已經有其他key值的元素被占用,則利用固定的演算法尋找一定步長的下個位置,依次直至找到能夠存放的位置,在ThreadLocalMap步長是1,
線性探測 是通過 AtomicInteger 的原子性方法 getAndAdd 獲取個位置陣列中的位置,如果該位置已被占用則通過 nextIndex 方法獲取下一個位置,回圈直到有空位置為止,
結構如下
public class ThreadLocal<T> {
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
static class ThreadLocalMap {
private Entry[] table;
private int size = 0;
//繼承弱參考
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
//對k加上弱參考WeakReference
super(k);
value = v;
}
}
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//回圈探測下個hashcode 的位置
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
}
}
1.2 ThreadLocalMap
ThreadLocalMap其實就是ThreadLocal的一個靜態內部類(多執行緒共享),里面定義了一個Entry[]陣列來保存資料,而且還是繼承的弱參考, key為弱參考,在Entry內部使用ThreadLocal作為key,使用我們設定的value作為value,
- 每個Thread維護著一個ThreadLocalMap的參考
- ThreadLocalMap是ThreadLocal的內部類,用Entry來進行存盤
- ThreadLocal創建的副本是存盤在自己的threadLocals中的,也就是自己的ThreadLocalMap,
- ThreadLocalMap的鍵值為ThreadLocal物件,而且可以有多個threadLocal變數,因此保存在map中
- 在進行get之前,必須先set,否則會報空指標例外,當然也可以初始化一個,但是必須重寫initialValue()方法,
- ThreadLocal本身并不存盤值,它只是作為一個key來讓執行緒從ThreadLocalMap獲取value,
1.3 記憶體泄漏
記憶體泄漏問題:
上面這張圖詳細的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的關系,
- 1、Thread中有一個map,就是ThreadLocalMap
- 2、ThreadLocalMap的key是ThreadLocal,值是我們自己設定的,
- 3、ThreadLocal是一個弱參考weakReference,當為null時,會被當成垃圾回收
- 4、重點來了,突然我們ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此時我們的ThreadLocalMap生命周期和Thread的一樣,它不會回收,這時候就出現了一個現象,那就是ThreadLocalMap的key沒了,但是value還在,這就造成了記憶體泄漏,
解決辦法:使用完ThreadLocal后,執行remove操作,避免出現記憶體溢位情況,
參考:
https://mp.weixin.qq.com/s/Gc1YPt_DPMNKbbE_I0jmSA
https://baijiahao.baidu.com/s?id=1653790035315010634&wfr=spider&for=pc
2. volatile
- 保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的,(實作可見性)
- 禁止進行指令重排序,(實作有序性)
- volatile 只能保證對單次讀/寫的原子性,i++ 這種操作不能保證原子性,
讀理解
當讀一個volatile變數時,JMM會把該執行緒對應的本地記憶體置為無效,執行緒接下來將從主記憶體中讀取共享變數,
寫理解
當寫一個volatile變數時,JMM會把該執行緒對應的本地中的共享變數值重繪到主記憶體,
將當前處理器快取行的資料寫回到系統記憶體,這個寫回記憶體的操作會告知在其他CPU你們拿到的變數是無效的下一次使用時候要重新共享記憶體拿,
使用 volatile 必須具備的條件
- 對變數的寫操作不依賴于當前值,
該變數沒有包含在具有其他變數的不變式中,
- 只有在狀態真正獨立于程式內其他內容時才能使用 volatile,
2.1 volatile 禁止指令重排
volatile是通過記憶體屏障來來禁止指令重排的,
記憶體屏障(Memory Barrier是一類同步屏障指令,是CPU或編譯器在對記憶體隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執行后才可以開始執行此點之后的操作,下表描述了和volatile有關的指令重排禁止行為:
記憶體屏障就是基于4個匯編級別的關鍵字來禁止指令重排的,其中volatile的規則如下:

-
當第二個操作是volatile寫時,不管第一個操作是什么,都不能重排序,這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之后,
-
當第一個操作是volatile讀時,不管第二個操作是什么,都不能重排序,這個規則確保volatile讀之后的操作不會被編譯器重排序到volatile讀之前,
-
當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序,
2.1.1 記憶體屏障(Memory Barrier)

StoreStore屏障:禁止上面的普通寫和下面的volatile寫重排序;
StoreLoad屏障:防止上面的volatile寫與下面可能有的volatile讀/寫重排序
LoadLoad屏障:禁止下面所有的普通讀操作和上面的volatile讀重排序
LoadStore屏障:禁止下面所有的普通寫操作和上面的volatile讀重排序
CPU中,每個CPU又有多級快取,一般分為L1,L2,L3,因為這些快取的出現,提高了資料訪問性能,避免每次都向記憶體索取,但是弊端也很明顯,不能實時的和記憶體發生資訊交換,分在不同CPU執行的不同執行緒對同一個變數的快取值不同,
- 硬體層的記憶體屏障分為兩種:
Load Barrier和Store Barrier即讀屏障和寫屏障,【記憶體屏障是硬體層的】
2.1.2 為什么需要記憶體屏障
由于現代作業系統都是多處理器作業系統,每個處理器都會有自己的快取,可能存再不同處理器快取不一致的問題,而且由于作業系統可能存在指令重排序,導致讀取到錯誤的資料,因此,作業系統提供了一些記憶體屏障以解決這種問題.
簡單來說:
1.在不同CPU執行的不同執行緒對同一個變數的快取值不同,為了解決這個問題,用volatile可以解決上面的問題,不同硬體對記憶體屏障的實作方式不一樣,java屏蔽掉這些差異,通過jvm生成記憶體屏障的指令,
2.對于讀屏障:在指令前插入讀屏障,可以讓高速快取中的資料失效,強制從主記憶體取,
2.1.3 記憶體屏障的作用
cpu執行指令可能是無序的,它有兩個比較重要的作用
1.阻止屏障兩側指令重排序
2.強制把寫緩沖區/高速快取中的臟資料等寫回主記憶體,讓快取中相應的資料失效,
2.2有序性
編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定型別的處理器重排序,對于編譯器來說,發現一個最優布置來最小化插入屏障的總數幾乎是不可能的,為此,JMM采取了保守策略:
- 在每個volatile寫操作的前面插入一個StoreStore屏障;寫完對寫可見
- 對于這樣的陳述句Store1;
StoreStore; Store2,在Store2及后續寫入操作執行前,保證Store1的寫入操作對其它處理器可見,
- 對于這樣的陳述句Store1;
- 在每個volatile寫操作后面插入一個StoreLoad屏障;寫完對讀可見
- 對于這樣的陳述句Store1;
StoreLoad; Load2,在Load2及后續所有讀取操作執行前,保證Store1的寫入對所有處理器可見,
- 對于這樣的陳述句Store1;
- 在每個volatile讀操作的后面插入一個LoadLoad屏障;讀完對讀可見
- 對于這樣的陳述句Load1;
LoadLoad; Load2,在Load2及后續讀取操作要讀取的資料被訪問前,保證Load1要讀取的資料被讀取完畢,
- 對于這樣的陳述句Load1;
- 在每個volatile讀操作之后再插入一個LoadStore屏障,讀完對寫可見
- 對于這樣的陳述句Load1;
LoadStore; Store2,在Store2及后續寫入操作被刷出前,保證Load1要讀取的資料被讀取完畢,
- 對于這樣的陳述句Load1;
volatile通過在volatile變數的操作前后插入記憶體屏障的方式,來禁止指令重排,進而保證多執行緒情況下對共享變數的有序性,
2.3 volatile 可見性
volatile對于可見性的實作,記憶體屏障也起著至關重要的作用,因為記憶體屏障相當于一個資料同步點,他要保證在這個同步點之后的讀寫操作必須在這個點之前的讀寫操作都執行完之后才可以執行,并且在遇到記憶體屏障的時候,快取資料會和主存進行同步,或者把快取資料寫入主存、或者從主存把資料讀取到快取,
作業系統中的快取和JVM中執行緒的本地記憶體并不是一回事,通常我們可以認為:MESI可以解決快取層面的可見性問題,使用volatile關鍵字,可以解決JVM層面的可見性問題,
快取可見性問題的延伸:由于傳統的MESI協議的執行成本比較大,所以CPU通過Store Buffer和Invalidate Queue組件來解決,但是由于這兩個組件的引入,也導致快取和主存之間的通信并不是實時的,也就是說,快取一致性模型只能保證快取變更可以保證其他快取也跟著改變,但是不能保證立刻、馬上執行,
在計算機記憶體模型中,也是使用記憶體屏障來解決快取的可見性問題的(再次強調:快取可見性和并發編程中的可見性可以互相類比,但是他們并不是一回事兒),寫記憶體屏障(Store Memory Barrier)可以促使處理器將當前store buffer(存盤快取)的值寫回主存,讀記憶體屏障(Load Memory Barrier)可以促使處理器處理invalidate queue(失效佇列),進而避免由于Store Buffer和Invalidate Queue的非實時性帶來的問題,
記憶體屏障也是保證可見性的重要手段,作業系統通過記憶體屏障保證快取間的可見性,JVM通過給volatile變數加入記憶體屏障保證執行緒之間的可見性,
2.4 volatile 與 synchronized 的比較
- volatile是變數修飾符,其修飾的變數具有可見性,
- volatile主要用在多個執行緒感知實體變數被更改了場合,從而使得各個執行緒獲得最新的值,它強制執行緒每次從主記憶體中取到變數,而不是從執行緒的私有記憶體中讀取變數,從而保證了資料的可見性,
- ①volatile輕量級,只能修飾變數,synchronized重量級,還可修飾方法
- ②volatile只能保證資料的可見性,不能用來同步,因為多個執行緒并發訪問volatile修飾的變數不會阻塞,
- synchronized不僅保證可見性,而且還保證原子性,因為,只有獲得了鎖的執行緒才能進入臨界區,從而保證臨界區中的所有陳述句都全部執行,多個執行緒爭搶synchronized鎖物件時,會出現阻塞,
參考:https://www.hollischuang.com/archives/2673?spm=a2c6h.12873639.0.0.1c786ca5UcOGO5
3. Condition
在 lock 介面和 AbstractQueuedSynchronizer 中的ConditionObject類都有用到 condition 介面
interface Lock {
Condition newCondition();
}
在使用Lock之前,我們使用的最多的同步方式應該是synchronized關鍵字來實作同步方式了,配合Object的wait()、notify()系列方法可以實作等待/通知模式,Condition介面也提供了類似Object的監視器方法,與Lock配合可以實作等待/通知模式,但是這兩者在使用方式以及功能特性上還是有差別的,Object和Condition介面的一些對比,

condition物件是依賴于lock物件的,意思就是說condition物件需要通過lock物件進行創建出來(呼叫Lock物件的newCondition()方法),但是需要注意在呼叫方法前獲取鎖,
一般都會將Condition物件作為成員變數,當呼叫await()方法后,當前執行緒會釋放鎖并在此等待,而其他執行緒呼叫Condition物件的signal()方法,通知當前執行緒后,當前執行緒才從await()方法回傳,并且在回傳前已經獲取了鎖,
3.1 condition常用方法
condition可以通俗的理解為條件佇列,當一個執行緒在呼叫了await方法以后,直到執行緒等待的某個條件為真的時候才會被喚醒,這種方式為執行緒提供了更加簡單的等待/通知模式,Condition必須要配合鎖一起使用,因為對共享狀態變數的訪問發生在多執行緒環境下,一個Condition的實體必須與一個Lock系結,因此Condition一般都是作為Lock的內部實作,
- await() :造成當前執行緒在接到信號或被中斷之前一直處于等待狀態,
- await(long time, TimeUnit unit) :造成當前執行緒在接到信號、被中斷或到達指定等待時間之前一直處于等待狀態
- awaitNanos(long nanosTimeout) :造成當前執行緒在接到信號、被中斷或到達指定等待時間之前一直處于等待狀態,回傳值表示剩余時間,如果在nanosTimesout之前喚醒,那么回傳值 = nanosTimeout - 消耗時間,如果回傳值 <= 0 ,則可以認定它已經超時了,
- awaitUninterruptibly() :造成當前執行緒在接到信號之前一直處于等待狀態,【注意:該方法對中斷不敏感】,
- awaitUntil(Date deadline) :造成當前執行緒在接到信號、被中斷或到達指定最后期限之前一直處于等待狀態,如果沒有到指定時間就被通知,則回傳true,否則表示到了指定時間,回傳false,
- signal() :喚醒一個等待執行緒,該執行緒從等待方法回傳前必須獲得與Condition相關的鎖,
- signal()All :喚醒所有等待執行緒,能夠從等待方法回傳的執行緒必須獲得與Condition相關的鎖,
3.2 condition原理:
Condition是AQS的內部類,每個Condition物件都包含一個佇列(等待佇列),等待佇列是一個FIFO的佇列,在佇列中的每個節點都包含了一個執行緒參考,該執行緒就是在Condition物件上等待的執行緒,如果一個執行緒呼叫了Condition.await()方法,那么該執行緒將會釋放鎖、構造成節點加入等待佇列并進入等待狀態,等待佇列的基本結構如下所示,

等待分為首節點和尾節點,當一個執行緒呼叫Condition.await()方法,將會以當前執行緒構造節點,并將節點從尾部加入等待佇列,新增節點后將尾部節點換為新增的節點,節點參考更新本來就是在獲取鎖以后的操作,所以不需要CAS保證,也是執行緒安全的操作,
等待
當執行緒呼叫了await方法以后,執行緒就作為佇列中的一個節點被加入到等待佇列中去了,同時會釋放鎖的占用,當從await方法回傳的時候,一定會獲取condition相關聯的鎖,當等待佇列中的節點被喚醒的時候,則喚醒節點的執行緒開始嘗試獲取同步狀態(鎖),如果不是通過其他執行緒呼叫Condition.signal()方法喚醒,而是對等待執行緒進行中斷,則會拋出InterruptedException例外資訊,
通知
呼叫Condition的signal()方法,將會喚醒在等待佇列中等待最長時間的節點(條件佇列里的首節點),在喚醒節點前,會將節點移到同步佇列中,即可以去競爭獲取鎖,,當前執行緒加入到等待佇列中如圖所示:

在呼叫signal()方法之前必須先判斷是否獲取到了鎖,接著獲取等待佇列的首節點,將其移動到同步佇列并且利用LockSupport喚醒節點中的執行緒,節點從等待佇列移動到同步佇列如下圖所示:

被喚醒的執行緒將從await方法中的while回圈中退出,隨后加入到同步狀態的競爭當中去,成功獲取到競爭的執行緒則會回傳到await方法之前的狀態,
3.3condition 總結
呼叫await方法后,將當前執行緒加入Condition等待佇列中,當前執行緒釋放鎖,否則別的執行緒就無法拿到鎖而發生死鎖,自旋(while)掛起,不斷檢測節點是否在同步佇列中了,如果是則嘗試獲取鎖,否則掛起,當執行緒被signal方法喚醒,被喚醒的執行緒將從await()方法中的while回圈中退出來,然后呼叫acquireQueued()方法競爭同步狀態,
4. CountDownLatch(閂鎖)
CountDownLatch適用于在多執行緒的場景需要等待所有子執行緒全部執行完畢之后再做操作的場景,
初始化一個CountDownLatch實體傳參3,因為我們有3個子執行緒,每次子執行緒執行完畢之后呼叫countDown()方法給計數器-1,主執行緒呼叫await()方法后會被阻塞,直到最后計數器變為0,await()方法回傳,執行完畢,他和join()方法的區別就是join會阻塞子執行緒直到運行結束,而CountDownLatch可以在任何時候讓await()回傳,而且用ExecutorService沒法用join了,相比起來,CountDownLatch更靈活,
CountDownLatch基于AQS實作,volatile變數state維持倒數狀態,多執行緒共享變數可見,
- CountDownLatch通過建構式初始化傳入引數實際為AQS的state變數賦值,維持計數器倒數狀態
- 當主執行緒呼叫await()方法時,當前執行緒會被阻塞,當state不為0時進入AQS阻塞佇列等待,
- 其他執行緒呼叫countDown()時,state值原子性遞減,當state值為0的時候,喚醒所有呼叫await()方法阻塞的執行緒
4.1 CountDownLatch與thread.join()的區別
join,在當前執行緒中,如果呼叫某個thread的join方法,那么當前執行緒就會被阻塞,直到thread執行緒執行完畢,當前執行緒才能繼續執行,join的原理是,不斷的檢查thread是否存活,如果存活,那么讓當前執行緒一直wait,直到thread執行緒終止,執行緒的this.notifyAll 就會被呼叫,
CountDownLatch中我們主要用到兩個方法一個是await()方法,呼叫這個方法的執行緒會被阻塞,另外一個是countDown()方法,呼叫這個方法會使計數器減一,當計數器的值為0時,因呼叫await()方法被阻塞的執行緒會被喚醒,繼續執行,
呼叫join方法需要等待thread執行完畢才能繼續向下執行,而CountDownLatch只需要檢查計數器的值為零就可以繼續向下執行,相比之下,CountDownLatch更加靈活一些,可以實作一些更加復雜的業務場景,
CountDownLatch 小例子實作:
5. CyclicBarrier(籬柵)
CyclicBarrier叫做回環屏障,它的作用是讓一組執行緒全部達到一個狀態之后再全部同時執行,而且他有一個特點就是所有執行緒執行完畢之后是可以重用的,
CountDownLatch非常相似,初始化傳入3個執行緒和一個任務,執行緒呼叫await()之后進入阻塞,計數器-1,當計數器為0時,就去執行CyclicBarrier中建構式的任務,當任務執行完畢后,喚醒所有阻塞中的執行緒,這驗證了CyclicBarrier讓一組執行緒全部達到一個狀態之后再全部同時執行的效果,
CyclicBarrier 是通過 ReentrantLock加鎖,控制count屬性的加減計數的,也是基于AQS
可重用
每個子執行緒呼叫await()計數器減為0之后才開始繼續一起往下執行,此時count會恢復到最初計數,當再次呼叫await()時,就得再次等到計數器為0之后就又一起往下執行,這就是可重用,
CyclicBarrier還是基于AQS實作的,內部維護parties記錄總執行緒數,count用于計數,最開始count=parties,呼叫await()之后count原子遞減,當count為0之后,再次將parties賦值給count,這就是復用的原理,
- 當子執行緒呼叫await()方法時,獲取獨占鎖,同時對count遞減,進入阻塞佇列,然后釋放鎖
- 當第一個執行緒被阻塞同時釋放鎖之后,其他子執行緒競爭獲取鎖,操作同1
- 直到最后count為0,執行CyclicBarrier建構式中的任務,執行完畢之后子執行緒繼續向下執行
CyclicBarrier 小例子實作
CyclicBarrier可重用小例子實作
6. Semaphore(信號燈)
Semaphore叫做信號量,和前面兩個不同的是,他的計數器是遞增的,
稍微和前兩個有點區別,建構式傳入的初始值為0,當子執行緒呼叫release()方法時,計數器遞增,主執行緒acquire()傳參為3 則說明主執行緒一直阻塞,直到計數器為3才會回傳,
Semaphore還還還是基于AQS實作的,同時獲取信號量有公平和非公平兩種策略
- 主執行緒呼叫acquire()方法時,用當前信號量值-需要獲取的值,如果小于0,則進入同步阻塞佇列,大于0則通過CAS設定當前信號量為剩余值,同時回傳剩余值
- 子執行緒呼叫release()給當前信號量值計數器+1(增加的值數量由傳參決定),同時不停的嘗試因為呼叫acquire()進入阻塞的執行緒
Semaphore 小例子實作:
7. unsafe
AtomicInteger的自增函式incrementAndGet()的原始碼時,發現自增函式底層呼叫的是unsafe.getAndAddInt(),但是由于JDK本身只有Unsafe.class,只通過class檔案中的引數名,并不能很好的了解方法的作用,所以我們通過OpenJDK 8 來查看Unsafe的原始碼:
// ------------------------- JDK 8 -------------------------
// AtomicInteger 自增方法
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
unsafe java 魔法類
8. StampedLock
在Java 8中引入了一種鎖的新機制——StampedLock,它可以看成是讀寫鎖的一個改進版本,StampedLock提供了一種樂觀讀鎖的實作,這種樂觀讀鎖類似于無鎖的操作,完全不會阻塞寫執行緒獲取寫鎖,從而**緩解讀多寫少時寫執行緒“饑餓”**現象,由于StampedLock提供的樂觀讀鎖不阻塞寫執行緒獲取讀鎖,當執行緒共享變數從主記憶體load到執行緒作業記憶體時,會存在資料不一致問題,所以當使用StampedLock的樂觀讀鎖時,需要遵從如下圖用例中使用的模式來確保資料的一致性,
小結
CountDownLatch通過計數器提供了比join更靈活的多執行緒控制方式,join會阻塞子執行緒直到運行結束,而CountDownLatch可以在任何時候讓await()回傳,CyclicBarrier也可以達到CountDownLatch的效果,而且有可復用的特點,Semaphore則是采用信號量遞增的方式,開始的時候并不需要關注需要同步的執行緒個數,并且提供獲取信號的公平和非公平策略,
參考:
unsafe java 魔法類
說說CountDownLatch,CyclicBarrier,Semaphore的原理?
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/234356.html
標籤:AI
上一篇:WIFI類物聯網產品配網方式簡述
下一篇:Kubernetes學習總結(4)——Kubernetes v1.20 重磅發布 | 新版本核心主題 & 主要變化解讀
