《深入理解Java虛擬機》學(xué)習筆記(九)(Java內(nèi)存模型與線程)

Java內(nèi)存模型與線程

Java內(nèi)存模型

主內(nèi)存與工作內(nèi)存

  • Java內(nèi)存模型的主要目標是定義程序中各個變量的訪問規(guī)則面殖,即在虛擬機中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細節(jié)私杜。
    • 變量(Variables)與Java編程中所說的變量有所區(qū)別轨蛤,它包括了實例字段、 靜態(tài)字段和構(gòu)成數(shù)組對象的元素,但不包括局部變量與方法參數(shù),因為后者是線程私有的恤筛,不會被共享,自然就不會存在競爭問題芹橡。
  • Java內(nèi)存模型規(guī)定了所有的變量都存儲在主內(nèi)存(Main Memory)中毒坛。 每條線程還有自己的工作內(nèi)存。線程的工作內(nèi)存中保存了被該線程使用到的變量的主內(nèi)存副本拷貝林说,線程對變量的所有操作(讀取煎殷、 賦值等)都必須在工作內(nèi)存中進行,而不能直接讀寫主內(nèi)存中的變量腿箩。不同的線程之間也無法直接訪問對方工作內(nèi)存中的變量豪直,線程間變量值的傳遞均需要通過主內(nèi)存來完成。
    圖1 線程珠移、 主內(nèi)存弓乙、 工作內(nèi)存三者的交互關(guān)系

內(nèi)存間交互操作

Java內(nèi)存模型中定義了以下8種操作來完成主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,虛擬機實現(xiàn)時必須保證下面提及的每一種操作都是原子的钧惧、 不可再分

  • lock(鎖定):作用于主內(nèi)存的變量暇韧,它把一個變量標識為一條線程獨占的狀態(tài)。
  • unlock(解鎖):作用于主內(nèi)存的變量浓瞪,它把一個處于鎖定狀態(tài)的變量釋放出來懈玻,釋放后的變量才可以被其他線程鎖定。
  • read(讀惹洹):作用于主內(nèi)存的變量涂乌,它把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動作使用英岭。
  • load(載入):作用于工作內(nèi)存的變量湾盒,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
  • use(使用):作用于工作內(nèi)存的變量巴席,它把工作內(nèi)存中一個變量的值傳遞給執(zhí)行引擎历涝,每當虛擬機遇到一個需要使用到變量的值的字節(jié)碼指令時將會執(zhí)行這個操作诅需。
  • assign(賦值):作用于工作內(nèi)存的變量漾唉,它把一個從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作堰塌。
  • store(存儲):作用于工作內(nèi)存的變量赵刑,它把工作內(nèi)存中一個變量的值傳送到主內(nèi)存中,以便隨后的write操作使用场刑。
  • write(寫入):作用于主內(nèi)存的變量般此,它把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中蚪战。

如果要把一個變量從主內(nèi)存復(fù)制到工作內(nèi)存,那就要順序地執(zhí)行read和load操作铐懊,如果要把變量從工作內(nèi)存同步回主內(nèi)存邀桑,就要順序地執(zhí)行store和write操作

Java內(nèi)存模型還規(guī)定了在執(zhí)行上述8種基本操作時必須滿足如下規(guī)則:

  • 不允許read和load科乎、 store和write操作之一單獨出現(xiàn)壁畸,即不允許一個變量從主內(nèi)存讀取了,但工作內(nèi)存不接受茅茂,或者從工作內(nèi)存發(fā)起回寫了但主內(nèi)存不接受的情況出現(xiàn)捏萍。
  • 不允許一個線程丟棄它的最近的assign操作,即變量在工作內(nèi)存中改變了之后必須把該變化同步回主內(nèi)存空闲。
  • 不允許一個線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存中令杈。一個新的變量只能在主內(nèi)存中“誕生”,不允許在工作內(nèi)存中直接使用一個未被初始化(load或assign)的變量碴倾,換句話說逗噩,就是對一個變量實施use、 store操作之前跌榔,必須先執(zhí)行過了assign和load操作给赞。
  • 一個變量在同一個時刻只允許一條線程對其進行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次矫户,多次執(zhí)行l(wèi)ock后片迅,只有執(zhí)行相同次數(shù)的unlock操作,變量才會被解鎖皆辽。
  • 如果對一個變量執(zhí)行l(wèi)ock操作柑蛇,那將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個變量前驱闷,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值耻台。
  • 如果一個變量事先沒有被lock操作鎖定,那就不允許對它執(zhí)行unlock操作空另,也不允許去unlock一個被其他線程鎖定住的變量盆耽。
  • 對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store扼菠、 write操作)摄杂。

