自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

資深架構(gòu)師解讀Java多線程與并發(fā)模型之共享對象

原創(chuàng)
開發(fā) 后端
這是一篇總結(jié)Java多線程開發(fā)的長文。文章是從Java創(chuàng)建之初就存在的synchronized關(guān)鍵字引入,對Java多線程和并發(fā)模型進(jìn)行了探討。希望通過此篇內(nèi)容的解讀能幫助Java開發(fā)者更好的理清Java并發(fā)編程的脈絡(luò)。

【51CTO.com原創(chuàng)稿件】互聯(lián)網(wǎng)上充斥著對Java多線程編程的介紹,每篇文章都從不同的角度介紹并總結(jié)了該領(lǐng)域的內(nèi)容。但大部分文章都沒有說明多線程的實(shí)現(xiàn)本質(zhì),沒能讓開發(fā)者真正“過癮”。上篇內(nèi)容從Java的線程安全鼻祖內(nèi)置鎖介紹開始,讓你了解內(nèi)置鎖的實(shí)現(xiàn)邏輯和原理以及引發(fā)的性能問題,本篇接著說明Java多線程編程中鎖的存在是為了保障共享變量的線程安全使用。下面讓我們進(jìn)入正題。

以下內(nèi)容如無特殊說明均指代Java環(huán)境。

第二部分:共享對象 

使用Java編寫線程安全的程序關(guān)鍵在于正確的使用共享對象,以及安全的對其進(jìn)行訪問管理。在第一章我們談到Java的內(nèi)置鎖可以保障線程安全,對于其他的應(yīng)用來說并發(fā)的安全性是在內(nèi)置鎖這個“黑盒子”內(nèi)保障了線程變量使用的邊界。談到線程的邊界問題,隨之而來的是Java內(nèi)存模型另外的一個重要的含義,可見性。Java對可見性提供的原生支持是volatile關(guān)鍵字。

volatile關(guān)鍵字

volatile關(guān)鍵字是Java語言提供的原生實(shí)現(xiàn),可以理解為“易變的”。首先看一個例子:

  1. public class Share { 
  2.     private static boolean ready; 
  3.     private static int number; 
  4.  
  5.     private static class Node extends Thread { 
  6.         public void run() { 
  7.             while (!ready) 
  8.                 Thread.yield(); 
  9.             System.out.println(number); 
  10.         } 
  11.     } 
  12.  
  13.     public static void main(String[] args) { 
  14.         new Node().start(); 
  15.         number = 10; 
  16.         ready = true
  17.     } 
  18.  

代碼2.1:變量的可見性問題 

在代碼2.1中,可以看到按照正常的邏輯應(yīng)該打印10之后線程停止,但是實(shí)際的情況可能是打印出0或者程序永遠(yuǎn)不會被終止掉。其原因是沒有使用恰當(dāng)?shù)耐綑C(jī)制以保障線程的寫入操作對所有線程都是可見的。

我們一般將volatile理解為synchronized的輕量級實(shí)現(xiàn),在多核處理器中可以保障共享變量的“可見性”,但是不能保障原子性。關(guān)于原子性問題在該章節(jié)的程序變量規(guī)則會加以說明,下面我們先看下Java的內(nèi)存模型實(shí)現(xiàn)以了解JVM和計(jì)算機(jī)硬件是如何協(xié)調(diào)共享變量的以及volatile變量的可見性。 

Java內(nèi)存模型 

我們都知道現(xiàn)代計(jì)算機(jī)都是馮諾依曼結(jié)構(gòu)的,所有的代碼都是順序執(zhí)行的。如果計(jì)算機(jī)需要在CPU中運(yùn)算某個指令,勢必就會涉及對數(shù)據(jù)的讀取和寫入操作。由于程序數(shù)據(jù)的大部分內(nèi)容都是存儲在主內(nèi)存(RAM)中的,在這當(dāng)中就存在著一個讀取速度的問題,CPU很快而主內(nèi)存相對來說(相對CPU)就會慢上很多,為了解決這個速度階梯問題,各個CPU廠商都在CPU里面引入了高速緩存來優(yōu)化主內(nèi)存和CPU的數(shù)據(jù)交互。

此時當(dāng)CPU需要從主內(nèi)存獲取數(shù)據(jù)時,會拷貝一份到高速緩存中,CPU計(jì)算時就可以直接在高速緩存中進(jìn)行數(shù)據(jù)的讀取和寫入,提高吞吐量。當(dāng)數(shù)據(jù)運(yùn)行完成后,再將高速緩存的內(nèi)容刷新到主內(nèi)存中,此時其他CPU看到的才是執(zhí)行之后的結(jié)果,但在這之間存在著時間差。

看這個例子:

  1. int counter = 0; 

  2. counter = counter + 1;  

代碼2.2:自增不一致問題

代碼2.2在運(yùn)行時,CPU會從主內(nèi)存中讀取counter的值,復(fù)制一份到當(dāng)前CPU核心的高速緩存中,在CPU執(zhí)行完成加1的指令之后,將結(jié)果1寫入高速緩存中,最后將高速緩存刷新到主內(nèi)存中。這個例子代碼在單線程的程序中將正確的運(yùn)行下去。

