深刻理解JAVA併發中的有序性問題和解決之道

来源:https://www.cnblogs.com/alvinscript/archive/2022/12/05/16953848.html
-Advertisement-
Play Games

問題 Java併發情況下總是會遇到各種意向不到的問題,比如下麵的代碼: int num = 0; boolean ready = false; // 線程1 執行此方法 public void actor1(I_Result r) { if(ready) { r.r1 = num + num; } ...


問題

Java併發情況下總是會遇到各種意向不到的問題,比如下麵的代碼:

int num = 0;

boolean ready = false;
// 線程1 執行此方法
public void actor1(I_Result r) {
 if(ready) {
 	r.r1 = num + num;
 } else {
 	r.r1 = 1;
 }
}
// 線程2 執行此方法
public void actor2(I_Result r) { 
 num = 2;
 ready = true; 
}
  • 線程1中如果發現ready=true,那麼r1的值等於num + num,否則等於1,然後將結果保存到I_Result對象中
  • 線程2中先修改num=2,然後設置ready=true

那大家覺得I_Result中的r1值可能是多少呢?

  1. r1值等於4, 這個大家都能想到, CPU先執行了線程2,然後執行線程1
  2. r1值等於1,這個也容易理解,CPU先執行了線程1,然後執行線程2
  3. 那我如果說r1值有可能等於0,大家可能覺得離譜,不信的話,我們驗證下。

壓測驗證結果

由於併發問題出現的概率比較低,我們可以使用openjdk提供的jcstress框架進行壓測,就能夠出現各種可能的情況。

jcstress:全名The Java Concurrency Stress tests,是一個實驗工具和一套測試工具,用於幫助研究JVM、類庫和硬體中併發支持的正確性。詳細使用可以參考文章:https://www.cnblogs.com/wwjj4811/p/14310930.html

  1. 生成壓測工程
mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jcstress -DarchetypeArtifactId=jcstress-java-test-archetype -DarchetypeVersion=0.5 -DgroupId=com.alvin -DartifactId=juc-order -Dversion=1.0

生成的工程代碼如下圖:

  1. 填充測試內容

  • 方法actor1是壓測第一個線程乾的活,將結果保存到I_Result中。
  • 方法actor2是壓測第二個線程乾的活
  • 類前面的@Outcome註解用來展示驗證結果,特別是id="0"這個是我們感興趣的結果
  1. 運行壓測工程
mvn clean install 
java -jar target/jcstress.jar
  1. 查看運行結果

運行結果如下圖所示:

  • 有4000多次出現了0的結果
  • 大部分情況的結果還是1和4

你是不是還是很困惑,其實這就是併發執行的一些坑,我們下麵來解釋下原因。

原因分析

如果先要出現r1的值等於0,那麼有一個可能0+0=0,那麼也就是num=0

你可能想num怎麼可能等於0,代碼邏輯明明是先設置num=2,然後才修改ready=true, 最後才會走到num+num 的邏輯啊....

在併發的世界里,我們千萬不要被固有的思維限制了,那是不是有可能num=2ready=true的執行順序發生了變化呢。如果你想到這裡,也基本接近真相了。

原因: JAVA中在指令不存在依賴的情況下,會進行順序的調整,這種現象叫做指令重排序,是 JIT 編譯器在運行時的一些優化。這也是為什麼出現0的根本原因。

指令重排不會影響單線程執行的結果,但是在多線程的情況下,會有個可能出現問題。

理解指令重排序

前面提到出現問題的原因是因為指令重排序,你可能還是不大理解指令重排序究竟是什麼,以及它的作用,那我這邊用一個魚罐頭的故事帶大家理解下。

我們可以把工人當做CPU,魚當做指令,工人加工一條魚需要 50 分鐘,如果一條魚、一條魚順序加工,這樣是不是比較慢?

沒辦法得優化下,不然要喝西北風了,發現每個魚罐頭的加工流程有 5 個步驟:

  • 去鱗清洗 10分鐘
  • 蒸煮瀝水 10分鐘
  • 加註湯料 10分鐘
  • 殺菌出鍋 10分鐘
  • 真空封罐 10分鐘

每個步驟中也是用到不同的工具,那能否可以並行呢?如下圖所示:

我們發現中間用很多步驟是並行做的,大大的提高了效率。但是在並行加工魚的過程中,就會出現順序的調整,比如先做第二條的魚的某個步驟,然後在做第一條魚的步驟。

現代 CPU 支持多級指令流水線,幾乎所有的馮•諾伊曼型電腦的 CPU,其工作都可以分為 5 個階段:取指令、指令解碼、執行指令、訪存取數和結果寫回,可以稱之為五級指令流水線。CPU 可以在一個時鐘周期內,同時運行五條指令的不同階段(每個線程不同的階段),本質上流水線技術並不能縮短單條指令的執行時間,但變相地提高了指令地吞吐率。