對于volatile型變量的特殊規(guī)則

當一個變量定義為volatile之后,它將具備兩種特性

  • 第一是保證此變量對所有線程的可見性
    • 這里的“可見性”是指當一條線程修改了這個變量的值循榆,新值對于其他線程來說是可以立即得知的析恢。 而普通變量不能做到這一點,普通變量的值在線程間傳遞均需要通過主內(nèi)存來完成
    • volatile變量只能保證可見性秧饮,在不符合以下兩條規(guī)則的運算場景中映挂,我們?nèi)匀灰?strong>通過加鎖(使用synchronized或java.util.concurrent中的原子類)來保證原子性
      • 運算結(jié)果并不依賴變量的當前值泽篮,或者能夠確保只有單一的線程修改變量的值。
      • 變量不需要與其他的狀態(tài)變量共同參與不變約束柑船。
  • 使用volatile變量的第二個語義是禁止指令重排序優(yōu)化
    • 指令重排序是指CPU采用了允許將多條指令不按程序規(guī)定的順序分開發(fā)送給各相應(yīng)電路單元處理帽撑。
    • 有volatile修飾的變量,賦值后(mov%eax鞍时,0x150(%esi))多執(zhí)行了一個“l(fā)ock addl $0x0油狂,(%esp)”操作,這個操作相當于一個內(nèi)存屏障(Memory Barrier或Memory Fence寸癌,指重排序時不能把后面的指令重排序到內(nèi)存屏障之前的位置)专筷,只有一個CPU訪問內(nèi)存時,并不需要內(nèi)存屏障蒸苇;但如果有兩個或更多CPU訪問同一塊內(nèi)存磷蛹,且其中有一個在觀測另一個,就需要內(nèi)存屏障來保證一致性
      • lock前綴的作用:使得本CPU的Cache寫入了內(nèi)存溪烤,該寫入動作也會引起別的CPU或者別的內(nèi)核無效化(Invalidate)其Cache
  • volatile變量讀操作的性能消耗與普通變量幾乎沒有什么差別味咳,但是寫操作則可能會慢一些,因為它需要在本地代碼中插入許多內(nèi)存屏障指令來保證處理器不發(fā)生亂序執(zhí)行檬嘀。
  • 在工作內(nèi)存中槽驶,每次使用V(被volatile修飾的變量)前都必須先從主內(nèi)存刷新最新的值,用于保證能看見其他線程對變量V所做的修改后的值

對于long和double型變量的特殊規(guī)則

對于64位的數(shù)據(jù)類型(long和double)鸳兽,在模型中特別定義了一條相對寬松的規(guī)定:允許虛擬機將沒有被volatile修飾的64位數(shù)據(jù)的讀寫操作劃分為兩次32位的操作來進行掂铐,但允許虛擬機選擇把這些操作實現(xiàn)為具有原子性的操作
在實際開發(fā)中,目前各種平臺下的商用虛擬機幾乎都選擇把64位數(shù)據(jù)的讀寫操作作為原子操作來對待揍异,因此我們在編寫代碼時一般不需要把用到的long和double變量專門聲明為volatile全陨。

原子性、 可見性與有序性

原子性(Atomicity):由Java內(nèi)存模型來直接保證的原子性變量操作包括read衷掷、 load辱姨、assign、 use戚嗅、 store和write雨涛,我們大致可以認為基本數(shù)據(jù)類型的訪問(double和long是例外)讀寫是具備原子性的

  • synchronized塊之間的操作也具備原子性。

可見性(Visibility):可見性是指當一個線程修改了共享變量的值懦胞,其他線程能夠立即得知這個修改替久。

  • volatile的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新医瘫。
  • 除了volatile之外侣肄,Java還有兩個關(guān)鍵字能實現(xiàn)可見性,即synchronized和final醇份。
    • 同步塊的可見性是由“對一個變量執(zhí)行unlock操作之前稼锅,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)
    • 被final修飾的字段在構(gòu)造器中一旦初始化完成僚纷,并且構(gòu)造器沒有把“this”的引用傳遞出去矩距,那在其他線程中就能看見final字段的值。

有序性(Ordering):如果在本線程內(nèi)觀察怖竭,所有的操作都是有序的锥债;如果在一個線程中觀察另一個線程,所有的操作都是無序的痊臭。

  • Java語言提供了volatile和synchronized兩個關(guān)鍵字來保證線程之間操作的有序性哮肚,volatile關(guān)鍵字本身就包含了禁止指令重排序的語義,而synchronized則是由“一個變量在同一個時刻只允許一條線程對其進行l(wèi)ock操作”這條規(guī)則獲得的广匙,這條規(guī)則決定了持有同一個鎖的兩個同步塊只能串行地進入允趟。