但我們試想這樣一種情況,現(xiàn)在有兩個線程共同運(yùn)行該段代碼,初始化時兩個線程分別從主內(nèi)存中讀取了counter的值0到各自的高速緩存中,線程1在CPU1中運(yùn)算完成后寫入高速緩存Cache1,線程2在CPU2中運(yùn)算完成后寫入高速緩存Cache2,此時counter的值在兩個CPU的高速緩存中的值都是1。

此時CPU1將值刷新到主內(nèi)存中,counter的值為1,之后CPU2將counter的值也刷新到主內(nèi)存,counter的值覆蓋為1,最終的結(jié)果計(jì)算counter為1(正確的兩次計(jì)算結(jié)果相加應(yīng)為2)。這就是緩存不一致性問題。這會在多線程訪問共享變量時出現(xiàn)。

解決緩存不一致問題的方案:

  1. 通過總線鎖LOCK#方式。

  2. 通過緩存一致性協(xié)議。 

 

圖2.1 :緩存不一致問題 

圖2.1中提到的兩種內(nèi)存一致性協(xié)議都是從計(jì)算機(jī)硬件層面上提供的保障。CPU一般是通過在總線上增加LOCK#鎖的方式,鎖住對內(nèi)存的訪問來達(dá)到目的,也就是阻塞其他CPU對內(nèi)存的訪問,從而使只有一個CPU能訪問該主內(nèi)存。因此需要用總線進(jìn)行內(nèi)存鎖定,可以分析得到此種做法對CPU的吞吐率造成的損害很嚴(yán)重,效率低下。 

隨著技術(shù)升級帶來了緩存一致性協(xié)議,市場占有率較大的Intel的CPU使用的是MESI協(xié)議,該協(xié)議可以保障各個高速緩存使用的共享變量的副本是一致的。其實(shí)現(xiàn)的核心思想是:當(dāng)在多核心CPU中訪問的變量是共享變量時,某個線程在CPU中修改共享變量數(shù)據(jù)時,會通知其他也存儲了該變量副本的CPU將緩存置為無效狀態(tài),因此其他CPU讀取該高速緩存中的變量時,發(fā)現(xiàn)該共享變量副本為無效狀態(tài),會從主內(nèi)存中重新加載。但當(dāng)緩存一致性協(xié)議無法發(fā)揮作用時,CPU還是會降級使用總線鎖的方式進(jìn)行鎖定處理。 

一個小插曲:為什么volatile無法保障的原子性 

我們看下圖2.2,CPU在主內(nèi)存中讀取一個變量之后,拷貝副本到高速緩存,CPU在執(zhí)行期間雖然識別了變量的“易變性”,但是只能保障最后一步store操作的原子性,在load,use期間并未實(shí)現(xiàn)其原子性操作。

圖2.2:數(shù)據(jù)加載和內(nèi)存屏障 

JVM為了使我們的代碼得到最優(yōu)的執(zhí)行體驗(yàn),在進(jìn)行自我優(yōu)化時,并不保障代碼的先后執(zhí)行順序(滿足Happen-Before規(guī)則的除外),這就是“指令重排”,而上面提到的store操作保障了原子性,JVM是如何實(shí)現(xiàn)的呢?其原因是這里存在一個“內(nèi)存屏障”的指令(以后我們會談到整個內(nèi)容),這個是CPU支持的一個指令,該指令只能保障store時的原子性,但是不能保障整個操作的原子性。

從整個小插曲中,我們看到了volatile雖然有可見性的語義,但是并不能真正的保證線程安全。如果要保證并發(fā)線程的安全訪問,需要符合并發(fā)程序變量的訪問規(guī)則。  

并發(fā)程序變量的訪問規(guī)則 

       1. 原子性

程序的原子性和數(shù)據(jù)庫事務(wù)的原子性有著同樣的意義,可以保障一次操作要么全部執(zhí)行成功,要不全部都不執(zhí)行。

       2. 可見性

           可見性是微妙的,因?yàn)樽罱K的結(jié)果總是和我們的直覺大相徑庭,當(dāng)多個線程共同修改一個共享變量的值時,由于存在高速緩存中的變量副本操作,不能及時將數(shù)據(jù)刷新到主內(nèi)存,導(dǎo)致當(dāng)前線程在CP中的操作結(jié)果對其他CPU是不可見狀態(tài)。

       3. 有序性

有序性通俗的理解就是程序在JVM中是按照順序執(zhí)行的,但是前面已經(jīng)提到了JVM為了優(yōu)化代碼的執(zhí)行速度,會進(jìn)行“指令重排”。在單線程中“指令重排”并不會帶來安全問題,但在并發(fā)程序中,由于程序的順序不能保障,運(yùn)行過程中可能會出現(xiàn)不安全的線程訪問問題。

