加州大學圣地亞哥分校(美國)校訓:“愿知識之光普照大地⌒⒚埃”
夏季柬姚,收獲的季節(jié),可看著A股大盤一直趴著庄涡,又是徒勞了半年量承,不免稍有傷神。罷了罷了穴店,聊聊可愛的技術吧撕捍,這東西才是養(yǎng)家糊口的根本。這年頭泣洞,任何技能必須具有強大的變現(xiàn)能力忧风,如果NO,請換道球凰∈ㄍ龋可能很多朋友們不認同,俗話說:三百六十行行行出狀元呕诉。OK蚤霞,比如送外賣的小哥,送出劉翔的速度义钉,送出吉尼斯紀錄昧绣,也是肉眼可見的天花板吧。哈哈捶闸,上述純扯淡夜畴,正事開干。
這篇文章我不想簡單地告訴你volatile的語義作用删壮,JMM如何保證可見性等贪绘,咱們其實可以系統(tǒng)聊聊,前世今生還是很有必要的央碟。那就從硬件系統(tǒng)架構(gòu)/多核CPU主存可見性/JVM的底層實現(xiàn)講講税灌,來龍去脈了解清楚了,那才叫真的理解,而不是簡單地死記硬背菱涤,這才算是科學地學習苞也。
一 硬件系統(tǒng)架構(gòu)演變
我們知道,運行在計算機上的程序粘秆,指令是由CPU執(zhí)行的如迟,數(shù)據(jù)是存儲在主存中的,CPU從主存中讀取數(shù)據(jù)執(zhí)行指令攻走,再回寫到主存中殷勘。CPU執(zhí)行指令的速度是非常快的昔搂,但讀取寫入主存相對較慢的玲销,有人拖后腿了,怎么辦呢摘符?大家有沒有聽說過CPU高速緩存贤斜,就是來解決拖后腿問題的。
1/CPU高速緩存
CPU高速緩存為單個CPU所有议慰,只有運行在這個CPU上的線程才能訪問。緩存系統(tǒng)是以緩存行為單位存儲的奴曙,一般是64個字節(jié)别凹。按級分為L1 cache / L2 cache / 多核心共享L3 cache。執(zhí)行流程如下:
a/首先CPU使用自己的寄存器洽糟,然后使用速度更快的L1炉菲,其中L1D緩存數(shù)據(jù),L1I緩存指令坤溃;
b/L1緩存和次快的L2做數(shù)據(jù)同步拍霜,L2緩存和L3做數(shù)據(jù)同步;
c/L3為多個CPU共享的薪介,與主內(nèi)存做數(shù)據(jù)同步祠饺;
2/緩存寫入主存
a/直寫(write-through)
直寫是透過本級緩存,直接把數(shù)據(jù)寫到下一級緩存中汁政,或直接寫到主存中道偷,同時更新緩存中的數(shù)據(jù),緩存行永遠和它對應的內(nèi)存內(nèi)容相匹配记劈。
b/回寫(write-back)
緩存并不會立即把寫操作傳遞到下一級勺鸦,而是僅修改本級緩存中的數(shù)據(jù),并且把對應的緩存數(shù)據(jù)標記為臟數(shù)據(jù)目木,臟數(shù)據(jù)會觸發(fā)回寫换途,即把里面的內(nèi)容寫到對應的內(nèi)存或下一級緩存中,回寫后,臟數(shù)據(jù)就變干凈了军拟。
3/CPU緩存一致性方案
a/通過在總線上加LOCK#鎖的方式
這是一種獨占式的方式剃执,在同一時刻只能運行一個CPU,效率較為低下吻谋;
b/通過緩存一致性協(xié)議忠蝗,保證多核CPU對共享數(shù)據(jù)的可見性,主要有:
窺探技術:
所有內(nèi)存?zhèn)鬏敹及l(fā)生在一條共享的總線上漓拾,對所有CPU可見阁最;每個CPU不停地窺探總線上發(fā)生的數(shù)據(jù)交換,并追蹤其他緩存在做什么骇两。緩存是CPU獨享的速种,內(nèi)存是CPU共享的,緩存訪問內(nèi)存是需要仲裁的低千,即在同一個指令周期中配阵,只有一個緩存可以讀寫內(nèi)存。
MESI協(xié)議:是緩存行四種狀態(tài)的縮寫示血,如下
已修改緩存行(Modified)棋傍,該緩存行已經(jīng)被所屬的CPU修改了,其他CPU持有的該緩存行的拷貝也會變成失效狀態(tài)难审;
獨占緩存行(Exclusive)瘫拣,和主存內(nèi)容保持一致的拷貝,其他CPU持有的這份內(nèi)容的拷貝變成失效狀態(tài)告喊;
共享緩存行(Shared)麸拄,和主存內(nèi)容保持一致的拷貝,其他CPU也可持有拷貝黔姜,但只能讀取拢切,不允許寫入;
無效緩存行(Invalid)秆吵,CPU中的緩存行無效了淮椰;
總結(jié)來看,只有某個CPU獨占了這個緩存行纳寂,才能夠?qū)懭胧蛋刺幱贛或E的狀態(tài);當CPU想讀取該緩存行烈疚,該緩存行必須是共享狀態(tài)黔牵。
二 指令重排序
我們知道,CPU在執(zhí)行指令的時候為了提升性能爷肝,會有一定的指令重排猾浦。執(zhí)行結(jié)果與預期結(jié)果一致陆错,則重排一定是基于規(guī)則,volatile能夠提供一定的有序性金赦,禁止一定的指令重排音瓷。這里介紹下不同級別的重排序,如下:
1/編譯器優(yōu)化的重排序
編譯器在不改變單線程程序的語義前提下夹抗,可重新安排語句的執(zhí)行順序绳慎;
2/指令級并行的重排序
如果不存在數(shù)據(jù)依賴性,處理器可以改變指令的執(zhí)行順序漠烧,采用的是指令級并行技術杏愤,將多條指令重疊執(zhí)行;
3/內(nèi)存系統(tǒng)的重排序
由于處理器使用緩存和讀寫緩沖區(qū)已脓,這使得加載和存儲操作看上去是亂序執(zhí)行珊楼。
小結(jié):就Java程序而言,從java代碼到CPU執(zhí)行序列度液,也要經(jīng)過上述的重排序厕宗。分析來看,重排序會造成內(nèi)存可見性問題堕担。要想解決問題已慢,需要了解重排序遵循的準則,才能找到對應的方案霹购。
這里介紹下指令重排中單線程和多線程遵循的準則:
1/as-if-serial
只針對于單線程運行的程序佑惠,不管怎么怎么重排序,都不會改變執(zhí)行結(jié)果厕鹃。就是這么硬氣兢仰,編譯器/運行時和處理器重排序必須遵循乍丈。這里通俗理解下:主要是對指令之間數(shù)據(jù)具有依賴性禁止重排序剂碴。滿足as-if-serial基準,單線程程序看起來像是按順序執(zhí)行的轻专,避免了內(nèi)存可見性問題忆矛。
2/happen-before
happen-before原則是Java內(nèi)存模型對指令重排的約束,如A happen-before B请垛,則A 操作的結(jié)果將對B可見催训,但實際中A 的執(zhí)行順序未必在B之前,只要執(zhí)行結(jié)果與預期一致即可宗收。JMM基于happen-before 原則保證多線程的內(nèi)存可見性漫拭,具體準則如下:
a/程序順序規(guī)則:一個線程中的每個操作,happen-before于該線程中任意后續(xù)的操作混稽;
b/監(jiān)視器鎖規(guī)則:對一個監(jiān)視器的解鎖采驻,happen-before于隨后對這個監(jiān)視器的加鎖审胚;
c/傳遞性:if A happen-before B,B happen-before C礼旅,則A happen-before C膳叨;
d/線程的start方法happen-before于線程的后續(xù)所有操作;
e/線程上的所有操作happen-before于其他線程在該線程上join返回成功后的操作痘系;
f/volatile變量規(guī)則:對一個volatile域的寫菲嘴,happen-before于任意后續(xù)對這個域的讀。
三 volatile原理
1/基本語義
volatile用來修飾變量或?qū)ο筇洌袃蓪诱Z義:
a/保證可見性龄坪,但不保證原子性;
b/提供一定程度的有序性(happen-before),禁止指令重排序奴璃。
2/原理剖析
jvm是通過內(nèi)存屏障來實現(xiàn)volatile的悉默,內(nèi)存屏障有四種類型,如下:
a/LoadLoad:指令形如Load1苟穆;LoadLoad抄课;Load2,其語義是Load1的裝載要先于Load2的裝載雳旅;
b/LoadStore:指令形如Load1跟磨;LoadStore燕锥;Store2甘邀,其語義是Load1的裝載要先于Store2存儲指令刷新到內(nèi)存百框;
c/StoreLoad:指令形如Store1骇钦;StoreLoad旷档;Load2捏检,其語義是Store1存儲指令刷新到內(nèi)存要先于Load2的裝載惠况;
d/StoreStore:指令形如Store1瑟幕;StoreStore迎变;Store2充尉,其語義是Store1存儲指令刷新到內(nèi)存要先于Store2的存儲;
舉例衣形,以兩個操作讀寫為例看下插入的內(nèi)存屏障驼侠,如下:
操作 | 普通讀 | 普通寫 | volatile讀 | volatile寫 |
---|---|---|---|---|
普通讀 | LoadStore | |||
普通寫 | StoreStore | |||
volatile讀 | LoadLoad | LoadStore | LoadLoad | LoadStore |
volatile寫 | StoreLoad | StoreStore |
demo世界不孤單,請閱:
/**
* @author 阿倫故事
* @Description:測試volatile的作用
* 對比去掉全局變量num的volatile的修飾看下測試結(jié)果
* */
@Slf4j
public class VolatileTest {
//聲明volatile全局變量
private volatile int num = 0;
public static void main(String[] args) {
VolatileTest volatileTest = new VolatileTest();
//創(chuàng)建一個用于volatile寫的線程并啟動
new Thread(()->{
log.info("Thread name:"+Thread.currentThread().getName()+"--sleep");
try {
Thread.sleep(500);
volatileTest.num = 5;
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("Thread name:"+Thread.currentThread().getName()+"--dead");
}).start();
//創(chuàng)建一個用于volatile讀的線程并啟動
new Thread(()->{
log.info("Thread name:"+Thread.currentThread().getName()+"--num="+volatileTest.num);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("Thread name:"+Thread.currentThread().getName()+"--dead--num="+volatileTest.num);
}).start();
}
}
特此聲明:
分享文章有完整的知識架構(gòu)圖谆吴,將從以下幾個方面系統(tǒng)展開:
1 基礎(Linux/Spring boot/并發(fā))
2 性能調(diào)優(yōu)(jvm/tomcat/mysql)
3 高并發(fā)分布式
4 微服務體系
如果您覺得文章不錯倒源,請關注阿倫故事,您的支持是我堅持的莫大動力句狼,在此受小弟一拜笋熬!
每篇福利: