JMM
計算機物理內(nèi)存模型
java JMM
- Java內(nèi)存模型規(guī)定了所有的變量都存儲在主內(nèi)存(Main Memory)中(此處的主內(nèi)存與 介紹物理硬件時的主內(nèi)存名字一樣等浊,兩者也可以互相類比腮郊,但此處僅是虛擬機內(nèi)存的一部 分)。每條線程還有自己的工作內(nèi)存(Working Memory筹燕,可與前面講的處理器高速緩存類 比)轧飞,線程的工作內(nèi)存中保存了被該線程使用到的變量的主內(nèi)存副本拷貝。
- 線程對變量的 所有操作(讀取撒踪、賦值等)都必須在工作內(nèi)存中進(jìn)行过咬,而不能直接讀寫主內(nèi)存中的變量。 不同的線程之間也無法直接訪問對方工作內(nèi)存中的變量制妄,線程間變量值的傳遞均需要通過主 內(nèi)存來完成
內(nèi)存間交互操作
- 關(guān)于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議掸绞,即一個變量如何從主內(nèi)存拷貝到工作內(nèi) 存、如何從工作內(nèi)存同步回主內(nèi)存之類的實現(xiàn)細(xì)節(jié)耕捞,Java內(nèi)存模型中定義了以下8種操作來 完成衔掸,虛擬機實現(xiàn)時必須保證下面提及的每一種操作都是原子的
- lock(鎖定):作用于主內(nèi)存的變量,它把一個變量標(biāo)識為一條線程獨占的狀態(tài)俺抽。
- unlock(解鎖):作用于主內(nèi)存的變量敞映,它把一個處于鎖定狀態(tài)的變量釋放出來,釋放 后的變量才可以被其他線程鎖定磷斧。
- read(讀日裨浮):作用于主內(nèi)存的變量捷犹,它把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi) 存中,以便隨后的load動作使用冕末。
- load(載入):作用于工作內(nèi)存的變量萍歉,它把read操作從主內(nèi)存中得到的變量值放入工 作內(nèi)存的變量副本中。
- use(使用):作用于工作內(nèi)存的變量档桃,它把工作內(nèi)存中一個變量的值傳遞給執(zhí)行引 擎翠桦,每當(dāng)虛擬機遇到一個需要使用到變量的值的字節(jié)碼指令時將會執(zhí)行這個操作。
- assign(賦值):作用于工作內(nèi)存的變量胳蛮,它把一個從執(zhí)行引擎接收到的值賦給工作內(nèi) 存的變量销凑,每當(dāng)虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
- store(存儲):作用于工作內(nèi)存的變量仅炊,它把工作內(nèi)存中一個變量的值傳送到主內(nèi)存 中斗幼,以便隨后的write操作使用。
- write(寫入):作用于主內(nèi)存的變量抚垄,它把store操作從工作內(nèi)存中得到的變量的值放入 主內(nèi)存的變量中
對于volatile型變量的特殊規(guī)則
-
關(guān)鍵字volatile可以說是Java虛擬機提供的最輕量級的同步機制蜕窿,
-
是保證此變量對所有線程的可 見性,這里的“可見性”是指當(dāng)一條線程修改了這個變量的值呆馁,新值對于其他線程來說是可以 立即得知的
-
是Java里面的運算并非 原子操作桐经,導(dǎo)致volatile變量的運算在并發(fā)下一樣是不安全的
/** * * volatile在并發(fā)環(huán)境下并非原子操作 */ public class VolatileTest { public static volatile int race = 0; public static void increase() { race++; } public static final int THREAD_COUNT = 20; public static void main(String[] args) { for (int i = 0; i < THREAD_COUNT; i++) { new Thread(() -> { for (int j = 0; j < 10000; j++) { increase(); } }).start(); } //等待所有累加線程都結(jié)束,如果還有線程在運行,主線程就讓出cpu資源 while (Thread.activeCount() > 2) {//由于idea原因此處不能為一 Thread.yield(); } System.out.println(race); } /* public static void increase(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=0, args_size=0 0: getstatic #2 // Field race:I 3: iconst_1 4: iadd 5: putstatic #2 // Field race:I 8: return 當(dāng)getstatic指令把race的值取到操作棧頂時浙滤,volatile關(guān)鍵字保證了race的值在此 時是正確的阴挣,但是在執(zhí)行iconst_1、iadd這些指令的時候纺腊, 其他線程可能已經(jīng)把race的值加大 了畔咧,而在操作棧頂?shù)闹稻妥兂闪诉^期的數(shù)據(jù),所以putstatic指令執(zhí)行后就可能把較小的race值 同步回主內(nèi)存之中 */ }
-
-
-
禁止指令重排序優(yōu)化
/** * * volatile靜止指令重排序演示代碼 */ public class Singleton { private volatile static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } public static void main(String[] args) { Singleton.getInstance(); } } /* instance = new Singleton(); 這段代碼可以分為如下的三個步驟: memory = allocate(); // 1:分配對象的內(nèi)存空間 ctorInstance(memory); // 2:初始化對象 instance = memory; // 3:設(shè)置instance指向剛分配的內(nèi)存地址 我們知道揖膜,編輯器和處理器會進(jìn)行代碼優(yōu)化誓沸,而其中重要的一點是會將指令進(jìn)行重排序。 上邊的代碼經(jīng)過重排序后可能會變?yōu)椋?memory = allocate(); // 1:分配對象的內(nèi)存空間 instance = memory; // 3:設(shè)置instance指向剛分配的內(nèi)存地址 // 注意:此時對象尚未初始化 ctorInstance(memory); // 2:初始化對象 代碼對應(yīng)的匯編的執(zhí)行過程 * 0x01a3de0f:mov $0x3375cdb0壹粟,%esi ;……beb0cd75 33 ;{oop('Singleton')} 0x01a3de14:mov %eax拜隧,0x150(%esi) ;……89865001 0000 0x01a3de1a:shr $0x9,%esi ;……c1ee09 0x01a3de1d:movb $0x0趁仙,0x1104800(%esi) ;……c6860048 100100 0x01a3de24:lock addl$0x0洪添,(%esp) ;……f0830424 00 ;*put static instance ;- Singleton:getInstance@24 生成匯編碼是lock addl $0x0, (%rsp), 在寫操作(put static instance)之前使用了lock前綴,鎖住了總線和對應(yīng)的地址幸撕,這樣其他的CPU寫和讀都要等待鎖的釋放薇组。 當(dāng)寫完成后,釋放鎖坐儿,把緩存刷新到主內(nèi)存律胀。 加了 volatile之后宋光,volatile在最后加了lock前綴,把前面的步驟鎖住了炭菌,這樣如果你前面的步驟沒做完是無法執(zhí)行最后一步刷新到內(nèi)存的罪佳, 換句話說只要執(zhí)行到最后一步lock,必定前面的操作都完成了黑低。那么即使我們完成前面兩步或者三步了赘艳,還沒執(zhí)行最后一步lock,或者前面一步執(zhí)行了就切換線程2了克握, 線程B在判斷的時候也會判斷實例為空蕾管,進(jìn)而繼續(xù)進(jìn)來由線程B完成后面的所有操作。當(dāng)寫完成后菩暗,釋放鎖掰曾,把緩存刷新到主內(nèi)存。 ———————————————— 版權(quán)聲明:本文為CSDN博主「夏洛克卷」的原創(chuàng)文章停团,遵循 CC 4.0 BY-SA 版權(quán)協(xié)議旷坦,轉(zhuǎn)載請附上原文出處鏈接及本聲明。 原文鏈接:https://blog.csdn.net/zx48822821/article/details/86589753 * */
Java與Thread
-
線程的實現(xiàn)
實現(xiàn)線程主要有3種方式:使用內(nèi)核線程實現(xiàn)佑稠、使用用戶線程實現(xiàn)和使用用戶線程加輕 量級進(jìn)程混合實現(xiàn)秒梅。
內(nèi)核線程(Kernel-Level Thread,KLT)
- 用戶線程(User Thread,UT)使用用戶線程的優(yōu)勢在于不需要系統(tǒng)內(nèi)核支援,劣勢也在于沒有系統(tǒng)內(nèi)核的支援舌胶,所有 的線程操作都需要用戶程序自己處理
- 使用用戶線程加輕量級進(jìn)程混合實現(xiàn)
-
Java線程的實現(xiàn)
- 在目前的JDK版 本中捆蜀,操作系統(tǒng)支持怎樣的線程模型,在很大程度上決定了Java虛擬機的線程是怎樣映射 的辆琅,這點在不同的平臺上沒有辦法達(dá)成一致漱办,虛擬機規(guī)范中也并未限定Java線程需要使用哪 種線程模型來實現(xiàn)。
Java線程調(diào)度
線程調(diào)度是指系統(tǒng)為線程分配處理器使用權(quán)的過程,主要調(diào)度方式有兩種婉烟,分別是協(xié)同 式線程調(diào)度(Cooperative Threads-Scheduling)和搶占式線程調(diào)度(Preemptive ThreadsScheduling)。
協(xié)同 式線程調(diào)度暇屋,線程的執(zhí)行時間由線程本身來控制似袁,線程把自己的 工作執(zhí)行完了之后,要主動通知系統(tǒng)切換到另外一個線程上咐刨。
使用搶占式調(diào)度的多線程系統(tǒng)昙衅,那么每個線程將由系統(tǒng)來分配執(zhí)行時間,線程的切 換不由線程本身來決定
Java使用的線程調(diào)度方式就是搶 占式調(diào)度
線程狀態(tài)轉(zhuǎn)換
線程安全與鎖優(yōu)化
為了更加深入地理解線程安全定鸟,在這里我們可以不把線程安全當(dāng)做一個非真即假的二元 排他選項來看待而涉,按照線程安全的“安全程度”由強至弱來排序,我們[1]可以將Java語言中各種 操作共享的數(shù)據(jù)分為以下5類:不可變联予、絕對線程安全啼县、相對線程安全材原、線程兼容和線程對 立
不可變 (Immutable)的對象一定是線程安全的,無論是對象的方法實現(xiàn)還是方法的調(diào)用者季眷,都不需 要再采取任何的線程安全保障措施
-
在Java API中標(biāo)注自己是線程安全的類余蟹,大多數(shù) 都不是絕對的線程安全
/** * * 對vector線程安全的測試,通過對源碼debug測試發(fā)現(xiàn)會出現(xiàn) ArrayIndexOutOfBoundsException * 盡管這里使用到的Vector的get()、remove()和size()方法都是同步的子刮, 但是在多線程的環(huán)境中威酒, * 如果不在方法調(diào)用端做額外的同步措施的話,使用這段代碼仍然是 不安全的挺峡,因為如果另一個線程恰好在錯誤的時間里刪除了一個元素葵孤, * 導(dǎo)致序號i已經(jīng)不再 可用的話,再用i訪問數(shù)組就會拋出一個ArrayIndexOutOfBoundsException */ public class VectorTest { private static Vector<Integer> vector = new Vector<>(); public static void main(String[] args) { while (true) { for (int i = 0; i < 10; i++) { vector.add(i); } new Thread(() -> { for (int i = 0; i < vector.size(); i++) { vector.remove(i); } }).start(); new Thread(() -> { for (int i = 0; i < vector.size(); i++) { System.out.println(vector.get(i)); } }).start(); while (Thread.activeCount() > 90) ; } } }
代碼改進(jìn):
/** * * 改進(jìn)后debug源碼未發(fā)現(xiàn)異常情況 */ public class VectorTestImprove { private static Vector<Integer> vector = new Vector<>(); public static void main(String[] args) { while (true) { for (int i = 0; i < 10; i++) { vector.add(i); } new Thread(() -> { synchronized (vector) { for (int i = 0; i < vector.size(); i++) { vector.remove(i); } } }).start(); new Thread(() -> { synchronized (vector) { for (int i = 0; i < vector.size(); i++) { System.out.println(vector.get(i)); } } }).start(); while (Thread.activeCount() > 90) ; } } }
相對線程安全
相對的線程安全就是我們通常意義上所講的線程安全橱赠,在Java語言中佛呻,大部分的線程安全類都屬于這種類型,例如Vector病线、HashTable吓著、 Collections的synchronizedCollection()方法包裝的集合等
線程兼容
線程兼容是指對象本身并不是線程安全的,但是可以通過在調(diào)用端正確地使用同步手段 來保證對象在并發(fā)環(huán)境中可以安全地使用送挑,我們平常說一個類不是線程安全的绑莺,絕大多數(shù)時 候指的是這一種情況。Java API中大部分的類都是屬于線程兼容的惕耕,如與前面的Vector和 HashTable相對應(yīng)的集合類ArrayList和HashMap等纺裁。
線程對立
線程對立是指無論調(diào)用端是否采取了同步措施,都無法在多線程環(huán)境中并發(fā)使用的代 碼司澎。
線程安全的實現(xiàn)方法
互斥同步(Mutual Exclusion&Synchronization)是常見的一種并發(fā)正確性保障手段欺缘。同步 是指在多個線程并發(fā)訪問共享數(shù)據(jù)時,保證共享數(shù)據(jù)在同一個時刻只被一個(或者是一些挤安, 使用信號量的時候)線程使用谚殊。
從處理問題的方式上說,互斥同步屬于一種悲觀的 并發(fā)策略蛤铜,總是認(rèn)為只要不去做正確的同步措施(例如加鎖)嫩絮,那就肯定會出現(xiàn)問題,無論 共享數(shù)據(jù)是否真的會出現(xiàn)競爭围肥,它都要進(jìn)行加鎖
在Java中剿干,最基本的互斥同步手段就是synchronized關(guān)鍵字,我們還可以使用java.util.concurrent(下文稱J.U.C)包中的重入鎖 (ReentrantLock)來實現(xiàn)同步
非阻塞同步
隨著硬件指令集的發(fā)展穆刻,我們有了另外一個選擇:基于沖突檢測的 樂觀并發(fā)策略置尔,通俗地說,就是先進(jìn)行操作氢伟,如果沒有其他線程爭用共享數(shù)據(jù)榜轿,那操作就成 功了幽歼;如果共享數(shù)據(jù)有爭用,產(chǎn)生了沖突差导,那就再采取其他的補償措施(最常見的補償措施 就是不斷地重試试躏,直到成功為止),這種樂觀的并發(fā)策略的許多實現(xiàn)都不需要把線程掛起设褐, 因此這種同步操作稱為非阻塞同步(Non-Blocking Synchronization)颠蕴。
鎖優(yōu)化
自旋鎖與自適應(yīng)自旋
互斥同步對性能最大的影響是阻塞的實現(xiàn),掛起 線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成助析,這些操作給系統(tǒng)的并發(fā)性能帶來了很大的 壓力犀被。同時,虛擬機的開發(fā)團隊也注意到在許多應(yīng)用上外冀,共享數(shù)據(jù)的鎖定狀態(tài)只會持續(xù)很短 的一段時間寡键,為了這段時間去掛起和恢復(fù)線程并不值得。如果物理機器有一個以上的處理 器雪隧,能讓兩個或以上的線程同時并行執(zhí)行西轩,我們就可以讓后面請求鎖的那個線程“稍等一 下”,但不放棄處理器的執(zhí)行時間脑沿,看看持有鎖的線程是否很快就會釋放鎖藕畔。為了讓線程等 待,我們只需讓線程執(zhí)行一個忙循環(huán)(自旋)庄拇,這項技術(shù)就是所謂的自旋鎖注服,
自旋等待本身雖然避免了線程切換的開銷,但它是要占用處理器時間的措近, 因此溶弟,如果鎖被占用的時間很短,自旋等待的效果就會非常好瞭郑,反之辜御,如果鎖被占用的時間 很長,那么自旋的線程只會白白消耗處理器資源凰浮,而不會做任何有用的工作我抠,反而會帶來性 能上的浪費。因此袜茧,自旋等待的時間必須要有一定的限度,如果自旋超過了限定的次數(shù)仍然 沒有成功獲得鎖瓣窄,就應(yīng)當(dāng)使用傳統(tǒng)的方式去掛起線程了
自適應(yīng)意味著自旋的時間不再固定了笛厦,而是由前 一次在同一個鎖上的自旋時間及鎖的擁有者的狀態(tài)來決定