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