日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

問題

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=2和ready=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
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定