什么?面试官问我Java内存模型!这不得给我加薪?
內(nèi)存模型的基礎(chǔ)
- 通信 線程之間以何種機(jī)制來交換信息
- 共享內(nèi)存 隱式通信
- 消息傳遞 顯示通信
- 同步 程序中用于控制不同線程間操作,發(fā)生的相對(duì)順序的機(jī)制
- 共享內(nèi)存 顯式同步
- 消息傳遞 隱式同步
Java線程線程之間是通過共享內(nèi)存的方式實(shí)現(xiàn)通信的.
內(nèi)存模型的抽象結(jié)構(gòu)
- 共享變量
共享變量手內(nèi)存模型影響,線程會(huì)去主內(nèi)存里去加載共享變量,當(dāng)線程需要改變共享變量時(shí),會(huì)將本地內(nèi)存已更改的副本提交到主內(nèi)存.
- 局部變量
局部變量不會(huì)受內(nèi)存模型的影響
線程之間通信
指令重排
- 編譯器優(yōu)化的重排序
- 指令級(jí)并行的重排序
- 內(nèi)存系統(tǒng)的重排序
什么是指令重排?
int i=0; 2 int j=1;
按照我們的認(rèn)知,程序是一行一行往下執(zhí)行的,但是由于編譯器或運(yùn)行時(shí)環(huán)境為了優(yōu)化程序性能,采取對(duì)指令進(jìn)行重新排序執(zhí)行,也就是說在計(jì)算機(jī)執(zhí)行上面兩句話的時(shí)候,有可能第二條語(yǔ)句會(huì)優(yōu)先于第一條語(yǔ)句執(zhí)行.
然而并不是所有的指令都能重排,重排需要基于數(shù)據(jù)依賴性.
數(shù)據(jù)依賴性
如果兩個(gè)操作訪問同一個(gè)變量,且這兩個(gè)操作中有一個(gè)為寫操作,此時(shí)這兩個(gè)操作之間就存在數(shù)據(jù)依賴性。數(shù)據(jù)依賴分下列三種類型:
| 寫后讀 | a=1;b=a; | 寫一個(gè)變量之后,再讀這個(gè)位置. |
| 寫后寫 | a=1;a=2; | 寫一個(gè)變量之后,再寫這個(gè)變量. |
| 讀后寫 | a=b;b=1; | 讀一個(gè)變量之后,再寫這個(gè)變量. |
上面的情況,如果重排序了兩個(gè)操作的執(zhí)行順序,程序的執(zhí)行結(jié)果將會(huì)跟預(yù)期完全不一樣.
所以說,雖然編譯器和處理器可能會(huì)對(duì)操作做重排序,但是編譯器和處理器在重排序時(shí),會(huì)遵守?cái)?shù)據(jù)依賴性,編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序。
注意,這里所說的數(shù)據(jù)依賴性僅針對(duì)單個(gè)處理器中執(zhí)行的指令序列和單個(gè)線程中執(zhí)行的操作,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮。
as-if-serial
定義:不管怎么重排序(編譯器和處理器為了提?并?度),(單線程) 程序的執(zhí)?結(jié)果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語(yǔ)義。
happens-before
happens-before是JMM的最核心概念之一
JMM設(shè)計(jì)意圖
- 程序員對(duì)內(nèi)存模型的使用
- 為程序員提供足夠強(qiáng)的內(nèi)存可見性保證
- 編譯器和處理器對(duì)內(nèi)存模型的實(shí)現(xiàn)
- 對(duì)編譯器和處理器的限制要盡可能的放松
JMM禁止:
禁止編譯器和處理器會(huì)改變程序執(zhí)行結(jié)果的重排序.
JMM允許:
允許編譯器和處理器不會(huì)改變程序執(zhí)行結(jié)果的重排序.
happens-before規(guī)則
在JMM中,如果?個(gè)操作執(zhí)?的結(jié)果需要對(duì)另?個(gè)操作可?,那么這兩個(gè)操作之間必須要存在happens-before關(guān)系.
- 程序順序規(guī)則 ?個(gè)線程中的每個(gè)操作,happens-before于該線程中的任意后續(xù)操作.
- 監(jiān)視器鎖規(guī)則 對(duì)?個(gè)鎖的解鎖,happens-before于隨后對(duì)這個(gè)鎖的加鎖.
- volatile變量規(guī)則 對(duì)?個(gè)volatile域的寫,happens-before于任意后續(xù)對(duì)這個(gè)volatile域的讀.
- 傳遞性 如果A happens-before B,且B happens-before C,那么A happens-before C.
- start()規(guī)則 如果線程A執(zhí)?操作ThreadB.start()(啟動(dòng)線程B),那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作。
- join()規(guī)則 如果線程A執(zhí)?操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()返回
- 線程中斷規(guī)則 對(duì)線程interrupt?法的調(diào)?happens-before于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)?.
- 對(duì)象終結(jié)規(guī)則 ?個(gè)對(duì)象的初始化的完成,也就是構(gòu)造函數(shù)執(zhí)?的結(jié)束?定 happens-before它的finalize()?法.
JMM向程序員提供的happens-before規(guī)則能滿?程序員的需求.
JMM對(duì)編譯器和處理器的束縛已經(jīng)盡可能少.
JMM對(duì)程序員的承諾
如果?個(gè)操作happens-before另?個(gè)操作,那么第?個(gè)操作的執(zhí)?結(jié)果將對(duì)第?個(gè)操作 可?,?且第?個(gè)操作的執(zhí)?順序排在第?個(gè)操作之前.
JMM對(duì)編譯器和處理器重排序的約束原則
兩個(gè)操作之間存在happens-before關(guān)系,并不意味著Java平臺(tái)的具體實(shí)現(xiàn)必須要按照 happens-before關(guān)系指定的順序來執(zhí)?.
例子:
1 public class Demo29 { 2 int a=0; 3 boolean flag=false; 4 public void writer(){ 5 a=1; //1 6 flag=true; //2 7 } 8 public void reader(){ 9 if(flag){ //3 10 int i=a * a; //4 11 } 12 } 13 }假如線程B在進(jìn)行操作4時(shí),能否看到線程A在操作1對(duì)共享變量a的寫入呢? 不一定
| 時(shí)刻 | 線程A | 線程B | | T1 | flag=true | | | T2 | | if(flag) | | T3 | | int i=a*a | | T4 | a=1 | |當(dāng)線程A在執(zhí)行writer方法時(shí),因?yàn)橹噶钪嘏判?會(huì)先執(zhí)行flag=true,再執(zhí)行a=1.而線程B在執(zhí)行操作4時(shí)就會(huì)讀不到線程A對(duì)共享變量a的寫入,導(dǎo)致運(yùn)行結(jié)果超出預(yù)期.
解決方案1:
通過加鎖的方式來解決
1 public class Demo29 { 2 int a=0; 3 boolean flag=false; 4 public synchronized void writer(){ 5 a=1; //1 6 flag=true; //2 7 } 8 public synchronized void reader(){ 9 if(flag){ //3 10 int i=a * a; //4 11 } 12 } 13 }鎖的內(nèi)存語(yǔ)義:
- 線程A釋放?個(gè)鎖,實(shí)質(zhì)上是線程A向接下來將要獲取這個(gè)鎖的某個(gè)線程發(fā)出了(線程A 對(duì)共享變量所做修改的)消息。
- 線程B獲取?個(gè)鎖,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(在釋放這個(gè)鎖之前對(duì)共 享變量所做修改的)消息。
- 線程A釋放鎖,隨后線程B獲取這個(gè)鎖,這個(gè)過程實(shí)質(zhì)上是線程A通過主內(nèi)存向線程B發(fā) 送消息。
volatile的作用
volatile內(nèi)存語(yǔ)義
- 線程A寫?個(gè)volatile變量,實(shí)質(zhì)上是線程A向接下來將要讀這個(gè)volatile變量的某個(gè)線程 發(fā)出了(其對(duì)共享變量所做修改的)消息。
- 線程B讀?個(gè)volatile變量,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(在寫這個(gè)volatile 變量之前對(duì)共享變量所做修改的)消息。
- 線程A寫?個(gè)volatile變量,隨后線程B讀這個(gè)volatile變量,這個(gè)過程實(shí)質(zhì)上是線程A通過 主內(nèi)存向線程B發(fā)送消息。
volatile內(nèi)存語(yǔ)義的實(shí)現(xiàn)
| 第一個(gè)操作 | 普通讀/寫 |
| 普通讀/寫 | Y |
| volatile讀 | N |
| volatile寫 | Y |
-
當(dāng)?shù)?個(gè)操作是volatile寫時(shí),不管第?個(gè)操作是什么,都不能重排序。
-
當(dāng)?shù)?個(gè)操作是volatile讀時(shí),不管第?個(gè)操作是什么,都不能重排序。
-
當(dāng)?shù)?個(gè)操作是volatile寫,第?個(gè)操作是volatile讀時(shí),不能重排序。
內(nèi)存屏障
| LoadLoad Barriers | Load1;LoadLoad;Load2 | 確保Load1數(shù)據(jù)的裝載先于Load2及所有后續(xù)裝載指令的裝載 |
| StoreStore Barriers | Store1;StoreStore;Store2 | 確保Store1數(shù)據(jù)對(duì)其他處理器可見(刷新達(dá)到內(nèi)存)先于Store2及所有后續(xù)存儲(chǔ)指令的存儲(chǔ) |
| LoadStore Barriers | Load1;LoadStrore;Store2 | 確保Load1數(shù)據(jù)裝載先于Store2及所有后續(xù)的存儲(chǔ)指令刷新到內(nèi)存 |
| StoreLoad Barriers | Store;StoreLoad;Load2 | 確保Store1數(shù)據(jù)對(duì)其他處理器變得可見(指刷新到內(nèi)存)先于Load2及所有后續(xù)裝載指令的裝載.StoreLoad Barriers會(huì)使該屏障之前的所有內(nèi)存訪問指令(存儲(chǔ)和裝載指令)完成之后,才執(zhí)行該屏障之后的內(nèi)存訪問指令 |
- 在每個(gè)volatile寫操作的前?插??個(gè)StoreStore屏障
- 在每個(gè)volatile寫操作的后?插??個(gè)StoreLoad屏障
- 在每個(gè)volatile讀操作的后?插??個(gè)LoadLoad屏障
- 在每個(gè)volatile讀操作的后?插??個(gè)LoadStore屏障
Final的內(nèi)存語(yǔ)義
寫final域的重排序規(guī)則
- JMM禁止編譯器把final域的寫重排序到構(gòu)造函數(shù)之外.
- 編譯器會(huì)在final域的寫之后,構(gòu)造函數(shù)return之前插入一個(gè)StoreStore屏障
讀final域的重排序規(guī)則
- 在?個(gè)線程中,初次讀對(duì)象引?與初次讀該對(duì)象包含的final域,JMM禁?處理器重排序這兩個(gè)操作
- 在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)final引用的對(duì)象的成員域的寫入,與隨后在構(gòu)造函數(shù)外把這個(gè)被構(gòu)造對(duì)象的引用賦值給一個(gè)引用變量,這兩個(gè)操作之間不能重排序
寫final域的重排序規(guī)則
- 在構(gòu)造函數(shù)內(nèi)對(duì)?個(gè)final引?的對(duì)象的成員域 的寫?,與隨后在構(gòu)造函數(shù)外把這個(gè)被構(gòu)造對(duì)象的引?賦值給?個(gè)引?變量,這兩個(gè)操作之 間不能重排序。
多線程下的單例模式
雙重檢查鎖定
1 public class DoubleCheckedLocking {2 private static DoubleCheckedLocking doubleCheckedLocking;3 4 private DoubleCheckedLocking() {5 6 }7 8 public static DoubleCheckedLocking getInstance() { 9 if (doubleCheckedLocking == null) { 10 synchronized (DoubleCheckedLocking.class) { 11 if (doubleCheckedLocking == null) { 12 doubleCheckedLocking = new DoubleCheckedLocking();//問題出現(xiàn)在這里 13 } 14 } 15 } 16 return doubleCheckedLocking; 17 } 18 }我們來看看這段雙重檢查鎖定的單例模式有什么問題?
線程A設(shè)置指向剛分配的內(nèi)存地址后,線程B就判斷doubleCheckedLocking對(duì)象是否為空,然后直接返回未初始化的doubleCheckedLocking對(duì)象,這樣會(huì)引發(fā)出很嚴(yán)重的問題.
解決方案1:
使用volatile,禁止2和3重排序
1 public class DoubleCheckedLocking { 2 private volatile static DoubleCheckedLocking doubleCheckedLocking; 3 4 private DoubleCheckedLocking() { 5 6 } 7 8 public static DoubleCheckedLocking getInstance() { 9 if (doubleCheckedLocking == null) { 10 synchronized (DoubleCheckedLocking.class) { 11 if (doubleCheckedLocking == null) { 12 doubleCheckedLocking = new DoubleCheckedLocking();//問題出現(xiàn)在這里 13 } 14 } 15 } 16 return doubleCheckedLocking; 17 } 18 }解決方案2:
基于類初始化,允許2和3重排序,但不允許其他線程"看到這個(gè)重排序"
1 public class InstanceFactory { 2 private static class InstanceHolder { 3 public static DoubleCheckedLocking doubleCheckedLocking = new DoubleCheckedLocking(); 4 } 5 6 public static DoubleCheckedLocking getInstance() { 7 return InstanceHolder.doubleCheckedLocking; 8 } 9 }這里使用到了靜態(tài)內(nèi)部類的靜態(tài)屬性,類的靜態(tài)屬性只會(huì)在第一次調(diào)用的時(shí)候初始化,而且會(huì)有一個(gè)Class對(duì)象的初始化鎖,從而確保只會(huì)發(fā)生一次初始化.
最后,祝大家早日學(xué)有所成,拿到滿意offer
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
以上是生活随笔為你收集整理的什么?面试官问我Java内存模型!这不得给我加薪?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 初级Java开发面试必问项!!! 标识符
- 下一篇: 工行开户成功多久寄送