Java 存儲模型和共享對象詳解
Java 存儲模型和共享對象詳解
很多程序員對一個共享變量初始化要注意可見性和安全發(fā)布(安全地構(gòu)建一個對象,并其他線程能正確訪問)等問題不是很理解,認(rèn)為Java是一個屏蔽內(nèi)存細(xì)節(jié)的平臺,連對象回收都不需要關(guān)心,因此談到可見性和安全發(fā)布大多不知所云。其實(shí)關(guān)鍵在于對Java存儲模型,可見性和安全發(fā)布的問題是起源于Java的存儲結(jié)構(gòu)。
Java存儲模型原理
有很多書和文章都講解過Java存儲模型,其中一個圖很清晰地說明了其存儲結(jié)構(gòu):
由上圖可知, jvm系統(tǒng)中存在一個主內(nèi)存(Main Memory或Java Heap Memory),Java中所有變量都儲存在主存中,對于所有線程都是共享的。 每條線程都有自己的工作內(nèi)存(Working Memory),工作內(nèi)存中保存的是主存中某些變量的拷貝,線程對所有變量的操作都是在工作內(nèi)存中進(jìn)行,線程之間無法相互直接訪問,變量傳遞均需要通過主存完成。
這個存儲模型很像我們常用的緩存與數(shù)據(jù)庫的關(guān)系,因此由此可以推斷JVM如此設(shè)計(jì)應(yīng)該是為了提升性能,提高多線程的并發(fā)能力,并減少線程之間的影響。
Java存儲模型潛在的問題
一談到緩存, 我們立馬想到會有緩存不一致性問題,就是說當(dāng)有緩存與數(shù)據(jù)庫不一致的時候,就需要有相應(yīng)的機(jī)制去同步數(shù)據(jù)。同理,Java存儲模型也有這個問題,當(dāng)一個線程在自己工作內(nèi)存里初始化一個變量,當(dāng)還沒來得及同步到主存里時,如果有其他線程來訪問它,就會出現(xiàn)不可預(yù)知的問題。另外,JVM在底層設(shè)計(jì)上,對與那些沒有同步到主存里的變量,可能會以不一樣的操作順序來執(zhí)行指令,舉個實(shí)際的例子:
public class PossibleReordering { static int x = 0, y = 0; static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException { Thread one = new Thread(new Runnable() { public void run() { a = 1; x = b; } }); Thread other = new Thread(new Runnable() { public void run() { b = 1; y = a; } }); one.start(); other.start(); one.join(); other.join(); System.out.println("( "+ x + "," + y + ")"); } }
由于,變量x,y,a,b沒有安全發(fā)布,導(dǎo)致會不以規(guī)定的操作順序來執(zhí)行這次四次賦值操作,有可能出現(xiàn)以下順序:
出現(xiàn)這個問題也可以理解,因?yàn)榧热贿@些對象不可見,也就是說本應(yīng)該隔離在各個線程的工作區(qū)內(nèi),那么對于有些無關(guān)順序的指令,打亂順序執(zhí)行在JVM看來也是可行的。
因此,總結(jié)起來,會有以下兩種潛在問題:
- 緩存不一致性
- 重排序執(zhí)行
解決Java存儲模型潛在的問題
為了能讓開發(fā)人員安全正確地在Java存儲模型上編程,JVM提供了一個happens-before原則,有人整理得非常好,我摘抄如下:
- 在程序順序中, 線程中的每一個操作, 發(fā)生在當(dāng)前操作后面將要出現(xiàn)的每一個操作之前.
- 對象監(jiān)視器的解鎖發(fā)生在等待獲取對象鎖的線程之前.
- 對volitile關(guān)鍵字修飾的變量寫入操作, 發(fā)生在對該變量的讀取之前.
- 對一個線程的 Thread.start() 調(diào)用 發(fā)生在啟動的線程中的所有操作之前.
- 線程中的所有操作 發(fā)生在從這個線程的 Thread.join()成功返回的所有其他線程之前.
有了原則還不夠,Java提供了以下工具和方法來保證變量的可見性和安全發(fā)布:
- 使用 synchronized來同步變量初始化。此方式會立馬把工作內(nèi)存中的變量同步到主內(nèi)存中
- 使用 volatile關(guān)鍵字來標(biāo)示變量。此方式會直接把變量存在主存中而不是工作內(nèi)存中
- final變量。常量內(nèi)也是存于主存中
另外,一定要明確只有共享變量才會有以上那些問題,如果變量只是這個線程自己使用,就不用擔(dān)心那么多問題了
搞清楚Java存儲模型后,再來看共享對象可見性和安全發(fā)布的問題就較為容易了
共享對象的可見性
當(dāng)對象在從工作內(nèi)存同步到主內(nèi)存之前,那么它就是不可見的。若有其他線程在存取不可見對象就會引發(fā)可見性問題,看下面一個例子:
public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while (!ready) Thread.yield(); System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; } }
按照正常邏輯,應(yīng)該會輸出42,但其實(shí)際結(jié)果會非常奇怪,可能會永遠(yuǎn)沒有輸出(因?yàn)閞eady為false),可能會輸出0(因?yàn)橹嘏判騿栴}導(dǎo)致ready=true先執(zhí)行)。再舉一個更為常見的例子,大家都喜歡用只有set和get方法的pojo來設(shè)計(jì)領(lǐng)域模型,如下所示:
@NotThreadSafe public class MutableInteger { private int value; public int get() { return value; } public void set(int value) { this.value = value; } }
但是,當(dāng)有多個線程同時來存取某一個對象時,可能就會有類似的可見性問題。
為了保證變量的可見性,一般可以用鎖、 synchronized關(guān)鍵字、 volatile關(guān)鍵字或直接設(shè)置為final
共享變量發(fā)布
共享變量發(fā)布和我們常說的發(fā)布程序類似,就是說讓本屬于內(nèi)部的一個變量變?yōu)橐粋€可以被外部訪問的變量。發(fā)布方式分為以下幾種:
- 將對象引用存儲到公共靜態(tài)域
- 初始化一個可以被外部訪問的對象
- 將對象引用存儲到一個集合里
安全發(fā)布和保證可見性的方法類似,就是要同步發(fā)布動作,并使發(fā)布后的對象可見。
線程安全
其實(shí)當(dāng)我們把這些變量封閉在本線程內(nèi)訪問,就可以從根本上避免以上問題,現(xiàn)實(shí)中存在很多例子通過線程封閉來安全使用本不是線程安全的對象,比如:
- swing的可視化組件和數(shù)據(jù)模型對象并不是線程安全的,它通過將它們限制到swing的事件分發(fā)線程中,實(shí)現(xiàn)線程安全
- JDBC Connection對象沒有要求為線程安全,但JDBC的存取模式?jīng)Q定了一個Connection只會同時被一個線程使用
- ThreadLocal把變量限制在本線程中共享
感謝閱讀,希望能幫助到大家,謝謝大家對本站的支持!
相關(guān)文章
Spring IOC和DI實(shí)現(xiàn)原理及實(shí)例解析
這篇文章主要介紹了Spring IOC和DI實(shí)現(xiàn)原理及實(shí)例解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-06-06Spring?Security權(quán)限注解啟動及邏輯處理使用示例
這篇文章主要為大家介紹了Spring?Security權(quán)限注解啟動及邏輯處理使用示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07JAVA中數(shù)組插入與刪除指定元素的實(shí)例代碼
下面小編就為大家分享一篇JAVA中數(shù)組插入與刪除指定元素的實(shí)例代碼,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-02-02mybaties plus實(shí)體類設(shè)置typeHandler不生效的解決
這篇文章主要介紹了mybaties plus實(shí)體類設(shè)置typeHandler不生效的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08