一文徹底搞懂CAS實現原理 & 深入到CPU指令
本文導讀:
- 前言
- 如何保障執行緒安全
- CAS原理剖析
- CPU如何保證原子操作
- 解密CAS底層指令
- 小結
朋友,文章優先發布在公眾號上,如果你願意,可以掃右側二維碼支援一下下~,謝謝!
前言
日常編碼過程中,基本不會直接用到 CAS 操作,都是通過一些JDK 封裝好的併發工具類來使用的,在 java.util.concurrent 包下。
但是面試時 CAS 還是個高頻考點,所以呀,你還不得不硬著頭皮去死磕一下這塊的技能點,總比一問三不知強吧?
一般都是先針對一些簡單的併發知識問起,還有的面試官,比較直接:
面試官:Java併發工具類中的 CAS 機制講一講?
小東:額?大腦中問自己「啥是 CAS?」我聽過的,容我想一想...
一分鐘過去了...
小東:嘿嘿~,這塊我看過的,記不大清楚了。
面試官:好的,今天先到這吧~
小東:在路上
當然 CAS 你若真不懂,你可以引導面試官到你擅長的技術點上,用你的其他技能亮點扳回一局。
接下來,我們通過一個示例程式碼來說:
// 類的成員變數 static int data = 0; // main方法內程式碼 IntStream.range(0, 2).forEach((i) -> { new Thread(() -> { try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } IntStream.range(0, 100).forEach(y -> { data++; }); }).start(); }); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(data); }
結合圖示理解:
上述程式碼,問題很明顯,data 是類中的成員變數,int 型別,即共享的資源。當多個執行緒同時
執行 data++
操作時,結果可能不等於 200,為了模擬出效果,執行緒中 sleep 了 20 毫秒,讓執行緒就緒,程式碼執行多次,結果都不是 200 。
如何保障執行緒安全
示例程式碼執行結果表明了,多個執行緒同時操作共享變數導致了結果不準確,執行緒是不安全的。如何解決呢?
方案一:使用 synchronized 關鍵字
使用 synchronized 關鍵字,執行緒內使用同步程式碼塊,由JVM自身的機制來保障執行緒的安全性。
synchronized 關鍵程式碼:
// 類中定義的Object鎖物件 Object lock = new Object(); // synchronized 同步塊 () 中使用 lock 物件鎖定資源 IntStream.range(0, 100).forEach(y -> { synchronized (lock.getClass()) { data++; } });
方案二:使用 Lock 鎖
高併發場景下,使用 Lock 鎖要比使用 synchronized 關鍵字,在效能上得到極大的提高。
因為 Lock 底層是通過 AQS + CAS 機制來實現的。關於 AQS 機制可以參見往期文章 <<通過通過一個生活中的案例場景,揭開併發包底層AQS的神祕面紗>> 。CAS 機制會在文章中下面講到。
使用 Lock 的關鍵程式碼:
// 類中定義成員變數
Lock lock = new ReentrantLock();
// 執行 lock() 方法加鎖,執行 unlock() 方法解鎖
IntStream.range(0, 100).forEach(y -> {
lock.lock();
data++;
lock.unlock();
});
結合圖示理解:
方案三:使用 Atomic 原子類
除上面兩種方案還有沒有更為優雅的方案?synchronized 的使用在 JDK1.6 版本以後做了很多優化,如果併發量不大,相比 Lock 更為安全,效能也能接受,因其得益於 JVM 底層機制來保障,自動釋放鎖,無需硬編碼方式釋放鎖。而使用 Lock 方式,一旦 unlock() 方法使用不規範,可能導致死鎖。
JDK 併發包所有的原子類如下所示:
使用 AtomicInteger 工具類實現程式碼:
// 類中成員變數定義原子類
AtomicInteger atomicData = new AtomicInteger();
// 程式碼中原子類的使用方式
IntStream.range(0, 2).forEach((i) -> {
new Thread(() -> {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
IntStream.range(0, 100).forEach(y -> {
// 原子類自增
atomicData.incrementAndGet();
});
}).start();
});
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 通過 get () 方法獲取結果
System.out.println(atomicData.get());
結合圖示理解:
之所以推薦使用 Atomic 原子類,因為其底層基於 CAS 樂觀鎖來實現的,下文會詳細分析。
方案四:使用 LongAdder 原子類
LongAdder 原子類在 JDK1.8 中新增的類, 跟方案三中提到的 AtomicInteger 類似,都是在 java.util.concurrent.atomic 併發包下的。
LongAdder 適合於高併發場景下,特別是寫大於讀的場景,相較於 AtomicInteger、AtomicLong 效能更好,代價是消耗更多的空間,以空間換時間。
使用 LongAdder 工具類實現程式碼:
// 類中成員變數定義的LongAdder
LongAdder longAdderData = new LongAdder();
// 程式碼中原子類的使用方式
IntStream.range(0, 2).forEach((i) -> {
new Thread(() -> {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
IntStream.range(0, 100).forEach(y -> {
// 使用 increment() 方法自增
longAdderData.increment();
});
}).start();
});
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 使用 sum() 獲取結果
System.out.println(longAdderData.sum());
結合圖示理解:
但是,如果使用了 LongAdder 原子類,當然其底層也是基於 CAS 機制實現的。LongAdder 內部維護了 base 變數和 Cell[] 陣列,當多執行緒併發寫的情況下,各個執行緒都在寫入自己的 Cell 中,LongAdder 操作後返回的是個近似準確的值,最終也會返回一個準確的值。
換句話說,使用了 LongAdder 後獲取的結果並不是實時的,對實時性要求高的還是建議使用其他的原子類,如 AtomicInteger 等。
volatile 關鍵字方案?
可能還有朋友會說,還想到另外一種方案:使用** volatile
** 關鍵字啊。
經過驗證,是不可行的,大家可以試試,就本文給出的示例程式碼直接執行,結果都不等於 200,說明執行緒仍然是不安全的。
data++ 自增賦值並不是原子的,跟 Java記憶體模型有關。
在非執行緒安全的圖示中有標註執行執行緒本地,會有個記憶體副本,即本地的工作記憶體,實際執行過程會經過如下幾個步驟:
(1)執行執行緒從本地工作記憶體讀取 data,如果有值直接獲取,如果沒有值,會從主記憶體讀取,然後將其放到本地工作記憶體當中。
(2)執行執行緒在本地工作記憶體中執行 +1 操作。
(3)將 data 的值寫入主記憶體。
結論:請記住!
一個變數簡單的讀取和賦值操作是原子性的,將一個變數賦值給另外一個變數不是原子性的。
Java記憶體模型(JMM)僅僅保障了變數的基本讀取和賦值操作是原子性的,其他均不會保證的。如果想要使某段程式碼塊要求具備原子性,就需要使用 synchronized 關鍵字、併發包中的 Lock 鎖、併發包中 Atomic 各種型別的原子類來實現,即上面我們提到的四種方案都是可行的。
而 volatile
關鍵字修飾的變數,恰恰是不能保障原子性的,僅能保障可見性和有序性。
CAS原理剖析
CAS 被認為是一種樂觀鎖,有樂觀鎖,相對應的是悲觀鎖。
在上述示例中,我們使用了 synchronized,如果線上程競爭壓力大的情況下,synchronized 內部會升級為重量級鎖,此時僅能有一個執行緒進入程式碼塊執行,如果這把鎖始終不能釋放,其他執行緒會一直阻塞等待下去。此時,可以認為是悲觀鎖。
悲觀鎖會因執行緒一直阻塞導致系統上下文切換,系統的效能開銷大。
那麼,我們可以用樂觀鎖來解決,所謂的樂觀鎖,其實就是一種思想。
樂觀鎖,會以一種更加樂觀的態度對待事情,認為自己可以操作成功。當多個執行緒操作同一個共享資源時,僅能有一個執行緒同一時間獲得鎖成功,在樂觀鎖中,其他執行緒發現自己無法成功獲得鎖,並不會像悲觀鎖那樣阻塞執行緒,而是直接返回,可以去選擇再次重試獲得鎖,也可以直接退出。
CAS 正是樂觀鎖的核心演算法實現。
在示例程式碼的方案中都提到了 AtomicInteger、LongAdder、Lock鎖底層,此外,當然還包括 java.util.concurrent.atomic 併發包下的所有原子類都是基於 CAS 來實現的。
以 AtomicInteger 原子整型類為例,一起來分析下 CAS 底層實現機制。
atomicData.incrementAndGet()
原始碼如下所示:
// 提供自增易用的方法,返回增加1後的值
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// 額外提供的compareAndSet方法
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// Unsafe 類的提供的方法
public final int getAndAddInt (Object o,long offset, int delta){
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
我們看到了 AtomicInteger 內部方法都是基於 Unsafe 類實現的,Unsafe 類是個跟底層硬體CPU指令通訊的複製工具類。
由這段程式碼看到:
unsafe.compareAndSwapInt(this, valueOffset, expect, update)
所謂的 CAS,其實是個簡稱,全稱是 Compare And Swap,對比之後交換資料。
上面的方法,有幾個重要的引數:
(1)this,Unsafe 物件本身,需要通過這個類來獲取 value 的記憶體偏移地址。
(2)valueOffset,value 變數的記憶體偏移地址。
(3)expect,期望更新的值。
(4)update,要更新的最新值。
如果原子變數中的 value 值等於 expect,則使用 update 值更新該值並返回 true,否則返回 false。
再看如何獲得 valueOffset的:
// Unsafe例項
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
// 獲得value在AtomicInteger中的偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 實際變數的值
private volatile int value;
這裡看到了 value 實際的變數,是由 volatile 關鍵字修飾的,為了保證在多執行緒下的記憶體可見性。
為何能通過 Unsafe.getUnsafe() 方法能獲得 Unsafe 類的例項?其實因為 AtomicInteger 類也在 rt.jar 包下面的,所以 AtomicInteger 類就是通過 Bootstrap 根類載入器進行載入的。
原始碼如下所示:
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
// Bootstrap 類載入器是C++的,正常返回null,否則就拋異常。
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
類載入器委託關係:
CPU如何實現原子操作
CPU 處理器速度遠遠大於在主記憶體中的,為了解決速度差異,在他們之間架設了多級快取,如 L1、L2、L3 級別的快取,這些快取離CPU越近就越快,將頻繁操作的資料快取到這裡,加快訪問速度 ,如下圖所示:
現在都是多核 CPU 處理器,每個 CPU 處理器內維護了一塊位元組的記憶體,每個核心內部維護著一塊位元組的快取,當多執行緒併發讀寫時,就會出現快取資料不一致的情況。
此時,處理器提供:
- 匯流排鎖定
當一個處理器要操作共享變數時,在 BUS 總線上發出一個 Lock 訊號,其他處理就無法操作這個共享變量了。
缺點很明顯,匯流排鎖定在阻塞其它處理器獲取該共享變數的操作請求時,也可能會導致大量阻塞,從而增加系統的效能開銷。
- 快取鎖定
後來的處理器都提供了快取鎖定機制,也就說當某個處理器對快取中的共享變數進行了操作,其他處理器會有個嗅探機制,將其他處理器的該共享變數的快取失效,待其他執行緒讀取時會重新從主記憶體中讀取最新的資料,基於 MESI 快取一致性協議來實現的。
現代的處理器基本都支援和使用的快取鎖定機制。
注意:
有如下兩種情況處理器不會使用快取鎖定:
(1)當操作的資料跨多個快取行,或沒被快取在處理器內部,則處理器會使用匯流排鎖定。
(2)有些處理器不支援快取鎖定,比如:Intel 486 和 Pentium 處理器也會呼叫匯流排鎖定。
解密CAS底層指令
其實,掌握以上內容,對於 CAS 機制的理解相對來說算是比較清楚了。
當然,如果感興趣,也可以繼續深入學習用到了哪些硬體 CPU 指令。
底層硬體通過將 CAS 裡的多個操作在硬體層面語義實現上,通過一條處理器指令保證了原子性操作。這些指令如下所示:
(1)測試並設定(Tetst-and-Set)
(2)獲取並增加(Fetch-and-Increment)
(3)交換(Swap)
(4)比較並交換(Compare-and-Swap)
(5)載入連結/條件儲存(Load-Linked/Store-Conditional)
前面三條大部分處理器已經實現,後面的兩條是現代處理器當中新增加的。而且根據不同的體系結構,指令存在著明顯差異。
在IA64,x86 指令集中有 cmpxchg 指令完成 CAS 功能,在 sparc-TSO 也有 casa 指令實現,而在 ARM 和 PowerPC 架構下,則需要使用一對 ldrex/strex 指令來完成 LL/SC 的功能。在精簡指令集的體系架構中,則通常是靠一對兒指令,如:load and reserve 和 store conditional 實現的,在大多數處理器上 CAS 都是個非常輕量級的操作,這也是其優勢所在。
sun.misc.Unsafe 中 CAS 的核心方法:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
這三個方法可以對應去檢視 openjdk 的 hotspot 原始碼:
原始碼位置:hotspot/src/share/vm/prims/unsafe.cpp
#define FN_PTR(f) CAST_FROM_FN_PTR(void*, &f)
{CC"compareAndSwapObject", CC"("OBJ"J"OBJ""OBJ")Z", FN_PTR(Unsafe_CompareAndSwapObject)},
{CC"compareAndSwapInt", CC"("OBJ"J""I""I"")Z", FN_PTR(Unsafe_CompareAndSwapInt)},
{CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z", FN_PTR(Unsafe_CompareAndSwapLong)},
上述三個方法,最終在 hotspot 原始碼實現中都會呼叫統一的 cmpxchg 函式,可以在 hotspot 原始碼中找到核心程式碼。
原始碼地址:hotspot/src/share/vm/runtime/Atomic.cpp
cmpxchg 函式原始碼:
jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte*dest, jbyte compare_value) {
assert (sizeof(jbyte) == 1,"assumption.");
uintptr_t dest_addr = (uintptr_t) dest;
uintptr_t offset = dest_addr % sizeof(jint);
volatile jint*dest_int = ( volatile jint*)(dest_addr - offset);
// 物件當前值
jint cur = *dest_int;
// 當前值cur的地址
jbyte * cur_as_bytes = (jbyte *) ( & cur);
// new_val地址
jint new_val = cur;
jbyte * new_val_as_bytes = (jbyte *) ( & new_val);
// new_val存exchange_value,後面修改則直接從new_val中取值
new_val_as_bytes[offset] = exchange_value;
// 比較當前值與期望值,如果相同則更新,不同則直接返回
while (cur_as_bytes[offset] == compare_value) {
// 調用匯編指令cmpxchg執行CAS操作,期望值為cur,更新值為new_val
jint res = cmpxchg(new_val, dest_int, cur);
if (res == cur) break;
cur = res;
new_val = cur;
new_val_as_bytes[offset] = exchange_value;
}
// 返回當前值
return cur_as_bytes[offset];
}
原始碼中具體變數添加了註釋,因為都是 C++ 程式碼,所以作為了解即可 ~
jint res = cmpxchg(new_val, dest_int, cur);
這裡就是呼叫了彙編指令 cmpxchg 了,其中也是包含了三個引數,跟CAS上的引數能對應上。
總結
任何技術都要找到適合的場景,都不是萬能的,CAS 機制也一樣,也有副作用。
問題1:
作為樂觀鎖的一種實現,當多執行緒競爭資源激烈的情況下,而且鎖定的資源處理耗時,那麼其他執行緒就要考慮自旋的次數限制,避免過度的消耗 CPU。
另外,可以考慮上文示例程式碼中提到的 LongAdder 來解決,LongAdder 以空間換時間的方式,來解決 CAS 大量失敗後長時間佔用 CPU 資源,加大了系統性能開銷的問題。
問題2:
A-->B--->A 問題,假設有一個變數 A ,修改為B,然後又修改為了 A,實際已經修改過了,但 CAS 可能無法感知,造成了不合理的值修改操作。
整數型別還好,如果是物件引用型別,包含了多個變數,那怎麼辦?加個版本號或時間戳唄,沒問題!
JDK 中 java.util.concurrent.atomic 併發包下,提供了 AtomicStampedReference,通過為引用建立個 Stamp 類似版本號的方式,確保 CAS 操作的正確性。
希望此文大家收藏消化,CAS 在JDK併發包底層實現中是個非常重要的演算法。
撰文不易,文章中有什麼問題還請指正!
歡迎關注我的公眾號,掃二維碼關注獲得更多精彩文章,與你一同成長~
相關推薦
一文徹底搞懂CAS實現原理 & 深入到CPU指令
本文導讀: 前言 如何保障執行緒安全 CAS原理剖析 CPU如何保證原子操作 解密CAS底層指令 小結 朋友,文章優先發布在公眾號上,如果你願意,可以掃右側二維碼支援一下下~,謝謝! 前言 日常編碼過程中,基本不會直接用到 CAS 操作,都是通過一些JDK 封裝好的併發工具類來使用的,在 java.
一文徹底搞懂python中的self
在介紹Python的self用法之前,先來介紹下Python中的類和例項…… 我們知道,面向物件最重要的概念就是類(class)和例項(instance),類是抽象的模板,比如學生這個抽象的事物,可以用一個Student類來表示。而例項是根據類創建出來的一個個具體的“物件”,每一個物件都
一文徹底搞懂python的垃圾回收機制
一 、什麼是記憶體管理和垃圾回收 Python GC主要使用引用計數(reference counting)來跟蹤和回收垃圾。在引用計數的基礎上,通過“標記-清除”(mark and sweep)解決容器物件可能產生的迴圈引用問題,通過“分代回收”(genera
一文徹底搞懂卷積神經網路的“感受野”,看不懂你來找我!
一、什麼是“感受野” 1.1 感受野的概念 “感受野”的概念來源於生物神經科學,比如當我們的“感受器”,比如我們的手受到刺激之後,會將刺激傳輸至中樞神經,但是並不是一個神經元就能夠接受整個面板的刺激,因為面板面積大,一個神經元可想而知肯定接受不完,而且我們同
一文輕鬆搞懂redis叢集原理及搭建與使用
三種叢集策略: https://blog.csdn.net/q649381130/article/details/79931791 https://blog.csdn.net/qq_34337272/article/details/79982529 redis主從複製和叢集實現原理:
一文徹底搞懂linux全域性環境變數生效順序
一、前言在登入linux系統並啟動一個bash shell時,預設情況下bash會在若干個檔案中查詢環境變數的設定。這些檔案可統稱為系統環境檔案。⭐️bash檢查環境變數檔案的情況取決於系統執行shell的方式 二、系統執行Shell的方式1、通過系統使用者登陸後預設執行的shell2、非登入互動式執行sh
一文徹底搞懂linux全局環境變量生效順序
錯誤 一個 支持 個人 檢查 技術分享 生效 water 正版 一、前言在登錄linux系統並啟動一個bash shell時,默認情況下bash會在若幹個文件中查找環境變量的設置。這些文件可統稱為系統環境文件。??bash檢查環境變量文件的情況取決於系統運行shell的方式
一文徹底搞懂股權投資中GP/LP關係! | 資本智庫
https://www.sohu.com/a/157617708_393543 國內股權投資市場是一個西學東漸的過程。三十餘年來,伴隨國內經濟體制改革的不斷深化、創新創業的全面開展,股權投資行業從無到有,從不毛沙漠變成燦然綠洲,雙創口號下,大勢依然強勁,經歷過功過成敗、喜怒反思,正昂首闊步邁向“
一文徹底搞懂BERT
一文徹底搞懂BERT 一、什麼是BERT? 沒錯下圖中的小黃人就是文字的主角Bert ,而紅色的小紅人你應該也聽過,他就是ELMo。2018年釋出的BERT 是一個 NLP 任務的里程碑式模型,它的釋出勢必會帶來一個 NLP 的新時代。B
一文徹底搞懂JS前端5大模組化規範及其區別
## 碼文不易,轉載請帶上本文連結,感謝~ https://www.cnblogs.com/echoyya/p/14577243.html [toc] 在開發以及面試中,總是會遇到有關模組化相關的問題,始終不是很明白,不得要領,例如以下問題,回答起來也是模稜兩可,希望通過這篇文章,能夠讓大家瞭解十之一二,
一文快速搞懂MySQL InnoDB事務ACID實現原理
test 用戶 bin 輔助索引 做的 text 訪問 通過 可重復 【51CTO.com原創稿件】說到數據庫事務,想到的就是要麽都做修改,要麽都不做,或者是 ACID 的概念。其實事務的本質就是鎖、並發和重做日誌的結合體。 這一篇主要講一下 InnoDB 中的事務到底是如
JAVA併發程式設計:一文全面搞懂併發程式設計
序言 哈哈哈哈哈哈,原諒我這個標題黨哈,我現在也只是剛入門併發程式設計,學習的過程過程中發現好多專業詞語不會讀或者是讀不準。。所以就彙總了下,把一些比較難讀的給標上英標啦。。 正文 callable:['kɔ:ləbl] 一個類似runnable的介面,方法可以有返回值
這一次徹底搞懂 Git Rebase
一、起因 上線構建的過程中掃了一眼程式碼變更,突然發現, commit 提交竟然多達 62 次。我們來看看都提交了什麼東西: 這裡我們先不說 git 提交規範,就單純這麼多次無用的 commit 就很讓人不舒服。可能
一文輕鬆搞懂Vuex
概念: Vuex 是一個專為 Vue.js 應用程式開發的狀態管理模式(官網地址:https://vuex.vuejs.org/zh/)。它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。 換成我們大白話來說:Vuex就是一個狀態管理模式,可以簡單的理解為
這一次 徹底搞懂Vue針對陣列和雙向繫結(MVVM)的處理方式
歡迎關注我的部落格:https://github.com/wangweianger/myblog Vue內部實現了一組觀察陣列的
一文徹底搞定譜聚類
Clustering 聚類 譜聚類 上文我們引入了是聚類,並介紹了第一種聚類演算法K-means。今天,我們來介紹一種流行的聚類演算法——譜聚類(Spectral Clustering),它的實現簡單,而且效果往往好於傳統的聚類演算法,如k-means,但是其背後的原理涉及了很多重要而複雜的知識,如圖論,矩陣
一本徹底搞懂MySQL索引優化EXPLAIN百科全書
1、MySQL邏輯架構 日常在CURD的過程中,都避免不了跟資料庫打交道,大多數業務都離不開資料庫表的設計和SQL的編寫,那如何讓你編寫的SQL語句效能更優呢? 先來整體看下MySQL邏輯架構圖: MySQL整體邏輯架構圖可以分為Server和儲存引擎層。 Server層: Server層涵蓋了MySQL
一文徹底讀懂MySQL事務的四大隔離級別
## 前言 之前分析一個死鎖問題,發現自己對資料庫隔離級別理解還不夠清楚,所以趁著這幾天假期,整理一下MySQL事務的四大隔離級別相關知識,希望對大家有幫助~ ![](https://user-gold-cdn.xitu.io/2020/4/5/171498a008c91b4c?w=985&h=6
面試都在問的「微服務」「RPC」「服務治理」「下一代微服務」一文帶你徹底搞懂!
❝ 文章每週持續更新,各位的「三連」是對我最大的肯定。可以微信搜尋公眾號「 後端技術學堂 」第一時間閱讀(一般比部落格早更新一到兩篇) ❞ 單體式應用程式 與微服務相對的另一個概念是傳統的「單體式應用程式」( Monolithic application ),單體式應用內部包含了所有需要的服務。而且各個服務功
一文搞懂引數傳遞原理
![](https://i.loli.net/2021/01/12/1s37bXrxSl8Cp2f.jpg) # 前言 最近一年多的時間陸續接觸了一些對我來說陌生的語言,主要就是 `Python` 和 `Go`,期間為了快速實現需求只是依葫蘆畫瓢的擼程式碼;並沒有深究一些細節與原理。 就拿引數傳遞一事來