JMM 即 java memery model ,java 內(nèi)存模型,再談java內(nèi)存模型之前,先認(rèn)識下Java內(nèi)存接口
Java內(nèi)存結(jié)構(gòu)主要分為推(heap),棧(stack),方法區(qū),本地方法區(qū),寄存器/計(jì)數(shù)器,

java 運(yùn)行內(nèi)存結(jié)構(gòu)
寄存器/程序計(jì)數(shù)器:確切的講是一個(gè)數(shù)據(jù)結(jié)構(gòu),用來保存正在執(zhí)行的程序的內(nèi)存地址,每個(gè)線程都有一個(gè)獨(dú)立的程序計(jì)數(shù)器,互不影響,相當(dāng)于threadlocal ,是線程安全的
java棧:存放一些基本數(shù)據(jù)類型初始數(shù)據(jù)和對象的引用,Java??偸桥c線程關(guān)聯(lián)在一起的,每當(dāng)創(chuàng)建一個(gè)線程,JVM就會為該線程創(chuàng)建對應(yīng)的Java棧
由于Java棧是與線程對應(yīng)起來的,Java棧數(shù)據(jù)不是線程共有的,所以不需要關(guān)心其數(shù)據(jù)一致性,也不會存在同步鎖的問題。
在Hot Spot虛擬機(jī)中,可以使用-Xss參數(shù)來設(shè)置棧的大小。
堆 Heap: 堆是JVM所管理的內(nèi)存中國最大的一塊,是被所有Java線程鎖共享的,不是線程安全的,在JVM啟動時(shí)創(chuàng)建。堆是存儲Java對象的地方,這一點(diǎn)Java虛擬機(jī)規(guī)范中描述是:所有的對象實(shí)例以及數(shù)組都要在堆上分配。Java堆是GC管理的主要區(qū)域,從內(nèi)存回收的角度來看,由于現(xiàn)在GC基本都采用分代收集算法,所以Java堆還可以細(xì)分為:新生代和老年代;新生代再細(xì)致一點(diǎn)有Eden空間、From Survivor空間、To Survivor空間等。
方法區(qū):存放java類的一些信息,包括所加載類的信息包括名稱,修飾符,常量,final變量,靜態(tài)變量即類變量,位于方法區(qū)方法信息以及filed信息,Class 的getClassName, isinterface等方法都源于此,方法區(qū)是線程間共享的,不會頻繁的被GC,存放于JVM的永久代中,大小可以通過參數(shù)來設(shè)置,可以通過-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。
常量池:是方法區(qū)中的一個(gè)數(shù)據(jù)結(jié)構(gòu)。常量池中存儲了如字符串、final變量值、類名和方法名常量。常量池在編譯期間就被確定,并保存在已編譯的.class文件中。一般分為兩類:字面量和應(yīng)用量。字面量就是字符串、final變量等。類名和方法名屬于引用量。引用量最常見的是在調(diào)用方法的時(shí)候,根據(jù)方法名找到方法的引用,并以此定為到函數(shù)體進(jìn)行函數(shù)代碼的執(zhí)行。引用量包含:類和接口的權(quán)限定名、字段的名稱和描述符,方法的名稱和描述符。
本地方法區(qū):本地方法棧和Java棧所發(fā)揮的作用非常相似,區(qū)別不過是Java棧為JVM執(zhí)行Java方法服務(wù),而本地方法棧為JVM執(zhí)行Native方法服務(wù)。本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常。
Java內(nèi)存模型的主要目標(biāo)是定義程序中各個(gè)變量的訪問規(guī)則,即在JVM中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)。此處的變量與Java編程里面的變量有所不同步,它包含了實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對象的元素,但不包含局部變量(基本數(shù)據(jù)類型和引用類型)和方法參數(shù),因?yàn)楹笳呤蔷€程私有的,不會共享,當(dāng)然不存在數(shù)據(jù)競爭問題(如果局部變量是一個(gè)reference引用類型,它引用的對象在Java堆中可被各個(gè)線程共享,但是reference引用本身在Java棧的局部變量表中,是線程私有的)。為了獲得較高的執(zhí)行效能,Java內(nèi)存模型并沒有限制執(zhí)行引起使用處理器的特定寄存器或者緩存來和主內(nèi)存進(jìn)行交互,也沒有限制即時(shí)編譯器進(jìn)行調(diào)整代碼執(zhí)行順序這類優(yōu)化措施。
JMM規(guī)定了所有的變量都存儲在主內(nèi)存(Main Memory)中。每個(gè)線程還有自己的工作內(nèi)存(Working Memory),線程的工作內(nèi)存中保存了該線程使用到的變量的主內(nèi)存的副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量(volatile變量仍然有工作內(nèi)存的拷貝,但是由于它特殊的操作順序性規(guī)定,所以看起來如同直接在主內(nèi)存中讀寫訪問一般)。不同的線程之間也無法直接訪問對方工作內(nèi)存中的變量,線程之間值的傳遞都需要通過主內(nèi)存來完成。
Java內(nèi)存模型是圍繞著并發(fā)編程中原子性、可見性、有序性這三個(gè)特征來建立的,那我們依次看一下這三個(gè)特征:
原子性:操作具有事務(wù)性,指令執(zhí)行要么全部執(zhí)行完,要么回滾!
基本類型數(shù)據(jù)的訪問大都是原子操作,long 和double類型的變量是64位,但是在32位JVM中,32位的JVM會將64位數(shù)據(jù)的讀寫操作分為2次32位的讀寫操作來進(jìn)行,這就導(dǎo)致了long、double類型的變量在32位虛擬機(jī)中是非原子操作,數(shù)據(jù)有可能會被破壞,也就意味著多個(gè)線程在并發(fā)訪問的時(shí)候是線程非安全的。
可見性:變量在主內(nèi)存進(jìn)行變更,對所有的線程都是可見,同步的!
除了volatile關(guān)鍵字能實(shí)現(xiàn)可見性之外,還有synchronized,Lock,final也是可以的
Java內(nèi)存屏障和可見性
由于現(xiàn)代的操作系統(tǒng)都是多處理器.而每一個(gè)處理器都有自己的緩存,并且這些緩存并不是實(shí)時(shí)都與內(nèi)存發(fā)生信息交換.這樣就可能出現(xiàn)一個(gè)cpu上的緩存數(shù)據(jù)與另一個(gè)cpu上的緩存數(shù)據(jù)不一致的問題.而這樣在多線程開發(fā)中,就有可能導(dǎo)致出現(xiàn)一些異常行為.
而操作系統(tǒng)底層為了這些問題,提供了一些內(nèi)存屏障用以解決這樣的問題.目前有4種屏障.
LoadLoad屏障:對于這樣的語句Load1; LoadLoad; Load2,在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
StoreStore屏障:對于這樣的語句Store1; StoreStore; Store2,在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對其它處理器可見。
LoadStore屏障:對于這樣的語句Load1; LoadStore; Store2,在Store2及后續(xù)寫入操作被刷出前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
StoreLoad屏障:對于這樣的語句Store1; StoreLoad; Load2,在Load2及后續(xù)所有讀取操作執(zhí)行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的。在大多數(shù)處理器的實(shí)現(xiàn)中,這個(gè)屏障是個(gè)萬能屏障,兼具其它三種內(nèi)存屏障的功能。
使用
java中對內(nèi)存屏障的使用在一般的代碼中不太容易見到.常見的有兩種.
通過 Synchronized關(guān)鍵字包住的代碼區(qū)域,當(dāng)線程進(jìn)入到該區(qū)域讀取變量信息時(shí),保證讀到的是最新的值.這是因?yàn)樵谕絽^(qū)內(nèi)對變量的寫入操作,在離開同步區(qū)時(shí)就將當(dāng)前線程內(nèi)的數(shù)據(jù)刷新到內(nèi)存中,而對數(shù)據(jù)的讀取也不能從緩存讀取,只能從內(nèi)存中讀取,保證了數(shù)據(jù)的讀有效性.這就是插入了StoreStore屏障
使用了volatile修飾變量,則對變量的寫操作,會插入StoreLoad屏障.
其余的操作,則需要通過Unsafe這個(gè)類來執(zhí)行.
Unsafe中內(nèi)存屏障的使用
UNSAFE.putOrderedObject類似這樣的方法,會插入StoreStore內(nèi)存屏障
Unsafe.putVolatiObject 則是插入了StoreLoad屏障
有序性: 對于一個(gè)線程的代碼而言,我們總是以為代碼的執(zhí)行是從前往后的,依次執(zhí)行的。這么說不能說完全不對,在單線程程序里,確實(shí)會這樣執(zhí)行;但是在多線程并發(fā)時(shí),程序的執(zhí)行就有可能出現(xiàn)亂序。用一句話可以總結(jié)為:在本線程內(nèi)觀察,操作都是有序的;如果在一個(gè)線程中觀察另外一個(gè)線程,所有的操作都是無序的。前半句是指“線程內(nèi)表現(xiàn)為串行語義(WithIn Thread As-if-Serial Semantics)”,后半句是指“指令重排”現(xiàn)象和“工作內(nèi)存和主內(nèi)存同步延遲”現(xiàn)象。
Java提供了兩個(gè)關(guān)鍵字volatile和synchronized來保證多線程之間操作的有序性,volatile關(guān)鍵字本身通過加入內(nèi)存屏障來禁止指令的重排序,而synchronized關(guān)鍵字通過一個(gè)變量在同一時(shí)間只允許有一個(gè)線程對其進(jìn)行加鎖的規(guī)則來實(shí)現(xiàn),
在單線程程序中,不會發(fā)生“指令重排”和“工作內(nèi)存和主內(nèi)存同步延遲”現(xiàn)象,只在多線程程序中出現(xiàn)。
happens-before原則:
Java內(nèi)存模型中定義的兩項(xiàng)操作之間的次序關(guān)系,如果說操作A先行發(fā)生于操作B,操作A產(chǎn)生的影響能被操作B觀察到,“影響”包含了修改了內(nèi)存中共享變量的值、發(fā)送了消息、調(diào)用了方法等。
下面是Java內(nèi)存模型下一些”天然的“happens-before關(guān)系,這些happens-before關(guān)系無須任何同步器協(xié)助就已經(jīng)存在,可以在編碼中直接使用。如果兩個(gè)操作之間的關(guān)系不在此列,并且無法從下列規(guī)則推導(dǎo)出來的話,它們就沒有順序性保障,虛擬機(jī)可以對它們進(jìn)行隨意地重排序。
a.程序次序規(guī)則(Pragram Order Rule):在一個(gè)線程內(nèi),按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。準(zhǔn)確地說應(yīng)該是控制流順序而不是程序代碼順序,因?yàn)橐紤]分支、循環(huán)結(jié)構(gòu)。
b.管程鎖定規(guī)則(Monitor Lock Rule):一個(gè)unlock操作先行發(fā)生于后面對同一個(gè)鎖的lock操作。這里必須強(qiáng)調(diào)的是同一個(gè)鎖,而”后面“是指時(shí)間上的先后順序。
c.volatile變量規(guī)則(Volatile Variable Rule):對一個(gè)volatile變量的寫操作先行發(fā)生于后面對這個(gè)變量的讀取操作,這里的”后面“同樣指時(shí)間上的先后順序。
d.線程啟動規(guī)則(Thread Start Rule):Thread對象的start()方法先行發(fā)生于此線程的每一個(gè)動作。
e.線程終于規(guī)則(Thread Termination Rule):線程中的所有操作都先行發(fā)生于對此線程的終止檢測,我們可以通過Thread.join()方法結(jié)束,Thread.isAlive()的返回值等作段檢測到線程已經(jīng)終止執(zhí)行。
f.線程中斷規(guī)則(Thread Interruption Rule):對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測是否有中斷發(fā)生。
g.對象終結(jié)規(guī)則(Finalizer Rule):一個(gè)對象初始化完成(構(gòu)造方法執(zhí)行完成)先行發(fā)生于它的finalize()方法的開始。
g.傳遞性(Transitivity):如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。
一個(gè)操作”時(shí)間上的先發(fā)生“不代表這個(gè)操作會是”先行發(fā)生“,那如果一個(gè)操作”先行發(fā)生“是否就能推導(dǎo)出這個(gè)操作必定是”時(shí)間上的先發(fā)生 “呢?也是不成立的,一個(gè)典型的例子就是指令重排序。所以時(shí)間上的先后順序與happens-before原則之間基本沒有什么關(guān)系,所以衡量并發(fā)安全問題一切必須以happens-before 原則為準(zhǔn)。
PS:springmvc controller 是單例的,所以成員變量是線程非安全的,建議使用trheadlocal
實(shí)例變量如果是單例的就是線程非安全的,如果不是單例的就是安全的,Struts2默認(rèn)不是單例的,所以訪問一個(gè)類的實(shí)例變量是線程安全的!