綜上,要想在并發(fā)編程環(huán)境中安全的運(yùn)行程序,就必須滿足原子性、可見性和有序性。只要以上任何一點(diǎn)沒有保障,那程序運(yùn)行就可能出現(xiàn)不可預(yù)知的錯誤。最后我們介紹一下Java并發(fā)的“殺手锏”,Happens-Before法則,符合該法則的情況下可以保障并發(fā)環(huán)境下變量的訪問規(guī)則。 

Happens-Before法則: 

  1. 程序次序法則:線程中的每個動作A都Happens-Before于該線程中的每一個動作B,在程序中,所有的動作B都出現(xiàn)在動作A之后。

  2. Lock法則:對于一個Lock的解鎖操作總是Happens-Before于每一個后續(xù)對該Lock的加鎖操作。

  3. volatile變量法則:對于volatile變量的寫入操作Happens-Before于后續(xù)對同一個變量的讀操作。

  4. 線程啟動法則:在一個線程里,對Thread.start()函數(shù)的調(diào)用會Happens-Before于每一個啟動線程中的動作。

  5. 線程終結(jié)法則:線程中的任何動作都Happens-Before于其他線程檢測到這個線程已經(jīng)終結(jié)或者從Thread.join()函數(shù)調(diào)用中成功返回或者Thread.isAlive()函數(shù)返回false。

  6. 中斷法則:一個線程調(diào)用另一個線程的interrupt總是Happens-Before于被中斷的線程發(fā)現(xiàn)中斷。

  7. 終結(jié)法則:一個對象的構(gòu)造函數(shù)的結(jié)束總是Happens-Before于這個對象的finalizer(Java沒有直接的類似C的析構(gòu)函數(shù))的開始。

  8. 傳遞性法則:如果A事件Happens-Before于B事件,并且B事件Happens-Before于C事件,那么A事件Happens-Before于C事件。

當(dāng)一個變量在多線程競爭中被讀取和存儲,如果并未按照Happens-Before的法則,那么他就會存在數(shù)據(jù)競爭關(guān)系。 

總結(jié) 

關(guān)于Java的共享變量的內(nèi)容就介紹到這里,現(xiàn)在你已經(jīng)明白Java的volatile關(guān)鍵字的含義了,了解了為什么volatile不能保障原子性的原因了,了解了Happens-Before規(guī)則能讓我們的Java程序運(yùn)行的更加安全。

通過這兩節(jié)內(nèi)容希望可以幫助你更深入的了解Java的并發(fā)概念中的內(nèi)置鎖和共享變量。Java的并發(fā)內(nèi)容還有很多,例如在某些場景下比synchronized效率要更高的Lock,阻塞隊(duì)列,同步器等。 

參考文獻(xiàn): 

《Java并發(fā)編程實(shí)戰(zhàn)》

更多精彩請?jiān)L問:

  • XSS常見攻擊與防御

https://zhuanlan.zhihu.com/p/30475175

  • 利用500W條微博語料對評論進(jìn)行情感分析:

https://zhuanlan.zhihu.com/p/30061051

  • 還在手調(diào)網(wǎng)絡(luò)權(quán)限?資深I(lǐng)T工程師都這樣玩企業(yè)組網(wǎng)

https://zhuanlan.zhihu.com/p/29787843

  • 微服務(wù)在互聯(lián)網(wǎng)公司演進(jìn)過程

https://zhuanlan.zhihu.com/p/29758427
 

作者簡介 

魏靚:現(xiàn)就職于五阿哥(www.wuage.com)任職專職架構(gòu)師工作,負(fù)責(zé)平臺的基礎(chǔ)設(shè)施搭建工作。 

【51CTO原創(chuàng)稿件,合作站點(diǎn)轉(zhuǎn)載請注明原文作者和出處為51CTO.com】

 

責(zé)任編輯:龐桂玉 來源: 51CTO.com
相關(guān)推薦

2017-11-17 15:57:09

Java多線程并發(fā)模型

2018-07-03 15:46:24

Java架構(gòu)師源碼

2011-06-13 10:41:17

JAVA

2017-09-16 18:29:00

代碼數(shù)據(jù)庫線程

2012-11-01 15:08:10

IBM資深架構(gòu)師

2018-02-05 09:30:23

高性能高并發(fā)服務(wù)

2018-09-13 15:00:51

JavaHashMap架構(gòu)師

2021-07-19 07:55:24

多線程模型Redis

2010-05-04 08:44:42

Java并發(fā)模型

2013-10-17 15:45:24

紅帽

2013-10-17 15:54:46

紅帽

2015-04-10 17:35:26

WOT2015谷歌資深架構(gòu)師李聰

2021-06-07 09:35:11

架構(gòu)運(yùn)維技術(shù)

2022-05-26 08:31:41

線程Java線程與進(jìn)程

2019-10-21 09:32:48

緩存架構(gòu)分層

2020-01-16 15:35:00

高并發(fā)架構(gòu)服務(wù)器

2013-11-14 10:06:11

紅帽redhat

2009-02-19 16:19:48

SaaS開發(fā)SaaS安全SaaS

2012-12-17 17:38:37

System CentWindows SerHyper-V

2017-12-18 16:33:55

多線程對象模型
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號