jvm(12)-java内存模型与线程
生活随笔
收集整理的這篇文章主要介紹了
jvm(12)-java内存模型与线程
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
【0】README
0.1)本文部分文字描述轉自“深入理解jvm”,旨在學習“java內存模型與線程” 的基礎知識;
【1】概述 1)并發處理的廣泛應用是使得 Amdahl 定律代替摩爾定律稱為計算機性能發展源動力的根本原因; 2)Amdahl 定律:該定律通過系統中并行化與串行化的比重來描述多處理器系統能獲得的運算加速能力; 3)摩爾定律:該定律用于描述處理器晶體管數量與運行效率間的發展關系; Conclusion)這兩個定律的更替代表了近年來硬件發展從追求處理器頻率到追求多核心并行處理的發展過程;
【2】硬件的效率與一致性 1)高速緩存(干貨——引入高速緩存)
【3】java內存模型 0)intro to java內存模型:java虛擬機規范試圖定義一種java內存模型(java memory model)來屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓java程序在各種平臺下都能達到一致的內存訪問效果; 【3.1】主內存與工作內存 1)java內存模型的主要目標:定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中讀取變量這樣的底層細節; 2)java內存模型規定了:所有的變量都存儲在主內存中。
【3.4】對于long 和 double 型變量的特殊規則 1)java內存模型要求lock, unlock, read, load, assign, use,store,write這8個操作都具有原子性:?但對于64位的數據類型(long和double),在模型中特別定義了一條相對寬松的規定:允許虛擬機將沒有被 volatile修飾的64位數據的讀寫操作劃分為兩次32位的操作來進行,即允許虛擬機實現選擇可以不保證64位數據類型的load, store,read和write這4個操作的原子性,這點就是所謂的 long 和double 的非原子性協定; 2)這項寬松的規定所導致的problem:如果有多個線程共享一個并未聲明為 volatile的long 或 double類型的變量,并且同時對它們進行讀取和修改操作,那么某些線程可能會讀取到一個既非原值,也不是其他線程修改值的代表了半個變量的數值; 3)不過這種讀取到的“半個變量”的case非常罕見:因為java內存模型雖然允許虛擬機不把long 和 double 變量的讀寫實現成原子操作,但允許虛擬機選擇把 這些操作實現為具有原子性的操作,而且還強烈建議虛擬機這樣實現;(干貨——不過這種讀取到的“半個變量”的case非常罕見)
【3.5】原子性,可見性與有序性 0)intro: java內存模型是圍繞著在并發過程中如何處理原子性, 可見性和有序性這3個特征來建立的; 1)原子性:由于java內存模型來直接保證的原子性變量操作包括 read,load,assign,use,store和write,我們大致認為基本數據類型的訪問讀寫數據是具備原子性的。
【1】概述 1)并發處理的廣泛應用是使得 Amdahl 定律代替摩爾定律稱為計算機性能發展源動力的根本原因; 2)Amdahl 定律:該定律通過系統中并行化與串行化的比重來描述多處理器系統能獲得的運算加速能力; 3)摩爾定律:該定律用于描述處理器晶體管數量與運行效率間的發展關系; Conclusion)這兩個定律的更替代表了近年來硬件發展從追求處理器頻率到追求多核心并行處理的發展過程;
【2】硬件的效率與一致性 1)高速緩存(干貨——引入高速緩存)
- 1.1)problem:由于計算機的存儲設備與處理器的運算速率有幾個數量級的差距;
- 1.2)solution:引入一層讀寫速度盡可能接近處理器速度的高速緩存(cache) 來作為內存與處理器間的緩沖: 將運算需要使用到的數據復制到緩存中,讓運算能快速進行,當運算結束后再從緩沖同步回內存中,這樣處理器就無須等待緩慢的內存讀寫了;
- 2.1)問題描述(problem): 當多個處理器的運算任務都涉及到同一塊內存區域時,將可能導致各自的緩存數據不一致,那同步到內存時以誰的數據為準呢?
- 2.2)solution: 需要各個處理器遵循一些協議,在讀寫時要根據協議來進行操作,這類協議有 MSI, MESI,等。
- 2.3)內存模型: 可以理解為 在特定的操作協議下,對特定的內存或高速緩存進行讀寫訪問的過程抽象;(干貨——java內存模型定義)
【3】java內存模型 0)intro to java內存模型:java虛擬機規范試圖定義一種java內存模型(java memory model)來屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓java程序在各種平臺下都能達到一致的內存訪問效果; 【3.1】主內存與工作內存 1)java內存模型的主要目標:定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中讀取變量這樣的底層細節; 2)java內存模型規定了:所有的變量都存儲在主內存中。
- 2.1)每條線程還有自己的工作內存: 線程的工作內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作(讀取,賦值)都必須在工作內存中進行,而不能直接讀寫內存中的變量。不同的線程之間也無法直接訪問對方工作內存中的變量;(干貨——每條線程還有自己的工作內存,工作內存定義)
- 2.2)線程間變量值的傳遞均需要通過主內存來完成:線程、內存、工作內存三者關系如下所示:
- 3.1)如果硬要扯上關系,則:主內存主要對應于java堆中的對象實例數據部分,而工作內存則對應于虛擬機棧中的部分區域;
- 3.2)更低層次上說:主內存就直接對應于物理硬件的內存,而為了獲得更好的運行速度,虛擬機可能會讓工作內存優先存儲于寄存器和高速緩存中,因為程序運行時主要訪問讀寫的是工作內存;
- o1)lock(鎖定):作用于主內存的變量,它把一個變量標識為一條線程獨占的狀態;
- o2)unlock(解鎖):作用于主內存的變量,它把一個處于鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定;
- o3)read(讀取):作用于主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨后的load 動作使用;
- o4)load(載入):作用于工作內存的變量, 它把 read 操作從主內存中得到的變量放入工作內存的變量副本中;
- o5)use(使用):作用于工作內存的變量,它把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作;
- o6)assign(賦值):作用于工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作;
- o7)store(存儲):作用于工作內存的變量, 它把工作內存中一個變量的值傳送到主內存中,以便隨后的write操作使用;
- o8)write(寫入):作用于主內存的變量, 它把store操作從工作內存中得到的變量的值放入主內存的變量中;
- 2.1)把變量從主內存復制到工作內存:順序執行read和load操作(目的地是工作內存);
- 2.2)把變量從工作內存同步回主內存:順序執行store和write操作(目的地是主內存);
- Attention)java內存模型只要求上述兩個操作必須按順序執行,沒有保證是連續執行;即read和 load 之間,store和 write之間可以插入其他指令;(干貨——java內存模型只要求上述兩個操作必須按順序執行,沒有保證是連續執行,它們之間還可以插入其他指令)
- r1)不允許read和 load,store 和 write操作之一單獨出現,即不允許一個變量從主內存讀取了但工作內存不接受,或者從工作內存發起回寫了但主內存不接受的情況出現;
- r2)不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變了之后必須把該變化同步回主內存;
- r3)不允許一個線程無原因地(沒有發生過任何 assign操作)把數據從線程的工作內存同步回主內存中;
- r4)一個新變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化的變量,換句話說,對一個變量實施 use,store操作前,必須先執行過 assign 和 load 操作;
- r5)一個變量在同一個時刻只允許一個線程對其進行lock 操作,但lock操作可以被同一條線程重復執行多次,多次執行 lock后,只有執行相同次數的unlock 操作,變量才會被解鎖;
- r6)如果對一個變量執行lock 操作,那將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行 load 或 assign 操作初始化變量的值;
- r7)如果一個變量事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許去 unlock 一個被其他線程鎖定住的變量;
- r8)對一個變量執行unlock 變量前,必須先把此變量同步回主內存中(執行store, write操作);
- Conclusion) 這8種內存訪問操作以及上述規則限定,再加上稍后介紹的對 volatile 的一些特殊規定,就已經完全確定了java 程序中哪些內存訪問操作在并發下是安全的;
- c1)保證此變量對所有線程的可見性,這里的可見性指:當一條線程修改了這個變量的值,新值對于其他線程來說是可以立即得知的。而普通變量做不到這一點,其在線程間傳遞需要通過主內存來完成;(干貨——這就是為什么會出現數據的臟讀)
- c2)對于volatile變量的可見性,有一些誤解: volatile變量對所有線程都是可見的,對volatile變量所有的寫操作都能立刻反應到其他線程中,即,volatile變量在各個線程中是一致的,所有基于 volatile變量的運算在并發下是安全的。上述語句中的錯誤在于并不能得出“基于 volatile變量的運算在并發下是安全的”這個結論。
- 2.1)看個荔枝:
- public class VolatileTest {public static volatile int race = 0;public static void increase() {race++;}public static final int THREADS_COUNT = 20;public static void main(String[] args) {Thread[] threads = new Thread[THREADS_COUNT];for (int i = 0; i < threads.length; i++) {threads[i] = new Thread(new Runnable() {@Overridepublic void run() {for (int j = 0; j < 10000; j++) {increase();}}});threads[i].start();}// 等待所有累計線程都endingwhile(Thread.activeCount() > 1) {Thread.yield();}System.out.println(race);} }
- 對以上執行結果的分析(Analysis):
- A1)以上代碼的正確輸出結果是20000, 而執行的結果每次都不一樣,且都小于20000;
- A2)用javap 反編譯命令得到如下字節碼,發現increase()方法對應4條字節碼指令(return指令不算):
- 對以上字節碼的分析(Analysis):
- A1)當 getstatic指令 把 race 的值取到操作棧頂時,volatile關鍵字保證了 race的值在此時是正確的,但是在執行 iconst_1, iadd 這些指令的時候,其他線程可能已經把race 的值加大了,而在操作棧頂的值就變成了過期的數據,所以 putstatic 指令執行后就可能把較小的race 值同步回主內存中;
- A2)客觀上說,在這里使用 字節碼來分析并發問題,不是很嚴謹。因為即使編譯出來只有一條字節碼指令,也不意味著執行這條指令就是一個原子操作。一條字節碼指令也可能會轉化成若干條本地機器碼指令,此處使用 -XX:+PrintAssembly 參數輸出反匯編來分析會更加嚴謹;(干貨——因為即使編譯出來只有一條字節碼指令,也不意味著執行這條指令就是一個原子操作)
- scene1)運算結果并不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值;
- scene2)變量不需要與其他的狀態變量共同參與不變約束;
- 如果定義initialized變量沒有使用volatile修飾:就可能會由于指令重排序的優化,導致位于線程A 中最后一句代碼“initialized=true”被提前執行;這樣在線程B中使用配置信息的代碼就可能出現錯誤,而volatile關鍵字則可以避免此類情況的發生;
【3.4】對于long 和 double 型變量的特殊規則 1)java內存模型要求lock, unlock, read, load, assign, use,store,write這8個操作都具有原子性:?但對于64位的數據類型(long和double),在模型中特別定義了一條相對寬松的規定:允許虛擬機將沒有被 volatile修飾的64位數據的讀寫操作劃分為兩次32位的操作來進行,即允許虛擬機實現選擇可以不保證64位數據類型的load, store,read和write這4個操作的原子性,這點就是所謂的 long 和double 的非原子性協定; 2)這項寬松的規定所導致的problem:如果有多個線程共享一個并未聲明為 volatile的long 或 double類型的變量,并且同時對它們進行讀取和修改操作,那么某些線程可能會讀取到一個既非原值,也不是其他線程修改值的代表了半個變量的數值; 3)不過這種讀取到的“半個變量”的case非常罕見:因為java內存模型雖然允許虛擬機不把long 和 double 變量的讀寫實現成原子操作,但允許虛擬機選擇把 這些操作實現為具有原子性的操作,而且還強烈建議虛擬機這樣實現;(干貨——不過這種讀取到的“半個變量”的case非常罕見)
【3.5】原子性,可見性與有序性 0)intro: java內存模型是圍繞著在并發過程中如何處理原子性, 可見性和有序性這3個特征來建立的; 1)原子性:由于java內存模型來直接保證的原子性變量操作包括 read,load,assign,use,store和write,我們大致認為基本數據類型的訪問讀寫數據是具備原子性的。
- 1.1)同步塊——synchronized關鍵字:如果應用場景需要一個更大范圍的原子性保證,java內存模型還提供了lock 和 unlock 操作來滿足這些需求,盡管虛擬機沒有把lock 和 unlock 操作直接開放給用戶使用,但是卻提供了更高層次的字節碼指令 monitorenter 和 monitorexit 來隱式地使用這兩個操作;
- 1.2)這兩個字節碼指令反映到java代碼中就是同步塊——synchronized關鍵字:因此在synchronized塊之間的操作也具備原子性;
- 2.1)java內存模型是通過在變量修改后將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存作為傳遞媒介的方式來實現可見性的,無論是普通變量還是volatile變量都是如此;
- 2.2)普通變量與 volatile變量的區別是:volatile的特殊規則保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新;所以volatile保證了多線程操作時變量的可見性,普通變量則不能保證這一點;
- 2.3)java還有兩個關鍵字實現可見性: synchronized 和 final;
- 2.3.1)同步塊的可見性: 是由對一個變量執行unlock 操作前,必須先把此變量同步回主內存中;
- 2.3.2)而final關鍵字的可見性:被final修飾的字段在構造器中一旦初始化完成,并且構造器沒有把this 的引用傳遞出去,那在其他線程中就能看見final 字段的值。
- 2.3.3)看個荔枝:
- // final 可見性測試 public class FinalVisibilityTest {public static final int i;public final int j;static {i = 0;// do sth}{// 也可以選擇在構造函數中初始化j = 0;// do sth} }
- 對以上代碼的分析(Analysis):變量i 和 j 都具備可見性,它們無須同步就能被其他線程正確訪問;
- 3.1)java程序中天然的有序性總結為一句話:如果在本線程內觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。前半句是指: 線程內表現為串行的語義,后半句是指:指令重排序現象和工作內存與主內存同步延遲現象;
- 3.2)volatile和 synchronized關鍵字保證了線程間操作的有序性:volatile關鍵字本身就包含了禁止指令重排序的語義,而synchronized則是由 一個變量在同一時刻只允許一條線程對其進行lock 操作這條規則獲得的,這條規則決定了持有同一個鎖的兩個同步塊只能串行地進入;
- 3.1)程序次序規則:在一個線程內,按照程序代碼順序,書寫在前面的操作先行發生于書寫在后面的操作;準確地說,應該是控制流順序;
- 3.2)管程鎖定規則:一個unlock操作先行發生于后面對同一個鎖的lock操作;這里必須強調的是同一個鎖,而后面是指時間上的先后順序;
- 3.3)volatile變量規則:對一個volatile變量的寫操作先行發生于后面對這個變量的讀操作,這里的后面是指時間上的先后順序;
- 3.4)線程啟動規則:Thread對象的start() 方法先行發生于此線程的每一個動作;
- 3.5)線程終止規則:線程中的所有操作都先行發生于對此線程的終止檢測,可以通過Thread.join() 方法結束,Thread.isAlive() 的返回值等手段檢測到線程已經終止運行;
- 3.6)線程中斷規則:對線程interrupt() 方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生,可以通過 Thread.interrrupted() 方法檢測到是否有中斷發生;
- 3.7)對象終結規則:一個對象的初始化完成先行發生于它的finalize() 方法的開始;
- 3.8)傳遞性:如果操作A 先行發生于操作B, 操作B 先行發生于操作C,那就可以得出操作A 先行發生于 操作C的結論;
- A1)problem:假設線程A 先調用了 setValue(1), 之后線程B 調用了同一個對象的getValue() ,那么線程B 收到的value是什么?
- A2)可以判定:盡管線程A在操作時間上先于線程B, 但是無法確定線程B 中“getValue()” 方法的返回結果,換句話說,這里面的操作不是線程安全的;
- A3)solution:我們至少有兩種簡單的解決方案:
- solution1)要么把getter 和 ?setter方法都定義為 synchronized方法,這樣就可以套用管程鎖定規則;
- solution2)要么把value定義為 volatile變量,由于setter方法對value的修改不依賴于 value的原值,滿足volatile關鍵字使用場景,這樣就可以套用volatile變量規則來實現先行發生關系;
- A4)得出結論:一個操作時間上的先發生 不代表這個操作會是先行發生,那如果一個操作先行發生是否就能推導出這個操作必定是 時間上的先行發生呢? (顯然推導不出來)。一個典型的荔枝就是多次提到的“指令重排序”。
- A4.1)看個荔枝:
- // 以下操作在同一個線程中執行 int i = 1; int j = 2;
- 對上述代碼的分析(Analysis): 根據程序次序規則, int i = 1 的操作先行發生于 int j =2,但 int j = 2 完全可能先被處理器執行,這并不影響先行發生原則的正確性;
- Conclusion)以上兩個實例得出結論:時間先后順序與先行發生原則之間基本沒有太大的關系,所以我們衡量并發安全問題的時候不要受到時間順序的干擾,一切必須以先行發生原則為準;
- 0.1)線程是比進程更輕量級的調度執行單位: 線程的引入,可以把一個進程的資源分配和執行調度分開,各個線程既可以共享進程資源(內存地址,文件IO等),又可以獨立調度(線程是CPU 調度的基本單位);
- 0.2)線程實現的3種方式:使用內核線程實現,使用用戶線程實現,使用用戶線程加輕量級進程混合實現(干貨——線程實現的3種方式:使用內核線程實現+使用用戶線程實現+使用用戶線程和輕量級進程混合實現)
- 1.1)內核線程(KLT,Kernel-Level Thread):就是直接由操作系統內核(下稱內核)支持的線程,這種線程由內核來完成線程切換,內核通過操縱調度器對線程進行調度,并負責將線程的任務映射到各個處理器上。(干貨——內核線程和輕量級進程的定義)
- 1.2)程序一般不會直接去使用內核線程,而是去使用內核線程的一種高級接口——輕量級進程(Light Weight Process, LWP):輕量級進程就是我們通常意義上講的線程,由于每個輕量級線程都由一個內核線程支持,因此只有先支持內核線程,才能有輕量級進程。這種輕量級進程與內核線程間1:1 的關系稱為一對一的線程模型,如下圖所示:(干貨——引入輕量級進程)
- 1.3)輕量級進程有局限性:
- 1.3.1)首先:由于是基于內核線程實現的,所以各種線程操作,如創建,析構及同步,都需要進行系統調用, 而系統調用的代價相對較高,需要在用戶態和內核態中來回切換;
- 1.3.2)其次:每個輕量級進程都需要有一個內存線程的支持,因此輕量級進程要消耗一定的內核資源(如內核線程的棧空間),因此一個系統支持輕量級進程的數量是有限的;
- 3.1)在該實現方式下,既存在用戶線程,也存在輕量級進程;
- 3.2)用戶線程還是完全建立在用戶空間中:因此用戶線程的創建,切換,析構等操作依然廉價,并且可以支持大規模的用戶線程并發;
- 3.3)操作系統提供支持的輕量級進程則作為用戶線程和內核線程之間的橋梁:這樣可以使用內核提供的線程調度功能及處理器映射,并且用戶線程的系統調用要通過輕量級線程來完成,大大降低了整個進程被完全阻塞 的風險;(干貨——操作系統提供支持的輕量級進程則作為用戶線程和內核線程之間的橋梁)
- 3.4)在這種混合模式中:用戶線程與輕量級進程的數量比是不定的,即為 N:M 的關系,如下圖所示, 這種就是多對多的線程模型;
- 2.1)其好處是:實現簡單,而且由于線程要把自己的事情干完后才會進行線程切換,切換操作讀線程自己是可知的,所以沒有什么線程同步的問題;
- 2.2)其壞處是:線程執行時間不可控制,甚至如果一個線程編寫有問題,一直不告訴系統進行線程切換,那么程序就會一直阻塞在那里;
- 3.1)java使用的方式就是 搶占式線程調度方式;
- 3.2)雖然java 線程調度是系統自動完成的: 但我們還是可以建議系統給某些線程多分配一點執行時間,另外一些線程則可以少分配一點——這項操作可以通過設置線程優先級來完成;(干貨——設置java 線程優先級)
- 3.3.)不過線程優先級并不是太靠譜:因為java的線程是通過映射到系統的原生線程上來實現的,所以線程調度最終還是取決于 操作系統;雖然現在很多os 都提供了線程優先級,但不見得和 能與 java線程的優先級一一對應;如 Solaris中有 2^32 種優先級,而windows只有7種 ;(干貨——java的線程優先級并不是太靠譜)
- 3.4)下表顯示了 java線程優先級 與 windows 線程優先級之間的對應關系:
- C1)上文說到的“java線程優先級并不是太靠譜”,不僅僅是在說一些平臺上不同的優先級實際會變得相同這一點,還有其他case 讓我們不能太依賴優先級:優先級可能會被系統自行改變。(干貨——優先級可能會被系統自行改變)
- C2)如,在windows 中存在一個稱為 “優先級推進器”的功能,作用是 當系統發現一個線程執行得特別勤奮的話,可能會越過線程優先級去為它分配執行時間;
- 1.1)新建(New):創建后尚未啟動的線程處于這個狀態;
- 1.2)運行(Runnable):Runable包括了os 線程狀態中的 Running 和 Ready,也就是處于 此狀態的線程有可能正在執行,也有可能正在等待著CPU 為它分配執行時間;
- 1.3)無限期等待(Waiting):處于這種狀態的線程不會被分配CPU執行時間,它們要等待被其他線程顯式的喚醒。以下方法會讓線程陷入無限期的等待狀態(methods):
- m1)沒有設置Timeout參數的Object.wait()方法;
- m2)沒有設置Timeout參數的 Thread.join() 方法;
- m3)LockSupport.park() 方法;
- 1.4)限期等待(Timed Waiting):處于這種狀態的線程也不會被分配CPU 執行時間,不過無需等待被其他線程顯式喚醒,在一定時間之后,它們會由系統自動喚醒。以下方法會讓線程進入限期等待狀態(methods):
- m1)Thread.sleep() 方法;
- m2)設置了Timeout參數的Object.wait()方法;
- m3)設置了Timeout參數的 Thread.join() 方法;
- m4)LockSupport.parkNanos() 方法;
- m5)LockSupport.parkUntil() 方法;
- 1.5)阻塞(Blocked):線程被阻塞了,?阻塞狀態與等待狀態的區別是:阻塞狀態在等待著獲取到一個排他鎖,這個事件將在另外一個線程放棄這個鎖的時候發生;而等待狀態則是在等待一段時間,或者喚醒動作的發生。在程序等待進入同步區域的時候, 線程將進入這種狀態;
- 1.6)結束(Terminated):已經終止線程的線程狀態,線程已經結束執行;
總結
以上是生活随笔為你收集整理的jvm(12)-java内存模型与线程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ddos高防ip原理(ddos防ip原理
- 下一篇: jvm(13)-线程安全与锁优化