網站首頁 編程語言 正文
jmm底層實現
- 一,深入理解java內存模型
- 1,什么是jmm模型
- 2,數據同步的八大原子操作
- 3,java并發的三大特性
- 4,volatile
- 5,CPU緩存一致性
- 5.1MESI協議
- 5.2,volatile不保證原子性
- 6,總結
一,深入理解java內存模型
1,什么是jmm模型
java memory model,java內存模型,是一種針對于多線程工作的一種抽象的規范,主要是針對在多線程的并發狀態下,共享資源是如何被訪問的。jmm只是一種抽象的概念,并不真實存在。
jvm運行的實體是線程,每個線程在創建jvm時都會創建一個工作內存,用于存儲私有的數據。java內存模型規定所有的共享變量都存儲在主內存中,主內存是共享的內存區域,私有線程都可以訪問。但是線程操作變量必須在工作內存中進行,即通過拷貝復制的方式,對線程操作完成之后再將線程寫回到主線程中。不能直接操作主內存的變量,必須通過變量的副本拷貝到工作內存中。當然jmm這個主內存不像jvm一樣真實存在具體的區域,只是一種抽像出來的一種模型,即線程開啟之后,就會存在這種無形的規范。
工作內存是私有數據,因此不同線程無法訪問對方的工作內存,即本地變量對于其他線程是不可見的,線程間的通信需要通過主線程完成
如下圖所示。
共享變量是存儲在主內存里面的,線程ABC都是通過復制這個變量作為副本加入到當前線程的工作內存里面。主內存主要存儲的是java的實例對象。所有創建的實例對象都存放在主內存中。由于是共享區域,多條線程在訪問同一個變量時就會可能發生線程安全的問題。如下面的initFlag這個就是存儲在主內存中,線程AB就是兩個獨立的線程,會去訪問主內存中的這個變量initFlag
public class Jmm_Study {
//共享變量,存儲在主內存中
private volatile static boolean initFlag = false;
//計數器
private volatile static int counter = 0;
public static void refresh(){
log.info("refresh data.......");
initFlag = true;
log.info("refresh data success.......");
}
public static void main(String[] args){
Thread threadA = new Thread(()->{
while (!initFlag){
//System.out.println("runing");
//counter++;
}
log.info("線程:" + Thread.currentThread().getName()
+ "當前線程嗅探到initFlag的狀態的改變");
},"threadA");
threadA.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread threadB = new Thread(()->{
refresh();
},"threadB");
threadB.start();
}
}
2,數據同步的八大原子操作
1,lock:作用于主內存的變量,把一個變量標記為一條線程獨占狀態
2,unlock:把一個處于鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定
3,read(讀取):作用于主內存中,需要先對變量進行副本的拷貝,然后將變量值傳輸到工作內存中
4,load(載入):在工作內存中,需要對傳輸過來的副本變量進行一個獲取,并且存入到工作內存中
5,use(使用): 需要將獲取的變量傳給執行引擎
6,assign(賦值):執行引擎會將這個收到的變量賦值給工作內存的變量
7,store(存儲):修改這個傳過來的副本之后,會將修改的值存儲并送到主內存中
8,write(寫入):會將這個存儲的變量寫回到主內存中
即每個操作都具有原子性,即運行期間不可中斷。并且必須按 read–>load–>use 、assign–>store–>write這個順序執行,不允許亂序
3,java并發的三大特性
可見性:基于jmm的內存模型可知,線程之間的內部變量時不可訪問的。所以為了知道別的線程修改了這個對象的變量之后,自己線程也要知道,因此增加了這個可見性的規范。即線程B修改了主內存的變量值,需要去通知線程A,這個值被修改了,并且重新去獲取新的值。如通過volatile實現
原子性:要么同時成功,要么同時失敗。一個操作是不可中斷的,即使是在多線程環境下,一個操作一旦開始就不會被其他線程影響。
有序性:從時間片的角度上看,代碼應該從上往下順序執行,如入棧出棧等。但是編譯器認為如果經歷指令重排之后,即代碼的執行順序與代碼的編寫順序不一致,這樣的話cpu的效率會更高。除了cpu之外,這個java的編譯器也會對這個代碼進行指令重排。
volatile:java并發里面的一個輕量級的鎖。可以保證可見性,也可以保證有序性,但是不能保證原子性
4,volatile
在字節碼方面:對象會有一個ACC_VOLATILE指令
在底層方面:主要通過這個EMSI協議,來保證這個緩存的一致性。
在現象方面:主要可以保證數據的可見性和有序性
可以保證修改之后別的線程可以及時的看到。主要是通過這個緩存行的方式實現。但是不加volatile也能看到別的線程的更改,但是看到的時間不能確定。volatile只是保證了這個及時性。
通過以下程序可以發現,在不用synchronized鎖時,這個volatile修飾的counter并不能保證這個原子性。
private volatile static int counter = 0;
static Object object = new Object();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(()->{
for (int j = 0; j < 10000; j++) {
//保證原子性
//synchronized (object){
//counter++;//分三步- 讀,自加,寫回
//}
counter++;
}
});
thread.start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter);
}
如假設兩個線程A和B都去操作這個count++,通過這個八大原子類操作可以發現,需要先將數據加到工作內存中,最后修改之后再把數據返回給主內存中,因此每個線程都有以下兩個步驟在工作內存中:
counter = 0;線程A1 counter = 0;線程B1 ...
counter = counter + 1;線程A2 counter = counter + 1;線程B2 ...
由于在多線程的場景下,每個線程獲取cpu的資源都是輪詢的。因此可能會出現以下場景,線程A在執行到A1時,cpu資源此時給到了線程B,線程B會去主內存中獲取這個counter并且修改這個counter值,完成之后會將值返回到主內存中,并且之后會告訴其他線程這個值已經被修改,此時線程A收到了這個通知,因此也會去主內存中獲取到這個值,但是,之前存在的線程A的A2步驟就會被丟棄,這樣就會導致counter少加1,這就解釋了為什么最終結果會小于上面的100000了。因此volatile并不能保證線程之間的原子性。
內部通過內存屏障的方式實現有序性,禁止了指令重排。內存屏障會告訴這個編譯器,哪些地方不能實現這個指令重排,否則會直接報錯。
public class CodeReorder {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (;;){
i++;
x = 0; y = 0;
a = 0; b = 0;
Thread t1 = new Thread(new Runnable() {
public void run() {
shortWait(10000);
a = 1;
x = b;
//手動實現指令重排
UnsafeInstance.reflectGetUnsafe().fullFence();
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
b = 1;
UnsafeInstance.reflectGetUnsafe().fullFence();
y = a;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if(x == 0 && y == 0) {
System.out.println(result);
break;
} else {
log.info(result);
}
}
}
/**
* 等待一段時間,時間單位納秒
* @param interval
*/
public static void shortWait(long interval){
long start = System.nanoTime();
long end;
do{
end = System.nanoTime();
}while(start + interval >= end);
}
}
5,CPU緩存一致性
5.1MESI協議
MESI協議規定:對一個共享變量的讀操作可以是多個處理器并發執行的,但是如果是對一個共享變量的寫操作,只有一個處理器可以執行,其實也會通過排他鎖的機制保證就一個處理器能寫。MESI為緩存一致性協議中的其中一種,這四個字母分別表示四種狀態
M :Modified,在緩存行中,將主內存讀取的數據修改了
E :Exclusive,互斥或者獨占狀態,當前只有本cpu中獲取了這個變量
S :Shared,共享狀態,此時有多個cpu中的緩存行中都獲取了這個變量
I :Invalid,失效狀態,如果其他cpu中的緩存行這個值修改之后,當前cpu中的值就是臟數據,需要設置成失效狀態
工作原理:cpu啟動之后會采用一種監聽的模式,一直監聽bus總線里面的消息的傳遞。任何人通過bus總線從內存里面獲取了東西,cpu都可以感知到。
1,如一個cpu0要讀取變量x,先從總線里面獲取變量x,如果被lock前綴修飾之后,就會被cpu0監聽到消息被讀取,在讀取的cpu里面會增加一個這個變量的副本,并且此時設置的狀態為E,獨占狀態;
2,如果此時有cpu1讀取這個變量,也會在cpu1里面增加一個副本,并且由于有多個cpu此時都擁有這個變量的副本,因此會將這個狀態設置為S共享狀態;
3,如果兩個cpu都要修改同一個變量,則需要在每一個cpu里面的緩存行上加鎖。如果其中一個cpu,如cpu0將這個變量值修改,則需要往bus總線里面發出修改的消息,并且告知cpu1里面擁有同一個變量的緩存行,此時cpu1里面的這個數據就變成了臟數據,狀態需要設置成I,失效狀態,并且需要去主內存中讀取這個新數據。bus總線需要去裁決哪個cpu可以獲取修改這個變量的資格,如果總線裁決失效,就會上升到總線鎖。
5.2,volatile不保證原子性
結合這個MESI這個協議,再來分析一下之前這個counter,就可以很清楚的知道為啥小于100000次了。
counter = 0;線程A1 counter = 0;線程B1 ...
counter = counter + 1;線程A2 counter = counter + 1;線程B2 ...
如下圖,在cpu0和cpu1同時從主內存將這個副本拷貝到工作內存中,并且同時保存在當前cpu的緩存行中。此時兩個cpu都要向這個bus總線發送修改這個變量的請求,bus總線會通過這個總線裁決的方式來判斷哪個cpu擁有這個修改這個變量的執行權,主要通過這個電位高低的方式實現,如此時cpu1獲取到修改這個變量的執行權,那么就會執行以下的第三步,此時執行counter = counter + 1,并且會告知擁有這個變量的其他cpu,如cpu0,這個變量被修改了,此時cpu0的counter也會接收到這個通知,并且會將當前的counter設置成失效狀態,并且會丟棄它,那么這個第四步就會不執行,這樣就失去了一次counter++的操作,這樣就導致了這個總和小于100000了。
當然這個cpu0里面的這個counter也不一定是丟棄,也可能是覆蓋。EMSI只能保證這個緩存行的一致性,但是如果這個cpu0里面的1,4操作已經處于這個寄存器中,那么這個counter不一定只會去這個內存中獲取這個最新值,也可能是從寄存器獲取到這個最新值。無論是覆蓋還是丟棄,都可以得到最后的counter值為1,同時也說明了這個volatile并不能保證這個原子性。
6,總結
就是通過這個JMM的內存模型來規范這個在多線程的場景下的共享變量的訪問,并且通過八大原子操作,規范每一個線程的執行步驟。在多個線程只需要保證有序性和可見性的時候的時候,可以直接使用這個volatile,并且通過這個EMSI協議來保證緩存的一致性,即用一句話來解釋volatile就是,主動刷新主內存,強制過期其他線程的工作內存
原文鏈接:https://blog.csdn.net/zhenghuishengq/article/details/125550673
- 上一篇:aqs原理初探以及公平鎖和非公平鎖實現
- 下一篇:Shell中常用的基礎命令
相關推薦
- 2022-07-20 C語言詳細講解while語句的用法_C 語言
- 2022-09-17 react組件memo?useMemo?useCallback使用區別示例_React
- 2023-03-29 Android?Flutter中Offstage組件的使用教程詳解_Android
- 2022-11-15 python重用父類功能的兩種方式實例詳解_python
- 2022-09-22 String和StringBuilder的用法
- 2022-12-08 React源碼state計算流程和優先級實例解析_React
- 2022-12-12 Android?WindowManager深層理解view繪制實現流程_Android
- 2022-08-17 WPF中的導航框架概述_C#教程
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支