久久综合九色综合97婷婷-美女视频黄频a免费-精品日本一区二区三区在线观看-日韩中文无码有码免费视频-亚洲中文字幕无码专区-扒开双腿疯狂进出爽爽爽动态照片-国产乱理伦片在线观看夜-高清极品美女毛茸茸-欧美寡妇性猛交XXX-国产亚洲精品99在线播放-日韩美女毛片又爽又大毛片,99久久久无码国产精品9,国产成a人片在线观看视频下载,欧美疯狂xxxx吞精视频

有趣生活

當前位置:首頁>職場>關于java多線程高并發面試題總結(來了大廠面試Java崗)

關于java多線程高并發面試題總結(來了大廠面試Java崗)

發布時間:2024-01-24閱讀(16)

導讀再談多線程在我們的操作系統之上,可以同時運行很多個進程,并且每個進程之間相互隔離互不干擾。我們的CPU會通過時間片輪轉算法,為每一個進程分配時間片,并在時間....再談多線程

在我們的操作系統之上,可以同時運行很多個進程,并且每個進程之間相互隔離互不干擾。

我們的CPU會通過時間片輪轉算法,為每一個進程分配時間片,并在時間片使用結束后切換下一個進程繼續執行,通過這種方式來實現宏觀上的多個程序同時運行。

由于每個進程都有一個自己的內存空間,進程之間的通信就變得非常麻煩(比如要共享某些數據)而且執行不同進程會產生上下文切換,非常耗時,那么有沒有一種更好地方案呢?

后來,線程橫空出世,一個進程可以有多個線程,線程是程序執行中一個單一的順序控制流程,現在線程才是程序執行流的最小單元,各個線程之間共享程序的內存空間(也就是所在進程的內存空間),上下文切換速度也高于進程。

現在有這樣一個問題:

public static void main(String[] args) { int[] arr = new int[]{3, 1, 5, 2, 4}; //請將上面的數組按升序輸出}

按照正常思維,我們肯定是這樣:

public static void main(String[] args) { int[] arr = new int[]{3, 1, 5, 2, 4};//直接排序吧 Arrays.sort(arr); for (int i : arr) { System.out.println(i); }}


而我們學習了多線程之后,可以換個思路來實現:

public static void main(String[] args) { int[] arr = new int[]{3, 1, 5, 2, 4}; for (int i : arr) { new Thread(() -> { try { Thread.sleep(i * 1000); //越小的數休眠時間越短,優先被打印 System.out.println(i); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); }}

我們接觸過的很多框架都在使用多線程,比如Tomcat服務器,所有用戶的請求都是通過不同的線程來進行處理的,這樣我們的網站才可以同時響應多個用戶的請求,要是沒有多線程,可想而知服務器的處理效率會有多低。


在Java 5的時候,新增了java.util.concurrent(JUC)包,其中包括大量用于多線程編程的工具類,目的是為了更好的支持高并發任務,讓開發者進行多線程編程時減少競爭條件和死鎖的問題!


并發與并行

我們經常聽到并發編程,那么這個并發代表的是什么意思呢?而與之相似的并行又是什么意思?它們之間有什么區別?

比如現在一共有三個工作需要我們去完成。

關于java多線程高并發面試題總結(來了大廠面試Java崗)(1)

順序執行

順序執行其實很好理解,就是我們依次去將這些任務完成了:

關于java多線程高并發面試題總結(來了大廠面試Java崗)(2)

實際上就是我們同一時間只能處理一個任務,所以需要前一個任務完成之后,才能繼續下一個任務,依次完成所有任務。

并發執行

并發執行也是我們同一時間只能處理一個任務,但是我們可以每個任務輪著做(時間片輪轉):

關于java多線程高并發面試題總結(來了大廠面試Java崗)(3)

而我們Java中的線程,正是這種機制,當我們需要同時處理上百個上千個任務時,很明顯CPU的數量是不可能趕得上我們的線程數的,所以說這時就要求我們的程序有良好的并發性能,來應對同一時間大量的任務處理。

學習Java并發編程,能夠讓我們在以后的實際場景中,知道該如何應對高并發的情況。

并行執行

并行執行就突破了同一時間只能處理一個任務的限制,我們同一時間可以做多個任務:

關于java多線程高并發面試題總結(來了大廠面試Java崗)(4)

比如我們要進行一些排序操作,就可以用到并行計算,只需要等待所有子任務完成,最后將結果匯總即可。包括分布式計算模型MapReduce,也是采用的并行計算思路。


再談鎖機制

談到鎖機制,相信各位應該并不陌生了,我們在JavaSE階段,通過使用synchronized關鍵字來實現鎖,

這樣就能夠很好地解決線程之間爭搶資源的情況。

那么,synchronized底層到底是如何實現的呢?

我們知道,使用synchronized,一定是和某個對象相關聯的,比如我們要對某一段代碼加鎖,那么我們就需要提供一個對象來作為鎖本身

public static void main(String[] args) { synchronized (Main.class) { //這里使用的是Main類的Class對象作為鎖 }}

我們來看看,它變成字節碼之后會用到哪些指令:

關于java多線程高并發面試題總結(來了大廠面試Java崗)(5)

其中最關鍵的就是monitorenter指令了,可以看到之后也有monitorexit與之進行匹配(注意這里有2個),monitorenter和monitorexit分別對應加鎖和釋放鎖,在執行monitorenter之前需要嘗試獲取鎖。

每個對象都有一個monitor監視器與之對應,而這里正是去獲取對象監視器的所有權,一旦monitor所有權被某個線程持有,那么其他線程將無法獲得(管程模型的一種實現)。


在代碼執行完成之后,我們可以看到,一共有兩個monitorexit在等著我們,那么為什么這里會有兩個呢?

按理說monitorenter和monitorexit不應該一一對應嗎,這里為什么要釋放鎖兩次呢?

首先我們來看第一個,這里在釋放鎖之后,會馬上進入到一個goto指令,

跳轉到15行,而我們的15行對應的指令就是方法的返回指令,其實正常情況下只會執行第一個monitorexit釋放鎖,在釋放鎖之后就接著同步代碼塊后面的內容繼續向下執行了。

而第二個,其實是用來處理異常的,可以看到,它的位置是在12行,如果程序運行發生異常,那么就會執行第二個monitorexit,并且會繼續向下通過athrow指令拋出異常,而不是直接跳轉到15行正常運行下去。

關于java多線程高并發面試題總結(來了大廠面試Java崗)(6)


實際上synchronized使用的鎖就是存儲在Java對象頭中的,我們知道,對象是存放在堆內存中的,而每個對象內部,都有一部分空間用于存儲對象頭信息。

而對象頭信息中,則包含了Mark Word用于存放hashCode和對象的鎖信息,在不同狀態下,它存儲的數據結構有一些不同。

關于java多線程高并發面試題總結(來了大廠面試Java崗)(7)

重量級鎖

在JDK6之前,synchronized一直被稱為重量級鎖,monitor依賴于底層操作系統的Lock實現。

Java的線程是映射到操作系統的原生線程上,切換成本較高。而在JDK6之后,鎖的實現得到了改進。

我們先從最原始的重量級鎖開始:

我們說了,每個對象都有一個monitor與之關聯,在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的:

ObjectMonitor() { _header = NULL; _count = 0; //記錄個數 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; //處于wait狀態的線程,會被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //處于等待鎖block狀態的線程,會被加入到該列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ;}

每個等待鎖的線程都會被封裝成ObjectWaiter對象,進入到如下機制:

關于java多線程高并發面試題總結(來了大廠面試Java崗)(8)


設計思路

ObjectWaiter首先會進入 Entry Set等著,

當線程獲取到對象的monitor后進入 The Owner 區域并把monitor中的owner變量設置為當前線程,

同時monitor中的計數器count加1,若線程調用wait()方法,將釋放當前持有的monitor,owner變量恢復為null,count自減1,

同時該線程進入 WaitSet集合中等待被喚醒。若當前線程執行完畢也將釋放monitor并復位變量的值,以便其他線程進入獲取對象的monitor。


雖然這樣的設計思路非常合理,但是在大多數應用上,每一個線程占用同步代碼塊的時間并不是很長,我們完全沒有必要將競爭中的線程掛起然后又喚醒,并且現代CPU基本都是多核心運行的,我們可以采用一種新的思路來實現鎖。


新思路

在JDK1.4.2時,引入了自旋鎖(JDK6之后默認開啟),它不會將處于等待狀態的線程掛起,而是通過無限循環的方式,不斷檢測是否能夠獲取鎖。

由于單個線程占用鎖的時間非常短,所以說循環次數不會太多,可能很快就能夠拿到鎖并運行,這就是自旋鎖。

當然,僅僅是在等待時間非常短的情況下,自旋鎖的表現會很好,但是如果等待時間太長,由于循環是需要處理器繼續運算的,所以這樣只會浪費處理器資源,因此自旋鎖的等待時間是有限制的,默認情況下為10次,如果失敗,那么會進而采用重量級鎖機制。

關于java多線程高并發面試題總結(來了大廠面試Java崗)(9)


在JDK6之后,自旋鎖得到了一次優化,自旋的次數限制不再是固定的,而是自適應變化的。

比如在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運行,那么這次自旋也是有可能成功的,所以會允許自旋更多次。

當然,如果某個鎖經常都自旋失敗,那么有可能會不再采用自旋策略,而是直接使用重量級鎖。

輕量級鎖

從JDK 1.6開始,為了減少獲得鎖和釋放鎖帶來的性能消耗,就引入了輕量級鎖。

輕量級鎖的目標是,在無競爭情況下,減少重量級鎖產生的性能消耗

(并不是為了代替重量級鎖,實際上就是賭一手同一時間只有一個線程在占用資源),

包括系統調用引起的內核態與用戶態切換、線程阻塞造成的線程切換等。

它不像是重量級鎖那樣,需要向操作系統申請互斥量。


運作機制

在即將開始執行同步代碼塊中的內容時,會首先檢查對象的Mark Word,查看鎖對象是否被其他線程占用,

如果沒有任何線程占用,那么會在當前線程中所處的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于復制并存儲對象目前的Mark Word信息(官方稱為Displaced Mark Word)。

接著,虛擬機將使用CAS操作將對象的Mark Word更新為輕量級鎖狀態(數據結構變為指向Lock Record的指針,指向的是當前的棧幀)

CAS(Compare And Swap)是一種無鎖算法,

它并不會為對象加鎖,而是在執行的時候,看看當前數據的值是不是我們預期的那樣,

如果是,那就正常進行替換,

如果不是,那么就替換失敗。

比如有兩個線程都需要修改變量i的值,默認為10,

現在一個線程要將其修改為20,另一個要修改為30,

如果他們都使用CAS算法,那么并不會加鎖訪問i,而是直接嘗試修改i的值,

但是在修改時,需要確認i是不是10,

如果是,表示其他線程還沒對其進行修改,

如果不是,那么說明其他線程已經將其修改,此時不能完成修改任務,修改失敗。

在CPU中,CAS操作使用的是cmpxchg指令,能夠從最底層硬件層面得到效率的提升。

如果CAS操作失敗了的話,那么說明可能這時有線程已經進入這個同步代碼塊了,

這時虛擬機會再次檢查對象的Mark Word,是否指向當前線程的棧幀,

如果是,說明不是其他線程,而是當前線程已經有了這個對象的鎖,直接放心大膽進同步代碼塊即可。

如果不是,那確實是被其他線程占用了。

這時,輕量級鎖一開始的想法就是錯的(這時有對象在競爭資源,已經賭輸了),所以說只能將鎖膨脹為重量級鎖,按照重量級鎖的操作執行(注意鎖的膨脹是不可逆的)

如圖所示:

關于java多線程高并發面試題總結(來了大廠面試Java崗)(10)

所以,輕量級鎖 -> 失敗 -> 自適應自旋鎖 -> 失敗 -> 重量級鎖

解鎖過程同樣采用CAS算法,如果對象的MarkWord仍然指向線程的鎖記錄,

那么就用CAS操作把對象的MarkWord和復制到棧幀中的Displaced Mark Word進行交換。

如果替換失敗,說明其他線程嘗試過獲取該鎖,在釋放鎖的同時,需要喚醒被掛起的線程。

偏向鎖

偏向鎖相比輕量級鎖更純粹,干脆就把整個同步都消除掉,不需要再進行CAS操作了。

它的出現主要是得益于人們發現某些情況下某個鎖頻繁地被同一個線程獲取,

這種情況下,我們可以對輕量級鎖進一步優化。


偏向鎖實際上就是專門為單個線程而生的,當某個線程第一次獲得鎖時,如果接下來都沒有其他線程獲取此鎖,那么持有鎖的線程將不再需要進行同步操作。

可以從之前的MarkWord結構中看到,偏向鎖也會通過CAS操作記錄線程的ID,如果一直都是同一個線程獲取此鎖,那么完全沒有必要在進行額外的CAS操作。

當然,如果有其他線程來搶了,那么偏向鎖會根據當前狀態,決定是否要恢復到未鎖定或是膨脹為輕量級鎖。

如果我們需要使用偏向鎖,可以添加-XX: UseBiased參數來開啟。

所以,最終的鎖等級為:未鎖定 < 偏向鎖 < 輕量級鎖 < 重量級鎖

值得注意的是,如果對象通過調用hashCode()方法計算過對象的一致性哈希值,

那么它是不支持偏向鎖的,會直接進入到輕量級鎖狀態,

因為Hash是需要被保存的,而偏向鎖的Mark Word數據結構,無法保存Hash值;

如果對象已經是偏向鎖狀態,再去調用hashCode()方法,那么會直接將鎖升級為重量級鎖,并將哈希值存放在monitor(有預留位置保存)中。

關于java多線程高并發面試題總結(來了大廠面試Java崗)(11)

鎖消除和鎖粗化

鎖消除和鎖粗化都是在運行時的一些優化方案。

比如我們某段代碼雖然加了鎖,但是在運行時根本不可能出現各個線程之間資源爭奪的情況,

這種情況下,完全不需要任何加鎖機制,所以鎖會被消除。

鎖粗化則是我們代碼中頻繁地出現互斥同步操作,比如在一個循環內部加鎖,這樣明顯是非常消耗性能的,所以虛擬機一旦檢測到這種操作,會將整個同步范圍進行擴展。


JMM內存模型

注意這里提到的內存模型和我們在JVM中介紹的內存模型不在同一個層次,

JVM中的內存模型是虛擬機規范對整個內存區域的規劃,

而Java內存模型,是在JVM內存模型之上的抽象模型,具體實現依然是基于JVM內存模型實現的,以前的文章有介紹。

https://www.cnblogs.com/zwtblog/tag/,側邊欄支持搜索。

Java內存模型

我們在計算機組成原理中學習過,在我們的CPU中,一般都會有高速緩存,而它的出現,是為了解決內存的速度跟不上處理器的處理速度的問題。

所以CPU內部會添加一級或多級高速緩存來提高處理器的數據獲取效率,

但是這樣也會導致一個很明顯的問題,因為現在基本都是多核心處理器,每個處理器都有一個自己的高速緩存,那么又該怎么去保證每個處理器的高速緩存內容一致呢?

關于java多線程高并發面試題總結(來了大廠面試Java崗)(12)


為了解決緩存一致性的問題,需要各個處理器訪問緩存時都遵循一些協議,在讀寫時要根據協議來進行操作。

這類協議有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及Dragon Protocol等。

而Java也采用了類似的模型來實現支持多線程的內存模型:

關于java多線程高并發面試題總結(來了大廠面試Java崗)(13)


JMM(Java Memory Model)內存模型規定如下:

  • 所有的變量全部存儲在主內存(注意這里包括下面提到的變量,指的都是會出現競爭的變量,包括成員變量、靜態變量等,而局部變量這種屬于線程私有,不包括在內)
  • 每條線程有著自己的工作內存(可以類比CPU的高速緩存)線程對變量的所有操作,必須在工作內存中進行,不能直接操作主內存中的數據。
  • 不同線程之間的工作內存相互隔離,如果需要在線程之間傳遞內容,只能通過主內存完成,無法直接訪問對方的工作內存。

也就是說,每一條線程如果要操作主內存中的數據,那么得先拷貝到自己的工作內存中,并對工作內存中數據的副本進行操作,操作完成之后,也需要從工作副本中將結果拷貝回主內存中,具體的操作就是Save(保存)和Load(加載)操作。

那么各位肯定會好奇,這個內存模型,結合之前JVM所講的內容,具體是怎么實現的呢?

  • 主內存:對應堆中存放對象的實例的部分。
  • 工作內存:對應線程的虛擬機棧的部分區域,虛擬機可能會對這部分內存進行優化,
  • 將其放在CPU的寄存器或是高速緩存中。
  • 比如在訪問數組時,由于數組是一段連續的內存空間,
  • 所以可以將一部分連續空間放入到CPU高速緩存中,那么之后如果我們順序讀取這個數組,那么大概率會直接緩存命中。

前面我們提到,在CPU中可能會遇到緩存不一致的問題,而Java中,也會遇到,比如下面這種情況:

public class Main { private static int i = 0; public static void main(String[] args) throws InterruptedException { new Thread(() -> { for (int j = 0; j < 100000; j ) i ; System.out.println("線程1結束"); }).start(); new Thread(() -> { for (int j = 0; j < 100000; j ) i ; System.out.println("線程2結束"); }).start(); //等上面兩個線程結束 Thread.sleep(1000); System.out.println(i); }}

可以看到這里是兩個線程同時對變量i各自進行100000次自增操作,但是實際得到的結果并不是我們所期望的那樣。


那么為什么會這樣呢?

在之前學習了JVM之后,相信各位應該已經知道,自增操作實際上并不是由一條指令完成的(注意一定不要理解為一行代碼就是一個指令完成的):

關于java多線程高并發面試題總結(來了大廠面試Java崗)(14)


包括變量i的獲取、修改、保存,都是被拆分為一個一個的操作完成的,那么這個時候就有可能出現在修改完保存之前,另一條線程也保存了,但是當前線程是毫不知情的。

關于java多線程高并發面試題總結(來了大廠面試Java崗)(15)

所以說,在JavaSE階段講解這個問題的時候,是通過synchronized關鍵字添加同步代碼塊解決的,另外的解決方案(原子類)。

重排序

在編譯或執行時,為了優化程序的執行效率,編譯器或處理器常常會對指令進行重排序,有以下情況:

  1. 編譯器重排序:Java編譯器通過對Java代碼語義的理解,根據優化規則對代碼指令進行重排序。
  2. 機器指令級別的重排序:現代處理器很高級,能夠自主判斷和變更機器指令的執行順序。

令重排序能夠在不改變結果(單線程)的情況下,優化程序的運行效率,比如:

public static void main(String[] args) { int a = 10; int b = 20; System.out.println(a b);}

我們其實可以交換第一行和第二行:

public static void main(String[] args) { int b = 10; int a = 20; System.out.println(a b);}

即使發生交換,但是我們程序最后的運行結果是不會變的,當然這里只通過代碼的形式演示,實際上JVM在執行字節碼指令時也會進行優化,可能兩個指令并不會按照原有的順序進行。

雖然單線程下指令重排確實可以起到一定程度的優化作用,但是在多線程下,似乎會導致一些問題:

public class Main { private static int a = 0; private static int b = 0; public static void main(String[] args) { new Thread(() -> { if(b == 1) { if(a == 0) { System.out.println("A"); }else { System.out.println("B"); } } }).start(); new Thread(() -> { a = 1; b = 1; }).start(); }}

上面這段代碼,在正常情況下,按照我們的正常思維,是不可能輸出A的,因為只要b等于1,那么a肯定也是1才對,因為a是在b之前完成的賦值。

但是,如果進行了重排序,那么就有可能,a和b的賦值發生交換,b先被賦值為1,而恰巧這個時候,線程1開始判定b是不是1了,這時a還沒來得及被賦值為1,可能線程1就已經走到打印那里去了,所以,是有可能輸出A的。

volatile關鍵字

關鍵字volatile,開始之前我們先介紹三個詞語:

  • 原子性:其實之前講過很多次了,就是要做什么事情要么做完,要么就不做,不存在做一半的情況。
  • 可見性:指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
  • 有序性:即程序執行的順序按照代碼的先后順序執行。

我們之前說了,如果多線程訪問同一個變量,那么這個變量會被線程拷貝到自己的工作內存中進行操作,而不是直接對主內存中的變量本體進行操作。

下面這個操作看起來是一個有限循環,但是是無限的:

public class Main { private static int a = 0; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (a == 0); System.out.println("線程結束!"); }).start(); Thread.sleep(1000); System.out.println("正在修改a的值..."); a = 1; //很明顯,按照我們的邏輯來說,a的值被修改那么另一個線程將不再循環 }}

實際上這就是我們之前說的,雖然我們主線程中修改了a的值,但是另一個線程并不知道a的值發生了改變,所以循環中依然是使用舊值在進行判斷,因此,普通變量是不具有可見性的。


要解決這種問題,我們第一個想到的肯定是加鎖,同一時間只能有一個線程使用,這樣總行了吧,確實,這樣的話肯定是可以解決問題的:

public class Main { private static int a = 0; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (a == 0) { synchronized (Main.class){} } System.out.println("線程結束!"); }).start(); Thread.sleep(1000); System.out.println("正在修改a的值..."); synchronized (Main.class){ a = 1; } }}


但是,除了硬加一把鎖的方案,我們也可以使用volatile關鍵字來解決。

此關鍵字的第一個作用,就是保證變量的可見性。

當寫一個volatile變量時,JMM會把該線程本地內存中的變量強制刷新到主內存中去,并且這個寫會操作會導致其他線程中的volatile變量緩存無效。

這樣,另一個線程修改了這個變時,當前線程會立即得知,并將工作內存中的變量更新為最新的版本。

那么我們就來試試看:

public class Main { //添加volatile關鍵字 private static volatile int a = 0; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (a == 0); System.out.println("線程結束!"); }).start(); Thread.sleep(1000); System.out.println("正在修改a的值..."); a = 1; }}

結果還真的如我們所說的那樣,當a發生改變時,循環立即結束。

當然,雖然說volatile能夠保證可見性,但是不能保證原子性,要解決我們上面的i 的問題,可以使用加鎖來完成:

public class Main { private static volatile int a = 0; public static void main(String[] args) throws InterruptedException { Runnable r = () -> { for (int i = 0; i < 10000; i ) a ; System.out.println("任務完成!"); }; new Thread(r).start(); new Thread(r).start(); //等待線程執行完成 Thread.sleep(1000); System.out.println(a); }}


volatile不是能在改變變量的時候其他線程可見嗎,那為什么還是不能保證原子性呢?

還是那句話,自增操作是被瓜分為了多個步驟完成的,雖然保證了可見性,但是只要手速夠快,依然會出現兩個線程同時寫同一個值的問題(比如線程1剛剛將a的值更新為100,這時線程2可能也已經執行到更新a的值這條指令了,已經剎不住車了,所以依然會將a的值再更新為一次100)

那要是真的遇到這種情況,那么我們不可能都去寫個鎖吧?后面,我們會介紹原子類來專門解決這種問題。


最后一個功能就是volatile會禁止指令重排,也就是說,如果我們操作的是一個volatile變量,它將不會出現重排序的情況.

那么它是怎么解決的重排序問題呢?

若用volatile修飾共享變量,在編譯時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序

內存屏障(Memory Barrier)又稱內存柵欄,是一個CPU指令,它的作用有兩個:

保證特定操作的順序保證某些變量的內存可見性(volatile的內存可見性,其實就是依靠這個實現的)

由于編譯器和處理器都能執行指令重排的優化,

如果在指令間插入一條Memory Barrier則會告訴編譯器和CPU,

不管什么指令都不能和這條Memory Barrier指令重排序。

屏障類型指令示例說明LoadLoadLoad1;LoadLoad;Load2保證Load1的讀取操作在Load2及后續讀取操作之前執行StoreStoreStore1;StoreStore;Store2在Store2及其后的寫操作執行前,保證Store1的寫操作已刷新到主內存LoadStoreLoad1;LoadStore;Store2在Store2及其后的寫操作執行前,保證Load1的讀操作已讀取結束StoreLoadStore1;StoreLoad;Load2保證load1的寫操作已刷新到主內存之后,load2及其后的讀操作才能執行

所以volatile能夠保證,之前的指令一定全部執行,之后的指令一定都沒有執行,并且前面語句的結果對后面的語句可見。


最后我們來總結一下volatile關鍵字的三個特性:

  • 保證可見性
  • 不保證原子性
  • 防止指令重排

在之后我們的設計模式系列視頻中,還會講解單例模式下volatile的運用。

happens-before原則

經過我們前面的講解,相信各位已經了解了JMM內存模型以及重排序等機制帶來的優點和缺點.

綜上,JMM提出了happens-before(先行發生)原則,定義一些禁止編譯優化的場景,來向各位程序員做一些保證,只要我們是按照原則進行編程,那么就能夠保持并發編程的正確性。具體如下:

  • 程序次序規則:同一個線程中,按照程序的順序,前面的操作happens-before后續的任何操作。
    • 同一個線程內,代碼的執行結果是有序的。
    • 其實就是,可能會發生指令重排,但是保證代碼的執行結果一定是和按照順序執行得到的一致,
    • 程序前面對某一個變量的修改一定對后續操作可見的,不可能會出現前面才把a修改為1,
    • 接著讀a居然是修改前的結果,這也是程序運行最基本的要求。
  • 監視器鎖規則:對一個鎖的解鎖操作,happens-before后續對這個鎖的加鎖操作。
    • 就是無論是在單線程環境還是多線程環境,
    • 對于同一個鎖來說,一個線程對這個鎖解鎖之后,另一個線程獲取了這個鎖都能看到前一個線程的操作結果。
    • 比如前一個線程將變量x的值修改為了12并解鎖,
    • 之后另一個線程拿到了這把鎖,對之前線程的操作是可見的,可以得到x是前一個線程修改后的結果12(所以synchronized是有happens-before規則的)
  • volatile變量規則:對一個volatile變量的寫操作happens-before后續對這個變量的讀操作。
    • 就是如果一個線程先去寫一個volatile變量,緊接著另一個線程去讀這個變量,那么這個寫操作的結果一定對讀的這個變量的線程可見。
  • 線程啟動規則:主線程A啟動線程B,線程B中可以看到主線程啟動B之前的操作。
    • 在主線程A執行過程中,啟動子線程B,那么線程A在啟動子線程B之前對共享變量的修改結果對線程B可見。
  • 線程加入規則:如果線程A執行操作join()線程B并成功返回,那么線程B中的任意操作happens-before線程Ajoin()操作成功返回。
  • 傳遞性規則:如果A happens-before B,B happens-before C,那么A happens-before C。

那么我們來從happens-before原則的角度,來解釋一下下面的程序結果:

public class Main { private static int a = 0; private static int b = 0; public static void main(String[] args) { a = 10; b = a 1; new Thread(() -> { if(b > 10) System.out.println(a); }).start(); }}

首先我們定義以上出現的操作:

  • A:將變量a的值修改為10
  • B:將變量b的值修改為a 1
  • C:主線程啟動了一個新的線程,并在新的線程中獲取b,進行判斷,如果為true那么就打印a

首先我們來分析,由于是同一個線程,并且B是一個賦值操作且讀取了A,

那么按照程序次序規則,A happens-before B,接著在B之后,馬上執行了C,按照線程啟動規則,

在新的線程啟動之前,當前線程之前的所有操作對新的線程是可見的,

所以 B happens-before C,

最后根據傳遞性規則,由于A happens-before B,B happens-before C,所以A happens-before C,因此在新的線程中會輸出a修改后的結果10。

原文鏈接:https://www.cnblogs.com/zwtblog/p/16107798.html

作者:ML李嘉圖

如果覺得本文對你有幫助,可以轉發關注支持一下

TAGS標簽:  關于  java  線程  發面  試題  關于java多線程高

歡迎分享轉載→http://www.avcorse.com/read-220787.html

Copyright ? 2024 有趣生活 All Rights Reserve吉ICP備19000289號-5 TXT地圖HTML地圖XML地圖