先行發(fā)生原則

先行發(fā)生是Java內(nèi)存模型中定義的兩項操作之間的偏序關(guān)系

  • 程序次序規(guī)則(Program Order Rule):在一個線程內(nèi),按照程序代碼順序鸦致,書寫在前面的操作先行發(fā)生于書寫在后面的操作潮剪。 準確地說,應(yīng)該是控制流順序而不是程序代碼順序分唾,因為要考慮分支抗碰、 循環(huán)等結(jié)構(gòu)。
  • 管程鎖定規(guī)則(Monitor Lock Rule):一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作绽乔。 這里必須強調(diào)的是同一個鎖弧蝇,而“后面”是指時間上的先后順序。
  • volatile變量規(guī)則(Volatile Variable Rule):對一個volatile變量的寫操作先行發(fā)生于后面對這個變量的讀操作折砸,這里的“后面”同樣是指時間上的先后順序捍壤。
  • 線程啟動規(guī)則(Thread Start Rule):Thread對象的start()方法先行發(fā)生于此線程的每一個動作
  • 線程終止規(guī)則(Thread Termination Rule):線程中的所有操作都先行發(fā)生于對此線程的終止檢測鞍爱,我們可以通過Thread.join()方法結(jié)束鹃觉、Thread.isAlive()的返回值等手段檢測到線程已經(jīng)終止執(zhí)行。
  • 線程中斷規(guī)則(Thread Interruption Rule):對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生睹逃,可以通過Thread.interrupted()方法檢測到是否有中斷發(fā)生盗扇。
  • 對象終結(jié)規(guī)則(Finalizer Rule):一個對象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalize()方法的開始
  • 傳遞性(Transitivity):如果操作A先行發(fā)生于操作B沉填,操作B先行發(fā)生于操作C疗隶,那就可以得出操作A先行發(fā)生于操作C的結(jié)論

Java與線程

線程的實現(xiàn)

實現(xiàn)線程主要有3種方式:使用內(nèi)核線程實現(xiàn)翼闹、 使用用戶線程實現(xiàn)和使用用戶線程加輕量級進程混合實現(xiàn)斑鼻。

  • 1.使用內(nèi)核線程實現(xiàn)
    內(nèi)核線程(Kernel-Level Thread,KLT)就是直接由操作系統(tǒng)內(nèi)核(Kernel,下稱內(nèi)核)支持的線程猎荠,這種線程由內(nèi)核來完成線程切換坚弱,內(nèi)核通過操縱調(diào)度器(Scheduler)對線程進行調(diào)度蜀备,并負責將線程的任務(wù)映射到各個處理器上。 每個內(nèi)核線程可以視為內(nèi)核的一個分身荒叶,這樣操作系統(tǒng)就有能力同時處理多件事情碾阁,支持多線程的內(nèi)核就叫做多線程內(nèi)核(MultiThreads Kernel)。
    • 程序一般不會直接去使用內(nèi)核線程些楣,而是去使用內(nèi)核線程的一種高級接口——輕量級進程(Light Weight Process,LWP)脂凶,輕量級進程就是我們通常意義上所講的線程,由于每個輕量級進程都由一個內(nèi)核線程支持愁茁,因此只有先支持內(nèi)核線程蚕钦,才能有輕量級進程。 這種輕量級進程與內(nèi)核線程之間1:1的關(guān)系稱為一對一的線程模型
      圖2 輕量級進程與內(nèi)核線程之間1:1的關(guān)系
    • 輕量級進程要消耗一定的內(nèi)核資源(如內(nèi)核線程的椂旌埽空間)嘶居,因此一個系統(tǒng)支持輕量級進程的數(shù)量是有限的。
  • 2.使用用戶線程實現(xiàn)
    一個線程只要不是內(nèi)核線程道宅,就可以認為是用戶線程(User Thread,UT)
    圖3 進程與用戶線程之間1:N的關(guān)系
  • 3.使用用戶線程加輕量級進程混合實現(xiàn)


    圖4 用戶線程與輕量級進程之間N:M的關(guān)系
  • 4.Java線程的實現(xiàn)
    線程模型替換為基于操作系統(tǒng)原生線程模型來實現(xiàn)

Java線程調(diào)度

