您好,登錄后才能下訂單哦!
這期內(nèi)容當(dāng)中小編將會(huì)給大家?guī)?lái)有關(guān)怎么深入理解Java內(nèi)存模型JMM,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
Java 內(nèi)存模型(JMM)是一種抽象的概念,并不真實(shí)存在,它描述了一組規(guī)則或規(guī)范,通過(guò)這組規(guī)范定義了程序中各個(gè)變量(包括實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素)的訪問(wèn)方式。試圖屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異,以實(shí)現(xiàn)讓 Java 程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問(wèn)效果。
注意JMM與JVM內(nèi)存區(qū)域劃分的區(qū)別:
JMM描述的是一組規(guī)則,圍繞原子性、有序性和可見(jiàn)性展開;
相似點(diǎn):存在共享區(qū)域和私有區(qū)域
處理器上的寄存器的讀寫的速度比內(nèi)存快幾個(gè)數(shù)量級(jí),為了解決這種速度矛盾,在它們之間加入了高速緩存。
加入高速緩存帶來(lái)了一個(gè)新的問(wèn)題:緩存一致性。如果多個(gè)緩存共享同一塊主內(nèi)存區(qū)域,那么多個(gè)緩存的數(shù)據(jù)可能會(huì)不一致,需要一些協(xié)議來(lái)解決這個(gè)問(wèn)題。
所有的變量都 存儲(chǔ)在主內(nèi)存中,每個(gè)線程還有自己的工作內(nèi)存 ,工作內(nèi)存存儲(chǔ)在高速緩存或者寄存器中,保存了該線程使用的變量的主內(nèi)存副本拷貝。
線程只能直接操作工作內(nèi)存中的變量,不同線程之間的變量值傳遞需要通過(guò)主內(nèi)存來(lái)完成。
方法中的基本類型本地變量將直接存儲(chǔ)在工作內(nèi)存的棧幀結(jié)構(gòu)中;
引用類型的本地變量:引用存儲(chǔ)在工作內(nèi)存,實(shí)際存儲(chǔ)在主內(nèi)存;
成員變量、靜態(tài)變量、類信息均會(huì)被存儲(chǔ)在主內(nèi)存中;
主內(nèi)存共享的方式是線程各拷貝一份數(shù)據(jù)到工作內(nèi)存中,操作完成后就刷新到主內(nèi)存中。
Java 內(nèi)存模型定義了 8 個(gè)操作來(lái)完成主內(nèi)存和工作內(nèi)存的交互操作。
read:把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)焦ぷ鲀?nèi)存中
load:在 read 之后執(zhí)行,把 read 得到的值放入工作內(nèi)存的變量副本中
use:把工作內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎
assign:把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量
store:把工作內(nèi)存的一個(gè)變量的值傳送到主內(nèi)存中
write:在 store 之后執(zhí)行,把 store 得到的值放入主內(nèi)存的變量中
lock:作用于主內(nèi)存的變量
unlock
在單線程環(huán)境下不能改變程序的運(yùn)行結(jié)果;
存在數(shù)據(jù)依賴關(guān)系的不允許重排序;
無(wú)法通過(guò)Happens-before原則推到出來(lái)的,才能進(jìn)行指令的重排序。
Java 內(nèi)存模型保證了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如對(duì)一個(gè) int 類型的變量執(zhí)行 assign 賦值操作,這個(gè)操作就是原子性的。但是 Java 內(nèi)存模型允許虛擬機(jī)將沒(méi)有被 volatile 修飾的 64 位數(shù)據(jù)(long,double)的讀寫操作劃分為兩次 32 位的操作來(lái)進(jìn)行,即 load、store、read 和 write 操作可以不具備原子性。
有一個(gè)錯(cuò)誤認(rèn)識(shí)就是,int 等原子性的類型在多線程環(huán)境中不會(huì)出現(xiàn)線程安全問(wèn)題。前面的線程不安全示例代碼中,cnt 屬于 int 類型變量,1000 個(gè)線程對(duì)它進(jìn)行自增操作之后,得到的值為 997 而不是 1000。
為了方便討論,將內(nèi)存間的交互操作簡(jiǎn)化為 3 個(gè):load、assign、store。
下圖演示了兩個(gè)線程同時(shí)對(duì) cnt 進(jìn)行操作,load、assign、store 這一系列操作整體上看不具備原子性,那么在 T1 修改 cnt 并且還沒(méi)有將修改后的值寫入主內(nèi)存,T2 依然可以讀入舊值??梢钥闯?,這兩個(gè)線程雖然執(zhí)行了兩次自增運(yùn)算,但是主內(nèi)存中 cnt 的值最后為 1 而不是 2。因此對(duì) int 類型讀寫操作滿足原子性只是說(shuō)明 load、assign、store 這些單個(gè)操作具備原子性。
AtomicInteger 能保證多個(gè)線程修改的原子性。
使用 AtomicInteger 重寫之前線程不安全的代碼之后得到以下線程安全實(shí)現(xiàn):
public class AtomicExample { private AtomicInteger cnt = new AtomicInteger(); public void add() { cnt.incrementAndGet(); } public int get() { return cnt.get(); } }復(fù)制代碼
public static void main(String[] args) throws InterruptedException { final int threadSize = 1000; AtomicExample example = new AtomicExample(); // 只修改這條語(yǔ)句 final CountDownLatch countDownLatch = new CountDownLatch(threadSize); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < threadSize; i++) { executorService.execute(() -> { example.add(); countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(example.get()); }復(fù)制代碼
1000復(fù)制代碼
除了使用原子類之外,也可以使用 synchronized 互斥鎖來(lái)保證操作的原子性。它對(duì)應(yīng)的內(nèi)存間交互操作為:lock 和 unlock,在虛擬機(jī)實(shí)現(xiàn)上對(duì)應(yīng)的字節(jié)碼指令為 monitorenter 和 monitorexit。
public class AtomicSynchronizedExample { private int cnt = 0; public synchronized void add() { cnt++; } public synchronized int get() { return cnt; } }復(fù)制代碼
public static void main(String[] args) throws InterruptedException { final int threadSize = 1000; AtomicSynchronizedExample example = new AtomicSynchronizedExample(); final CountDownLatch countDownLatch = new CountDownLatch(threadSize); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < threadSize; i++) { executorService.execute(() -> { example.add(); countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println(example.get()); }復(fù)制代碼
1000復(fù)制代碼
可見(jiàn)性指當(dāng)一個(gè)線程修改了共享變量的值,其它線程能夠立即得知這個(gè)修改。Java 內(nèi)存模型是通過(guò)在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值來(lái)實(shí)現(xiàn)可見(jiàn)性的。JMM 內(nèi)部的實(shí)現(xiàn)通常是依賴于所謂的 內(nèi)存屏障 ,通過(guò) 禁止某些重排序 的方式,提供內(nèi)存 可見(jiàn)性保證 ,也就是實(shí)現(xiàn)了 各種 happen-before 規(guī)則 。與此同時(shí),更多復(fù)雜度在于,需要盡量確保各種編譯器、各種體系結(jié)構(gòu)的處理器,都能夠提供一致的行為。
主要有有三種實(shí)現(xiàn)可見(jiàn)性的方式:
volatile,會(huì) 強(qiáng)制 將該變量自己和當(dāng)時(shí)其他變量的狀態(tài)都 刷出緩存 。
synchronized,對(duì)一個(gè)變量執(zhí)行 unlock 操作之前,必須把變量值同步回主內(nèi)存。
final,被 final 關(guān)鍵字修飾的字段在構(gòu)造器中一旦初始化完成,并且沒(méi)有發(fā)生 this 逃逸(其它線程通過(guò) this 引用訪問(wèn)到初始化了一半的對(duì)象),那么其它線程就能看見(jiàn) final 字段的值。
對(duì)前面的線程不安全示例中的 cnt 變量使用 volatile 修飾,不能解決線程不安全問(wèn)題,因?yàn)?volatile 并不能保證操作的原子性。
有序性是指:在本線程內(nèi)觀察,所有操作都是有序的。在一個(gè)線程觀察另一個(gè)線程,所有操作都是無(wú)序的,無(wú)序是因?yàn)榘l(fā)生了指令重排序。在 Java 內(nèi)存模型中,允許編譯器和處理器對(duì)指令進(jìn)行重排序,重排序過(guò)程不會(huì)影響到單線程程序的執(zhí)行,卻會(huì)影響到多線程并發(fā)執(zhí)行的正確性。
volatile 關(guān)鍵字通過(guò)添加內(nèi)存屏障的方式來(lái)禁止指令重排,即重排序時(shí)不能把后面的指令放到內(nèi)存屏障之前。
也可以通過(guò) synchronized 來(lái)保證有序性,它保證每個(gè)時(shí)刻只有一個(gè)線程執(zhí)行同步代碼,相當(dāng)于是讓線程順序執(zhí)行同步代碼。
JSR-133內(nèi)存模型使用先行發(fā)生原則在Java內(nèi)存模型中保證多線程操作 可見(jiàn)性 的機(jī)制,也是對(duì)早期語(yǔ)言規(guī)范中含糊的可見(jiàn)性概念的一個(gè)精確定義。上面提到了可以用 volatile 和 synchronized 來(lái)保證有序性。除此之外,JVM 還規(guī)定了先行發(fā)生原則,讓一個(gè)操作 無(wú)需控制 就能先于另一個(gè)操作完成。
由于 指令重排序 的存在,兩個(gè)操作之間有happen-before關(guān)系, 并不意味著前一個(gè)操作必須要在后一個(gè)操作之前執(zhí)行。 僅僅要求前一個(gè)操作的執(zhí)行結(jié)果對(duì)于后一個(gè)操作是可見(jiàn)的,并且前一個(gè)操作 按順序 排在第二個(gè)操作之前。
Single Thread rule
在一個(gè)線程內(nèi),在程序前面的操作先行發(fā)生于后面的操作。
Monitor Lock Rule
一個(gè) unlock(解鎖) 操作 先行發(fā)生于 后面對(duì)同一個(gè)鎖的 lock(加鎖) 操作。
Volatile Variable Rule
對(duì)一個(gè) volatile 變量的 寫操作 先行發(fā)生于后面對(duì)這個(gè)變量的 讀操作 。
Thread Start Rule
Thread 對(duì)象的 start() 方法調(diào)用先行發(fā)生于此線程的每一個(gè)動(dòng)作。
Thread Join Rule
Thread 對(duì)象的結(jié)束先行發(fā)生于 join() 方法返回。
Thread Interruption Rule
對(duì)線程 interrupt() 方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生,可以通過(guò) interrupted() 方法檢測(cè)到是否有中斷發(fā)生。
Finalizer Rule
一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的 finalize() 方法的開始。
Transitivity
如果操作 A 先行發(fā)生于操作 B,操作 B 先行發(fā)生于操作 C,那么操作 A 先行發(fā)生于操作 C。
上述就是小編為大家分享的怎么深入理解Java內(nèi)存模型JMM了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。