日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

什么?面试官问我Java内存模型!这不得给我加薪?

發布時間:2023/12/4 java 59 豆豆
生活随笔 收集整理的這篇文章主要介紹了 什么?面试官问我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原理:被volatile關鍵字修飾的變量,編譯器與運行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。
  • 在訪問volatile變量時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變量是一種比sychronized關鍵字更輕量級的同步機制。當對非 volatile 變量進行讀寫的時候,每個線程先從內存拷貝變量到CPU緩存中。如果計算機有多個CPU,每個線程可能在不同的CPU上被處理,這意味著每個線程可以拷貝到不同的 CPU cache 中。而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內存中讀,跳過 CPU cache 這一步。
  • volatile在Java并發編程中常用于保持內存可見性和防止指令重排序。內存可見性(Memory Visibility):所有線程都能看到共享內存的最新狀態;防止指令重排:在基于偏序關系的Happens-Before內存模型中,指令重排技術大大提高了程序執行效率,但同時也引入了一些問題。
  • 可見性:volatile保持內存可見性的特殊規則:read、load、use動作必須連續出現;assign、store、write動作必須連續出現;每次讀取前必須先從主內存刷新最新的值;每次寫入后必須立即同步回主內存當中。也就是說,volatile關鍵字修飾的變量看到的隨時是自己的最新值。在線程1中對變量v的最新修改,對線程2是可見的。
  • 內存屏障:volatile防止指令重排的策略:在每個volatile寫操作的前面插入一個StoreStore屏障;在每個volatile寫操作的后面插入一個StoreLoad屏障;在每個volatile讀操作的后面插入一個LoadLoad屏障;在每個volatile讀操作的后面插入一個LoadStore屏障。
  • volatile 性能: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 BarriersLoad1;LoadLoad;Load2確保Load1數據的裝載先于Load2及所有后續裝載指令的裝載
    StoreStore BarriersStore1;StoreStore;Store2確保Store1數據對其他處理器可見(刷新達到內存)先于Store2及所有后續存儲指令的存儲
    LoadStore BarriersLoad1;LoadStrore;Store2確保Load1數據裝載先于Store2及所有后續的存儲指令刷新到內存
    StoreLoad BarriersStore;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内存模型!这不得给我加薪?的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。