【漫畫】JAVA併發編程三大Bug源頭(可見性、原子性、有序性)

来源:https://www.cnblogs.com/liuyanling/archive/2020/05/07/12845755.html
-Advertisement-
Play Games

原創聲明:本文轉載自公眾號【胖滾豬學編程】​ 某日,胖滾豬寫的代碼導致了一個生產bug,奮戰到凌晨三點依舊沒有解決問題。胖滾熊一看,只用了一個volatile就解決了。並告知胖滾豬,這是併發編程導致的坑。這讓胖滾豬堅定了要學好併發編程的決心。。於是,開始了我們併發編程的第一課。 序幕 BUG源頭之一 ...


原創聲明:本文轉載自公眾號【胖滾豬學編程】​

某日,胖滾豬寫的代碼導致了一個生產bug,奮戰到凌晨三點依舊沒有解決問題。胖滾熊一看,只用了一個volatile就解決了。並告知胖滾豬,這是併發編程導致的坑。這讓胖滾豬堅定了要學好併發編程的決心。。於是,開始了我們併發編程的第一課。

序幕

con2

BUG源頭之一:可見性

剛剛我們說到,CPU緩存可以提高程式性能,但緩存也是造成BUG源頭之一,因為緩存可以導致可見性問題。我們先來看一段代碼:

private static int count = 0;
public static void main(String[] args) throws Exception {
    Thread th1 = new Thread(() -> {
        count = 10;
    });
    Thread th2 = new Thread(() -> {
        //極小概率會出現等於0的情況
        System.out.println("count=" + count);
    });
    th1.start();
    th2.start();
}

按理來說,應該正確返回10,但結果卻有可能是0。

一個線程對變數的改變另一個線程沒有get到,這就是可見性導致的bug。一個線程對共用變數的修改,另外一個線程能夠立刻看到,我們稱為可見性。

那麼在談論可見性問題之前,你必須瞭解下JAVA的記憶體模型,我繪製了一張圖來描述:

JAVA_

主記憶體(Main Memory)

主記憶體可以簡單理解為電腦當中的記憶體,但又不完全等同。主記憶體被所有的線程所共用,對於一個共用變數(比如靜態變數,或是堆記憶體中的實例)來說,主記憶體當中存儲了它的“本尊”。

工作記憶體(Working Memory)

工作記憶體可以簡單理解為電腦當中的CPU高速緩存,但準確的說它是涵蓋了緩存、寫緩衝區、寄存器以及其他的硬體和編譯器優化。每一個線程擁有自己的工作記憶體,對於一個共用變數來說,工作記憶體當中存儲了它的“副本”。

線程對變數的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。
線程之間無法直接訪問對方的工作記憶體中的變數,線程間變數的傳遞均需要通過主記憶體來完成

現在再回到剛剛的問題,為什麼那段代碼會導致可見性問題呢,根據記憶體模型來分析,我相信你會有答案了。當多個線程在不同的 CPU 上執行時,這些線程操作的是不同的 CPU 緩存。比如下圖中,線程 A 操作的是 CPU-1 上的緩存,而線程 B 操作的是 CPU-2 上的緩存
image

由於線程對變數的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數,那麼對於共用變數V,它們首先是在自己的工作記憶體,之後再同步到主記憶體。可是並不會及時的刷到主存中,而是會有一定時間差。很明顯,這個時候線程 A 對變數 V 的操作對於線程 B 而言就不具備可見性了 。

con3_1

private volatile long count = 0;
​
private void add10K() {
    int idx = 0;
    while (idx++ < 10000) {
        count++;
    }
}
​
public static void main(String[] args) throws InterruptedException {
    TestVolatile2 test = new TestVolatile2();
    // 創建兩個線程,執行 add() 操作
    Thread th1 = new Thread(()->{
        test.add10K();
    });
    Thread th2 = new Thread(()->{
        test.add10K();
    });
    // 啟動兩個線程
    th1.start();
    th2.start();
    // 等待兩個線程執行結束
    th1.join();
    th2.join();
    // 介於1w-2w,即使加了volatile也達不到2w
    System.out.println(test.count);
}
​

con3_2

原創聲明:本文轉載自公眾號【胖滾豬學編程】​