處理器在進行重排序時,必須要考慮指令之間的數據依賴性

  • 單線程環境也存在指令重排,由於存在依賴性,最終執行結果和代碼順序的結果一致
  • 多線程環境中線程交替執行,由於編譯器優化重排,會獲取其他線程處在不同階段的指令同時執行

volatile關鍵字

那麼對於上面的問題,如何解決呢?

使用volatile關鍵字。

volatile 的底層實現原理是記憶體屏障,Memory Barrier(Memory Fence)

  • volatile 變數的寫指令後會加入寫屏障
  • volatile 變數的讀指令前會加入讀屏障

記憶體屏障本質上是一個CPU指令,形象點理解就是一個柵欄,攔在那裡,無法跨越。

記憶體屏障分為寫屏障和讀屏障,有什麼有呢?

  1. 保證可見性
  • 寫屏障保證在該屏障之前的,對共用變數的改動,都同步到主存當中
  • 讀屏障保證在該屏障之後,對共用變數的讀取,載入的是主存中最新數據
  1. 保證有序性
  • 寫屏障會確保指令重排序時,不會將寫屏障之前的代碼排在寫屏障之後
  • 讀屏障會確保指令重排序時,不會將讀屏障之後的代碼排在讀屏障之前

回到前面的問題,如果對ready加了volatile以後,那麼num=2就無法到後面去了,同樣讀取也是,如上圖所示。

final底層也是通過記憶體屏障實現的,它與volatile一樣。

  • 對final變數的寫指令加入寫屏障。也就是類初始化的賦值的時候會加上寫屏障。
  • 對final變數的讀指令加入讀屏障。載入記憶體中final變數的最新值。

總結

JAVA併發中的有序性問題其實比較難理解,本文通過一個例子驗證了併發情況下會出現有序性的問題,從而引發意想不到的結果。這個主要的原因是為了提高性能,指令會發生重排序導致的。為瞭解決這樣的問題,我們可以使用volatile這個關鍵字修飾變數,它能夠保證有序性和可見性,但是無法保證原子性。如果以後遇到一些成員變數或者靜態變數就要特別註意了,需要分析併發情況下會有哪些問題。

如果本文對你有幫助的話,請留下一個贊吧

更多技術幹活和學習資料盡在個人公眾號——JAVA旭陽

本文來自博客園,作者:JAVA旭陽,轉載請註明原文鏈接:https://www.cnblogs.com/alvinscript/p/16953848.html


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

-Advertisement-
Play Games
更多相關文章
  • 我國製造業擁有31個大類、179個中類和609個小類,是全球產業門類最齊全、產業體系最完整的製造業。二十大報告中強調:“堅持把發展經濟的著力點放在實體經濟上”“推動製造業高端化、智能化、綠色化發展”。這為中國製造邁向高質量發展指明瞭方向。 疫情發生以來,製造業面臨用工難、復產復工難、成本高、數據管理 ...
  • 1.4 Apache Hadoop 完全分散式集群搭建 軟體和操作系統版本 Hadoop框架是採用Java語言編寫,需要java環境(jvm) JDK版本:JDK8版本 集群: 知識點學習:統一使用vmware虛擬機虛擬三台linux節點,linux操作系統:Centos7 生產階段:建議最少5台服 ...
  • 本次我把CSS中的重難點整理出來,總共54個核心知識點,供大家複習,希望能幫到大家。這些重難點是進階高薪必需要掌握的知識點,同時也是面試必問的內容。 因為涉及的內容較多,我分5篇內容發出來,好逐一進行讓大家消化這些內容,本次我把前1-12個CSS重難點整理出來,具體內容如下: CSS選擇器與優先順序 ...
  • jQuery05 9.作業 9.1homework01 對多選框進行操作,輸出選中的多選框的個數,並且把選中愛好的名稱顯示。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>homework01</title ...
  • 前端開發之JavaScript 一、JavaScript簡介 ​ Javascript(簡稱‘JS’),是一門解釋型編程語言,主要用於Web、和瀏覽器中網頁中的動態渲染 ​ 是一門前端工程師的編程語言,相比於python它的邏輯並不是那麼嚴謹 二、JS基礎 語法註釋 單行註釋 //註釋內容 多行註釋 ...
  • JS簡介 JavaScript(簡稱JS) 是一種具有函數優先的輕量級,解釋型或即時編譯型編程語言雖然它是作為開發Web頁面的腳本語言而出名,但是它也被用到了很多非瀏覽器環境中,JavaScript 基於原型編程、多範式的動態腳本語言,並且支持面向對象、命令式、聲明式、函數式編程範式 ==JavaS ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 本組件目前只能用在React Native 的iOS端 本組件來之實際中的開發需求:可以檢測並且標記人臉,實現基本的美顏,可進行拍照、換行鏡頭等基礎相機功能。官方組件封裝 教程 本文代碼:DEMO 運行demo $ git clone h ...
  • 歡迎來的我的小院,恭喜你今天又要漲知識了! 案例內容 利用JavaScript實現文字逐步展現的動畫效果。 演示 學習 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>小院里的霍大俠</title> </ ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...