線程調(diào)度是指系統(tǒng)為線程分配處理器使用權(quán)的過程食听,主要調(diào)度方式有兩種,分別是協(xié)同式線程調(diào)度(Cooperative Threads-Scheduling)和搶占式線程調(diào)度(Preemptive ThreadsScheduling)污茵。

  • 協(xié)同式線程調(diào)度:線程的執(zhí)行時間由線程本身來控制樱报,線程把自己的工作執(zhí)行完了之后,要主動通知系統(tǒng)切換到另外一個線程上泞当。
  • 搶占式線程調(diào)度:每個線程將由系統(tǒng)來分配執(zhí)行時間迹蛤,線程的切換不由線程本身來決定

狀態(tài)轉(zhuǎn)換

Java語言定義了5種線程狀態(tài),在任意一個時間點襟士,一個線程只能有且只有其中的一種狀態(tài)

  • 新建(New):創(chuàng)建后尚未啟動的線程處于這種狀態(tài)盗飒。
  • 運行(Runable):Runable包括了操作系統(tǒng)線程狀態(tài)中的Running和Ready,也就是處于此狀態(tài)的線程有可能正在執(zhí)行陋桂,也有可能正在等待著CPU為它分配執(zhí)行時間逆趣。
  • 無限期等待(Waiting):處于這種狀態(tài)的線程不會被分配CPU執(zhí)行時間,它們要等待被其他線程顯式地喚醒嗜历。 以下方法會讓線程陷入無限期的等待狀態(tài):
    • 沒有設(shè)置Timeout參數(shù)的Object.wait()方法宣渗。
    • 沒有設(shè)置Timeout參數(shù)的Thread.join()方法蕉朵。
    • LockSupport.park()方法捣郊。
  • 限期等待(Timed Waiting):處于這種狀態(tài)的線程也不會被分配CPU執(zhí)行時間,不過無須等待被其他線程顯式地喚醒舟奠,在一定時間之后它們會由系統(tǒng)自動喚醒暴匠。 以下方法會讓線程進入限期等待狀態(tài):
    • Thread.sleep()方法鞍恢。
    • 設(shè)置了Timeout參數(shù)的Object.wait()方法。
    • 設(shè)置了Timeout參數(shù)的Thread.join()方法。
    • LockSupport.parkNanos()方法帮掉。
    • LockSupport.parkUntil()方法弦悉。
  • 阻塞(Blocked): 在程序等待進入同步區(qū)域的時候,線程將進入這種狀態(tài)旭寿【ǎ“阻塞狀態(tài)”與“等待狀態(tài)”的區(qū)別是:“阻塞狀態(tài)”在等待著獲取到一個排他鎖崇败,這個事件將在另外一個線程放棄這個鎖的時候發(fā)生盅称;而“等待狀態(tài)”則是在等待一段時間,或者喚醒動作的發(fā)生后室。
  • 結(jié)束(Terminated):已終止線程的線程狀態(tài)缩膝,線程已經(jīng)結(jié)束執(zhí)行。
    圖5 線程狀態(tài)轉(zhuǎn)換關(guān)系
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末岸霹,一起剝皮案震驚了整個濱河市疾层,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌贡避,老刑警劉巖痛黎,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異刮吧,居然都是意外死亡湖饱,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門杀捻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來井厌,“玉大人,你說我怎么就攤上這事致讥〗銎停” “怎么了?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵垢袱,是天一觀的道長墓拜。 經(jīng)常有香客問我,道長请契,這世上最難降的妖魔是什么咳榜? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮姚糊,結(jié)果婚禮上贿衍,老公的妹妹穿的比我還像新娘。我一直安慰自己救恨,他們只是感情好贸辈,可當我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般擎淤。 火紅的嫁衣襯著肌膚如雪奢啥。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天嘴拢,我揣著相機與錄音桩盲,去河邊找鬼。 笑死席吴,一個胖子當著我的面吹牛赌结,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播孝冒,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼柬姚,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了庄涡?” 一聲冷哼從身側(cè)響起量承,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎穴店,沒想到半個月后撕捍,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡泣洞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年忧风,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片斜棚。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡阀蒂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出弟蚀,到底是詐尸還是另有隱情蚤霞,我是刑警寧澤,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布义钉,位于F島的核電站昧绣,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏捶闸。R本人自食惡果不足惜夜畴,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望删壮。 院中可真熱鬧贪绘,春花似錦、人聲如沸央碟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至菱涤,卻和暖如春苞也,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背粘秆。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工如迟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人攻走。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓殷勘,卻偏偏與公主長得像,于是被迫代替她去往敵國和親陋气。 傳聞我的和親對象是個殘疾皇子劳吠,可洞房花燭夜當晚...
    茶點故事閱讀 43,509評論 2 348

推薦閱讀更多精彩內(nèi)容