原子性問題

一個不可分割的操作叫做原子性操作,它不會被線程調度機制打斷的,這種操作一旦開始,就一直運行到結束,中間不會有任何線程切換。註意線程切換是重點!

我們都知道CPU資源的分配都是以線程為單位的,並且是分時調用,操作系統允許某個進程執行一小段時間,例如 50 毫秒,過了 50 毫秒操作系統就會重新選擇一個進程來執行(我們稱為“任務切換”),這個 50 毫秒稱為“時間片”。而任務的切換大多數是在時間片段結束以後,

_

那麼線程切換為什麼會帶來bug呢?因為操作系統做任務切換,可以發生在任何一條CPU 指令執行完!註意,是 CPU 指令,CPU 指令,CPU 指令,而不是高級語言里的一條語句。比如count++,在java里就是一句話,但高級語言里一條語句往往需要多條 CPU 指令完成。其實count++包含了三個CPU指令!

  • 指令 1:首先,需要把變數 count 從記憶體載入到 CPU 的寄存器;
  • 指令 2:之後,在寄存器中執行 +1 操作;
  • 指令 3:最後,將結果寫入記憶體(緩存機制導致可能寫入的是 CPU 緩存而不是記憶體)。

小技巧:可以寫一個簡單的count++程式,依次執行javac TestCount.java,javap -c -s TestCount.class得到彙編指令,驗證下count++確實是分成了多條指令的。

volatile雖然能保證執行完及時把變數刷到主記憶體中,但對於count++這種非原子性、多指令的情況,由於線程切換,線程A剛把count=0載入到工作記憶體,線程B就可以開始工作了,這樣就會導致線程A和B執行完的結果都是1,都寫到主記憶體中,主記憶體的值還是1不是2,下麵這張圖形象表示了該歷程:

_

image

原創聲明:本文轉載自公眾號【胖滾豬學編程】​

有序性問題

JAVA為了優化性能,允許編譯器和處理器對指令進行重排序,即有時候會改變程式中語句的先後順序:

例如程式中:“a=6;b=7;”編譯器優化後可能變成“b=7;a=6;”只是在這個程式中不影響程式的最終結果。

有序性指的是程式按照代碼的先後順序執行。但是不要望文生義,這裡的順序不是按照代碼位置的依次順序執行指令,指的是最終結果在我們看起來就像是有序的。

重排序的過程不會影響單線程程式的執行,卻會影響到多線程併發執行的正確性。有時候編譯器及解釋器的優化可能導致意想不到的 Bug。比如非常經典的雙重檢查創建單例對象。

public class Singleton { 
 static Singleton instance; 
 static Singleton getInstance(){ 
 if (instance == null) { 
 synchronized(Singleton.class) { 
 if (instance == null) 
 instance = new Singleton(); 
 } 
 } 
 return instance; 
 } 
 }

你可能會覺得這個程式天衣無縫,我兩次判斷是否為空,還用了synchronized,剛剛也說了,synchronized 是獨占鎖/排他鎖。按照常理來說,應該是這麼一個邏輯:
線程A和B同時進來,判斷instance == null,線程A先獲取了鎖,B等待,然後線程 A 會創建一個 Singleton 實例,之後釋放鎖,鎖釋放後,線程 B 被喚醒,線程 B 再次嘗試加鎖,此時加鎖會成功,然後線程 B 檢查 instance == null 時會發現,已經創建過 Singleton 實例了,所以線程 B 不會再創建一個 Singleton 實例。

但多線程往往要有非常理性的思維,我們先分析一下 instance = new Singleton()這句話,根據剛剛原子性說到的,一句高級語言在cpu層面其實是多條指令,這也不例外,我們也很熟悉new了,它會分為以下幾條指令:
1、分配一塊記憶體 M;
2、在記憶體 M 上初始化 Singleton 對象;
3、然後 M 的地址賦值給 instance 變數。

如果真按照上述三條指令執行是沒問題的,但經過編譯優化後的執行路徑卻是這樣的:
1、分配一塊記憶體 M;
2、將 M 的地址賦值給 instance 變數;
3、最後在記憶體 M 上初始化 Singleton 對象

