十年網(wǎng)站開發(fā)經(jīng)驗 + 多家企業(yè)客戶 + 靠譜的建站團隊
量身定制 + 運營維護+專業(yè)推廣+無憂售后,網(wǎng)站問題一站解決
這篇文章主要介紹“Happens-before的作用是什么”,在日常操作中,相信很多人在Happens-before的作用是什么問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Happens-before的作用是什么”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!
創(chuàng)新互聯(lián)是一家集網(wǎng)站建設(shè),太倉企業(yè)網(wǎng)站建設(shè),太倉品牌網(wǎng)站建設(shè),網(wǎng)站定制,太倉網(wǎng)站建設(shè)報價,網(wǎng)絡(luò)營銷,網(wǎng)絡(luò)優(yōu)化,太倉網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強企業(yè)競爭力。可充分滿足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時我們時刻保持專業(yè)、時尚、前沿,時刻以成就客戶成長自我,堅持不斷學(xué)習(xí)、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實用型網(wǎng)站。
上一篇文章并發(fā) Bug 之源有三,請睜大眼睛看清它們 談到了可見性/原子性/有序性
三個問題,這些問題通常違背我們的直覺和思考模式,也就導(dǎo)致了很多并發(fā) Bug
為了解決 CPU,內(nèi)存,IO 的短板,增加了緩存,但這導(dǎo)致了可見性問題
編譯器/處理器擅自
優(yōu)化 ( Java代碼在編譯后會變成 Java 字節(jié)碼, 字節(jié)碼被類加載器加載到 JVM 里, JVM 執(zhí)行字節(jié)碼, 最終需要轉(zhuǎn)化為匯編指令在 CPU 上執(zhí)行) ,導(dǎo)致有序性問題
初衷是好的,但引發(fā)了新問題,最有效的辦法就禁止緩存和編譯優(yōu)化,問題雖然能解決,但「又回到最初的起點,呆呆地站在鏡子前」是很尷尬的,我們程序的性能就堪憂了.
作為我們程序猿不想寫出 bug 影響 KPI,所以希望內(nèi)存模型易于理解、易于編程。這就需要基于一個強內(nèi)存模型來編寫代碼
作為編譯器和處理器不想讓外人說它處理速度很慢,所以希望內(nèi)存模型對他們束縛越少越好,可以由他們擅自優(yōu)化,這就需要基于一個弱內(nèi)存模型
俗話說:「沒有什么事是開會解決不了的,如果有,那就再開一次」????
JSR-133 的專家們就有了新想法,既然不能完全禁止緩存和編譯優(yōu)化,那就按需禁用緩存和編譯優(yōu)化,按需就是要加一些約束,約束中就包括了上一篇文章簡單提到過的 volatile,synchronized,final三個關(guān)鍵字,同時還有你可能聽過的 Happens-Before原則(包含可見性和有序性的約束),Happens-before 規(guī)則也是本章的主要內(nèi)容
為了滿足二者的強烈需求,照顧到雙方的情緒,于是乎: JMM 就對程序猿說了一個善意的謊言: 「會嚴(yán)格遵守 Happpen-Befores 規(guī)則,不會重排序」讓程序猿放心,私下卻有自己的策略:
對于會改變程序執(zhí)行結(jié)果的重排序,JMM要求編譯器和處理器必須禁止這種重排序。
對于不會改變程序執(zhí)行結(jié)果的重排序, JMM對編譯器和處理器不做要求 (JMM允許這種重排序)。
我們來用個圖說明一下:
這就是那個善意的謊言,雖是謊言,但還是照顧到了程序猿的利益,所以我們只需要了解 happens-before 規(guī)則就能得到保證 (圖畫了好久,不知道是否說明了謊言的所在????,歡迎留言)
Happens-before 規(guī)則主要用來約束兩個操作,兩個操作之間具有 happens-before 關(guān)系, 并不意味著前一個操作必須要在后一個操作之前執(zhí)行,happens-before 僅僅要求前一個操作(執(zhí)行的結(jié)果)對后一個操作可見, (the first is visibleto and ordered before the second)
說了這么多,先來看一小段代碼帶你逐步走進 Happen-Befores 原則,看看是怎樣用該原則解決 可見性和 有序性的問題:
class ReorderExample { int x = 0; boolean flag = false; public void writer() { x = 42; //1 flag = true; //2 } public void reader() { if (flag) { //3 System.out.println(x); //4 } } }
假設(shè) A 線程執(zhí)行 writer 方法,B 線程執(zhí)行 reader 方法,打印出來的 x 可能會是 0,上一篇文章說明過: 因為代碼 1 和 2 沒有數(shù)據(jù)依賴關(guān)系,所以可能被重排序
flag = true; //2 x = 42; //1
所以,線程 A 將 flag = true
寫入但沒有為 x 重新賦值時,線程 B 可能就已經(jīng)打印了 x 是 0
那么為 flag 加上 volatile 關(guān)鍵字試一下:
volatile boolean flag = false;
即便加上了 volatile 關(guān)鍵字,這個問題在 java1.5 之前還是沒有解決,但 java1.5 和其之后的版本對 volatile 語義做了增強,問題得以解決,這就離不開 Happens-before 規(guī)則的約束了,總共有 6 個規(guī)則,且看
一個線程中的每個操作, happens-before 于該線程中的任意后續(xù)操作 第一感覺這個原則是一個在理想狀態(tài)下的"廢話",并且和上面提到的會出現(xiàn)重排序的情況是矛盾的,注意這里是一個線程中的操作,其實隱含了「as-if-serial」語義: 說白了就是只要執(zhí)行結(jié)果不被改變,無論怎么"排序",都是對的
這個規(guī)則是一個基礎(chǔ)規(guī)則,happens-before 是多線程的規(guī)則,所以要和其他規(guī)則約束在一起才能體現(xiàn)出它的順序性,別著急,繼續(xù)向下看
對一個 volatile 域的寫, happens-before 于任意后續(xù)對這個 volatile 域的讀
我將上面的程序添加兩行代碼作說明:
public class ReorderExample { private int x = 0; private int y = 1; private volatile boolean flag = false; public void writer(){ x = 42; //1 y = 50; //2 flag = true; //3 } public void reader(){ if (flag){ //4 System.out.println("x:" + x); //5 System.out.println("y:" + y); //6 } } }
這里涉及到了 volatile 的內(nèi)存增強語義,先來看個表格:
能否重排序 | 第二個操作 | 第二個操作 | 第二個操作 |
---|---|---|---|
第一個操作 | 普通讀/寫 | volatile 讀 | volatile 寫 |
普通讀/寫 | - | - | NO |
volatile 讀 | NO | NO | NO |
volatile 寫 | - | NO | NO |
從這個表格 最后一列可以看出:
如果第二個操作為 volatile 寫,不管第一個操作是什么,都不能重排序,這就確保了 volatile 寫之前的操作不會被重排序到 volatile 寫之后拿上面的代碼來說,代碼 1 和 2 不會被重排序到代碼 3 的后面,但代碼 1 和 2 可能被重排序 (沒有依賴也不會影響到執(zhí)行結(jié)果),說到這里和 程序順序性規(guī)則是不是就已經(jīng)關(guān)聯(lián)起來了呢?
從這個表格的 倒數(shù)第二行可以看出:
如果第一個操作為 volatile 讀,不管第二個操作是什么,都不能重排序,這確保了 volatile 讀之后的操作不會被重排序到 volatile 讀之前拿上面的代碼來說,代碼 4 是讀取 volatile 變量,代碼 5 和 6 不會被重排序到代碼 4 之前
volatile 內(nèi)存語義的實現(xiàn)是應(yīng)用到了 「內(nèi)存屏障」,因為這完全夠單獨寫一章的內(nèi)容,這里為了不掩蓋主角 Happens-before 的光環(huán),保持理解 Happens-before 的連續(xù)性,先不做過多說明
到這里,看這個規(guī)則,貌似也沒解決啥問題,因為它還要聯(lián)合第三個規(guī)則才起作用
如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C 直接上圖說明一下上面的例子
從上圖可以看出
x =42
和 y = 50
Happens-before flag = true
, 這是規(guī)則 1
寫變量(代碼 3) flag=true
Happens-before 讀變量(代碼 4) if(flag)
,這是規(guī)則 2
根據(jù)規(guī)則 3傳遞性規(guī)則,x =42
Happens-before 讀變量 if(flag)
謎案要揭曉了: 如果線程 B 讀到了 flag 是 true,那么
x =42
和y = 50
對線程 B 就一定可見了,這就是 Java1.5 的增強 (之前版本是可以普通變量寫和 volatile 變量寫的重排序的)
通常上面三個規(guī)則是一種聯(lián)合約束,到這里你懂了嗎?規(guī)則還沒完,繼續(xù)看
對一個鎖的解鎖 happens-before 于隨后對這個鎖的加鎖
這個規(guī)則我覺得你應(yīng)該最熟悉了,就是解釋 synchronized 關(guān)鍵字的,來看
public class SynchronizedExample { private int x = 0; public void synBlock(){ // 1.加鎖 synchronized (SynchronizedExample.class){ x = 1; // 對x賦值 } // 3.解鎖 } // 1.加鎖 public synchronized void synMethod(){ x = 2; // 對x賦值 } // 3. 解鎖 }
先獲取鎖的線程,對 x 賦值之后釋放鎖,另外一個再獲取鎖,一定能看到對 x 賦值的改動,就是這么簡單,請小伙伴用下面命令查看上面程序,看同步塊和同步方法被轉(zhuǎn)換成匯編指令有何不同?
javap -c -v SynchronizedExample
這和 synchronized 的語義相關(guān),小伙伴可以先自行了解一下,鎖的內(nèi)容時會做詳細(xì)說明
如果線程 A 執(zhí)行操作 ThreadB.start() (啟動線程B), 那么 A 線程的 ThreadB.start() 操作 happens-before 于線程 B 中的任意操作,也就是說,主線程 A 啟動子線程 B 后,子線程 B 能看到主線程在啟動子線程 B 前的操作,看個程序就秒懂了
public class StartExample { private int x = 0; private int y = 1; private boolean flag = false; public static void main(String[] args) throws InterruptedException { StartExample startExample = new StartExample(); Thread thread1 = new Thread(startExample::writer, "線程1"); startExample.x = 10; startExample.y = 20; startExample.flag = true; thread1.start(); System.out.println("主線程結(jié)束"); } public void writer(){ System.out.println("x:" + x ); System.out.println("y:" + y ); System.out.println("flag:" + flag ); } }
運行結(jié)果:
主線程結(jié)束 x:10 y:20 flag:true Process finished with exit code 0
線程 1 看到了主線程調(diào)用 thread1.start() 之前的所有賦值結(jié)果,這里沒有打印「主線程結(jié)束」,你知道為什么嗎?這個守護線程知識有關(guān)系
如果線程 A 執(zhí)行操作 ThreadB.join() 并成功返回, 那么線程 B 中的任意操作 happens-before 于線程 A 從 ThreadB.join() 操作成功返回,和 start 規(guī)則剛好相反,主線程 A 等待子線程 B 完成,當(dāng)子線程 B 完成后,主線程能夠看到子線程 B 的賦值操作,將程序做個小改動,你也會秒懂的
public class JoinExample { private int x = 0; private int y = 1; private boolean flag = false; public static void main(String[] args) throws InterruptedException { JoinExample joinExample = new JoinExample(); Thread thread1 = new Thread(joinExample::writer, "線程1"); thread1.start(); thread1.join(); System.out.println("x:" + joinExample.x ); System.out.println("y:" + joinExample.y ); System.out.println("flag:" + joinExample.flag ); System.out.println("主線程結(jié)束"); } public void writer(){ this.x = 100; this.y = 200; this.flag = true; } }
運行結(jié)果:
x:100 y:200 flag:true 主線程結(jié)束 Process finished with exit code 0
「主線程結(jié)束」這幾個字打印出來嘍,依舊和線程何時退出有關(guān)系
Happens-before 重點是解決前一個操作結(jié)果對后一個操作可見,相信到這里,你已經(jīng)對 Happens-before 規(guī)則有所了解,這些規(guī)則解決了多線程編程的可見性與有序性問題,但還沒有完全解決原子性問題(除了 synchronized)
start 和 join 規(guī)則也是解決主線程與子線程通信的方式之一
從內(nèi)存語義的角度來說, volatile 的寫-讀
與鎖的釋放-獲取
有相同的內(nèi)存效果;volatile 寫和鎖的釋放有相同的內(nèi)存語義; volatile 讀與鎖的獲取有相同的內(nèi)存語義,??????(敲黑板了) volatile 解決的是可見性問題,synchronized 解決的是原子性問題,這絕對不是一回事,后續(xù)文章也會說明
同步塊和同步方法在編譯成 CPU 指令后有什么不同?
線程有 Daemon(守護線程)和非 Daemon 線程,你知道線程的退出策略嗎?
關(guān)于 Happens-before 你還有哪些疑惑呢?
到此,關(guān)于“Happens-before的作用是什么”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>
網(wǎng)頁名稱:Happens-before的作用是什么
標(biāo)題網(wǎng)址:http://m.jiaotiyi.com/article/ipdpph.html