假如當執行完指令 2 時恰好發生了線程切換,切換到了線程 B 上;而此時線程 B 也執行 getInstance() 方法,那麼線程 B 在執行第一個判斷時會發現 instance != null ,所以直接返回 instance,而此時的 instance 是沒有初始化過的,如果我們這個時候訪問 instance 的成員變數就可能觸發空指針異常,如圖所示:

_

con4

總結

併發程式是一把雙刃劍,一方面大幅度提升了程式性能,另一方面帶來了很多隱藏的無形的難以發現的bug。我們首先要知道併發程式的問題在哪裡,只有確定了“靶子”,才有可能把問題解決,畢竟所有的解決方案都是針對問題的。併發程式經常出現的詭異問題看上去非常無釐頭,但是只要我們能夠深刻理解可見性、原子性、有序性在併發場景下的原理,很多併發 Bug 都是可以理解、可以診斷的。
總結一句話:可見性是緩存導致的,而線程切換會帶來的原子性問題,編譯優化會帶來有序性問題。至於怎麼解決呢!欲知後事如何,且聽下回分解。

原創聲明:本文轉載自公眾號【胖滾豬學編程】​

本文轉載自公眾號【胖滾豬學編程】 用漫畫讓編程so easy and interesting!歡迎關註!形象來源於微信表情包【胖滾家族】喜歡可以下載哦~


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • json 什麼是JSON "JSON" ( "JavaScript" Object Notation) 是一種輕量級的數據交換格式。它基於 "ECMAScript" 的一個子集。 JSON採用完全獨立於語言的文本格式,但是也使用了類似於C語言家族的習慣(包括 "C" 、C++、 "C " 、 "Ja ...
  • 使用JQuery完成表單的校驗(擴展) 事件: ​ 獲得焦點事件: onfocus ​ 失去焦點事件: onblur ​ 按鍵抬起事件: onkeyup ​ 滑鼠移入: onmouseenter ​ 滑鼠移出: onmouseout JS引入外部文件 : script標簽 需求分析 在用戶提交表單的 ...
  • 使用JQuery完成下列列表左右選擇 Js相關技術 ​ select下拉列表 ​ multiple 允許多選 ​ ondblclick : 雙擊事件 ​ for迴圈遍歷,一邊遍歷一邊移除出現的問題 需求分析 我們的商品通常包含已經有了的, 還有沒有的,現在我們需要有一個頁面用於動態編輯這些商品 步驟 ...
  • 1 #include <Windows.h> 2 #include <iostream> 3 using namespace std; 4 5 int main() 6 { 7 while(true) 8 { 9 if(-32767 == GetAsyncKeyState('A')) //不支持大小 ...
  • 1. OpenCV讀取圖片 1.1 簡述 OpenCV讀取圖片的方法是cv2.imread(),讀取出來圖片的格式是BGR與常規的彩色圖像的格式(RGB)相反,這一點一定要註意。 OpenCV顯示圖片的方法是cv2.imshow(),顯示的格式是BGR。 小生就言於此O(∩_∩)O哈哈~,直接上例子 ...
  • 消息隊列中間件是分散式系統中重要的組件,主要解決應用耦合,非同步消息,流量削鋒等問題。實現高性能,高可用,可伸縮和最終一致性架構。是大型分散式系統不可缺少的中間件。消息形式支持點對點和訂閱-發佈。 ActiveMQ是什麼 1、ActiveMQ是消息隊列技術,為解決高併發問題而生 2、ActiveMQ生 ...
  • 我們在的項目組呢,有一項工作是,收郵件(很大程度上使用郵件是為了存個底),然後從我們的系統裡邊查一下相關信息,然後回覆個郵件的工作。雖然工作量並不大,但是會把時間切的稀碎。為了拯救我的時間,所以做了一個郵件的值班機器人。讓他來頂替我自動回覆郵件,考慮到這個東西應該也有不少人會用得到,所以就把這個東西 ...
  • 數據很重要 在介紹MyBatis事務之前,先普及下資料庫事務相關知識 事務(Transaction)是訪問並可能更新資料庫中各種數據項的一個程式執行單元(unit)。事務通常由高級資料庫操縱語言或編程語言(如SQL,C++或Java)書寫的用戶程式的執行所引起,並用形如begin transacti ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...