Java并发编程实战_一线大厂架构师整理:java并发编程实践教程
并發(fā)編程是Java語言的重要特性之一, 在Java平臺上提供了許多基本的并發(fā)功能來輔助開發(fā)多線程應(yīng)用程序。然而,這些相對底層的并發(fā)功能與上層應(yīng)用程序的并發(fā)語義之間并不存在一種簡單而直觀的映射關(guān)系。因此,如何在Java并發(fā)應(yīng)用程序中正確且高效地使用這些功能就成了Java開發(fā)人員的關(guān)注重點(diǎn)。所以節(jié)課程將邀請一線大廠的Java架構(gòu)師總結(jié)了一個java并發(fā)編程實(shí)踐教程,歡迎一起來來閱讀
教程目錄
1、Java內(nèi)存模型
2、MESI緩存一致性協(xié)議與緩存行
3、并發(fā)常見問題?
4、volatile
5、鎖機(jī)制?
一、Java內(nèi)存模型
?內(nèi)存模型 ?
內(nèi)存模型可以理解為在特定的操作協(xié)議(緩存一致性協(xié)議)下,對特定的內(nèi)存和高速緩存進(jìn)行讀寫訪問的抽象。不同的物理機(jī)器,可能有著不同的“內(nèi)存模型”。Java虛擬機(jī)中定義的內(nèi)存模型可以屏蔽不同的硬件內(nèi)存模型,這樣就可以保證Java程序在各個平臺都能達(dá)到一致的內(nèi)存訪問效果,也就是常說的一次編寫到處運(yùn)行,因?yàn)閮?nèi)存模型為我們屏蔽掉了不同硬件平臺之間的差異。
?CPU緩存?
一級緩存和二級緩存:cpu各個core私有
三級緩存:cpu多core共享?
?CPU與內(nèi)存交互流程?
讀取流程:寄存器——>Load Buffer——>高速緩存(一級緩存——>二級緩存——>三級緩存)——> 內(nèi)存?
寫入流程:寄存器——>Store Buffer——>高速緩存(一級緩存——>二級緩存——>三級緩存)—— >內(nèi)存?
高速緩存和內(nèi)存之間通過內(nèi)存一致性協(xié)議(比如MESI協(xié)議)保證數(shù)據(jù)一致性(可見性) CPU和高速緩存直接通過Load Buffer(Store Buffer)減少堵塞時間,提高性能.
原子操作?
Java內(nèi)存模型為主內(nèi)存和工作內(nèi)存間的變量拷貝及同步定義8種原子性操作指令
lock(鎖定):
作用于主內(nèi)存的變量,把一個變量標(biāo)識為一條線程獨(dú)占狀態(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)虛擬機(jī)遇到一個需要使用變量的值的字節(jié)碼指令時將會執(zhí)行這個操作。assign(賦值):作用于工作內(nèi)存的變量它把一個從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
store(存儲):
作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個變量的值傳送到主內(nèi)存中,以便隨后的write的操作使用。
write(寫入):
作用于主內(nèi)存的變量,它把通過store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。?
二、MESI緩存一致性協(xié)議與緩存行
MESI緩存一致性協(xié)議?
MESI 是指4種狀態(tài)的首字母。每個Cache line有4個狀態(tài),可用2個bit表示,它們分別是:
MESI狀態(tài)轉(zhuǎn)換
觸發(fā)事件:?
MESI狀態(tài)遷移過程:
MESI性能優(yōu)化
緩存行數(shù)據(jù)修改,傳遞Invalidate失效消息給其他cpu緩存,但需要等待Invalidate Acknowledge確認(rèn)消息,cpu堵塞,降低性能。
cpu存儲緩存store bufffferes用來解決堵塞問題。緩存行修改時,只需要寫到store bufffferes中,傳遞Invalidate失效消息給其他cpu緩存,然后執(zhí)行其他邏輯。
別的CPU收到Invalidate消息時,把這個操作加入Invalidate Queues無效隊(duì)列,然后快速返回Invalidate Acknowledge消息,讓發(fā)起者做后續(xù)操作
cpu收到Invalidate Acknowledge確認(rèn)消息后,再把Store Bufffferes消息寫回緩存,修改狀態(tài)為(M)
其它CPU處理無效隊(duì)列里的無效消息時,讓緩存數(shù)據(jù)失效
其它CPU來取數(shù)據(jù)時,當(dāng)前cpu將數(shù)據(jù)刷新到內(nèi)存,狀態(tài)變?yōu)镾。?
?緩存行?
每個緩存里面都是由緩存行(cache line)組成的。緩存行是2的整數(shù)冪個連續(xù)字節(jié),一般為32-256個字節(jié)。最常見的緩存行大小是64個字節(jié)
偽共享問題
當(dāng)多線程修改互相獨(dú)立的變量時,如果這些變量共享同一個緩存行,就會無意中影響彼此的性能。出現(xiàn)在頻繁修改的場景中。
Core1運(yùn)行的線程想更新變量X,同時Core2的線程想要更新變量Y。這兩個變量在同一個緩存行中。每個線程都要去競爭緩存行的所有權(quán)來更新變量。
如果Core1獲得了所有權(quán),緩存子系統(tǒng)將會使Core2中對應(yīng)的緩存行失效。當(dāng)Core2獲得了所有權(quán)然后執(zhí)行更新操作,Core1就要使自己對應(yīng)的緩存行失效。這樣來來回回的經(jīng)過L3緩存,大大影響了性能。如果互相競爭的核心位于不同的插槽,就要額外橫跨插槽連接,問題可能更加嚴(yán)重。?
避免偽共享
解決偽共享最直接的方法就是填充(padding)。例如下面的VolatileLong,一個long占8個字節(jié),Java的對象頭占用8個字節(jié)(32位系統(tǒng))或者12字節(jié)(64位系統(tǒng),默認(rèn)開啟對象頭壓縮,不開啟占16字節(jié))。一個緩存行64字節(jié),那么我們可以填充6個long(6*8=48 個字節(jié))。這樣就能避免多個VolatileLong共享緩存行。?
public?class?VolatileLong?{???private?volatile?long?v;????//?private?long?v0,?v1,?v2,?v3,?v4,?v5?//?去掉注釋,開啟填充,避免緩存行共享?????}Java 8中引入了一個更加簡單的解決方案:@Contended 注解:
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.TYPE}) public @interface Contended { String value() default ""; }Contended注解可以用于類型上和屬性上,加上這個注解之后虛擬機(jī)會自動進(jìn)行填充,從而避免偽共享。這個注解在Java8 ConcurrentHashMap、ForkJoinPool和Thread等類中都有應(yīng)用。
JVM啟動參數(shù):-XX:-RestrictContended?
三、并發(fā)常見問題
?可見性 ?
線程一使用flflag變量進(jìn)行邏輯處理;線程二修改flflag變量。
線程一嗅探不到flflag的改變。?
public class VisibilityDemo { private static boolean flag = false; public static void main(String[] args) throws InterruptedException { new Thread(new Runnable() { @Override public void run() { while (!flag) { } System.out.println(Thread.currentThread().getName() + " 線程嗅探到 flag的修改");????????????}?????????????}).start();?????????????Thread.sleep(2000);? new?Thread(new?Runnable()?{? @Override? public?void?run()?{? System.out.println(Thread.currentThread().getName()?+?"開始修改??flag");?? flag = true; ? System .out.println(Thread.currentThread().getName() + "將flag修 ?改為false"); ?} }).start(); ? } ?}?
關(guān)于Thread.sleep(1000);的說明:?
主線程中的Thread.sleep(1000);經(jīng)過多次調(diào)整sleep時間大小(本地win10測試,1ms時偶爾無法正常結(jié)?束,大于1ms無法正常結(jié)束)?
結(jié)論:
cpu讀取數(shù)據(jù)流程:主內(nèi)存--->工作內(nèi)存(高速緩存)--->寄存器?
while經(jīng)過多次調(diào)用,返回一致的結(jié)果flag=false,cpu會進(jìn)行優(yōu)化,只取寄存器中的數(shù)據(jù),不會再去調(diào)用?緩存和主內(nèi)存。時間<=1ms,cpu會經(jīng)過寄存器刷高速緩存和內(nèi)存;時間>1ms,cpu只是訪問寄存器。
?有序性?
public class OrderlyDemo { ???private?static?int?x?=?0,y?=?0;????private?static?int?a?=?0,b?=?0;????private?static?int?i?=?0;????public?static?void?main(String[]?args)?throws?InterruptedException?{????for?(;;){????i++;?????x?=?0;y?=?0;?????a?=?0;b?=?0;?????Thread?thread1?=?new?Thread(new?Runnable()?{????? @Override????? ?public?void?run()?{???? ? waitTime(10000);? ????? a?=?1;?????? x?=?b;?? }?? });?????Thread?thread2?=?new?Thread(new?Runnable()?{????? @Override???? ?public?void?run()?{??? ? b = 1; ??? ? y = a; ??? ? } ??? }); ??? thread1.start();??? thread2.start(); ??? thread1.join(); ??? thread2.join(); ??? System.out.println("第" + i + "次執(zhí)行結(jié)果(" + x + "," + y + ")"); ??? if (x == 0 && y == 0){ ??? System.out.println("在第" + i + "次發(fā)生指令重排,(" + x + "," + y +")"); break; } } } public static void waitTime(int time){ long start = System.nanoTime(); long end; do { end = System.nanoTime(); }while (start + time >= end); } }原子性?
執(zhí)行5個線程,每個線程對number變量累加1000次。?
import java.util.ArrayList; import java.util.List;public class AtomicityDemo { private?static?int?number?=?0;????public?static?void?main(String[]?args)?throws?InterruptedException?????{?List?list?=?new?ArrayList<>(); for (int i=0; i<5; i++) { Thread t = new Thread(new Runnable() ??????????{?@Override??????????public?void?run()?{?????????? for?(int?i=0;?i<1000;?i++)?{?number++; }??????????} });??????? t.start();??????? list.add(t);?????}??????????for?(Thread?t?:?list)??????{?t.join();?????}??????????System.out.println("number="?+?number);?? }}?
四、volatile
使用volatile關(guān)鍵字,可解決可見性和有序性問題。
程序修改
可見性
private volatile static boolean flag = false;有序性
private volatile static int i = 0;volatile指令查看
將下載的hsdis-amd64.dll工具包解壓,復(fù)制到j(luò)dk安裝目錄的jre\bin\server目錄中
配置idea,在 VM options 選項(xiàng)中輸入?
volatile作用
volatile使用lock指令鎖定緩存行,將緩存行的數(shù)據(jù)立即寫回到主內(nèi)存中。
volatile使用內(nèi)存屏障禁止指令重排序來保證有序性
指令重排
指令重排序場景
編譯器( JIT編譯器)重排序。編譯器在不改變單線程語義(as-if-serial)的前提下,重新安排語句的執(zhí)行順序。
as-if-serial語義:不管怎么重排序,單線程下,程序的執(zhí)行結(jié)果不能被改變。編譯和處 理器都必須遵循as-if-serial語義。如果操作之間沒有數(shù)據(jù)依賴關(guān)系,就可以被重排序。
指令級并行的重排序。現(xiàn)代處理器采用了指令并行技術(shù)。如果不存在數(shù)據(jù)依賴性,處理器可 以改變語句對應(yīng)機(jī)器指令的執(zhí)行順序。
內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲操作看上去可能 是亂序執(zhí)行。(處理器的重排序)
內(nèi)存屏障?
內(nèi)存屏障(Memory Barrier)與內(nèi)存柵欄(Memory Fence)是同一個概念,不同的叫法。內(nèi)存屏障是解決硬件層面的可見性與重排序問題。內(nèi)存屏障是happen-before原則的實(shí)現(xiàn)。
?指令 | 描述 |
Store | 將處理器緩存的數(shù)據(jù)刷新到內(nèi)存中 |
Load | 將內(nèi)存存儲的數(shù)據(jù)拷貝到處理器的緩存中 |
| 屏障類型 | 指令示例 | 指令示例 |
| LoadLoad Barriers | ?Load1;LoadLoad;Load2 | 該屏障確保Load1數(shù)據(jù)的裝載先于Load2及其后所有裝載指令的的操作 |
| ?StoreStore Barriers | ?Store1;StoreStore;Store2 | 該屏障確保Store1立刻刷新數(shù)據(jù)到內(nèi)存(使其對其他處理器可見)的操作先于Store2及其后所有存儲指令的操作 |
| LoadStore Barriers | ?Load1;LoadStore;Store2 | 確保Load1的數(shù)據(jù)裝載先于Store2及其后所有的存儲指令刷新數(shù)據(jù)到內(nèi)存的操作 |
| ?StoreLoad Barriers | ?Store1;StoreLoad;Load2 | 該屏障確保Store1立刻刷新數(shù)據(jù)到內(nèi)存的操作先于Load2及其后所有裝載裝載指令的操作。它會使該屏障之前的所有內(nèi)存訪問指令(存儲指令和訪問指令)完成之后,才執(zhí)行該屏障之后的內(nèi)存訪問指令 |
StoreLoad ?Barriers同時具備其他三個屏障的效果,因此也稱之為全能屏障(mfence),是目前大多數(shù)處理器所支持的;但是相對其他屏障,該屏障的開銷相對昂貴。
x86架構(gòu)cpu的內(nèi)存屏障
x86架構(gòu)并沒有實(shí)現(xiàn)全部的內(nèi)存屏障。Store Barrier
sfence
指令實(shí)現(xiàn)了Store Barrier,相當(dāng)于StoreStore Barriers。
Load Barrier
lfence指令實(shí)現(xiàn)了Load Barrier,相當(dāng)于LoadLoad Barriers。
Full Barrier
mfence指令實(shí)現(xiàn)了Full Barrier,相當(dāng)于StoreLoad Barriers。
強(qiáng)制所有在mfence指令之前的store/load指令,都在該mfence指令執(zhí)行之前被執(zhí)行;所有在mfence指令之后的store/load指令,都在該mfence指令執(zhí)行之后被執(zhí)行。
volatile如何解決內(nèi)存可見性與重排序問題?
在編譯器層面,僅將volatile作為標(biāo)記使用,取消編譯層面的緩存和重排序。
如果硬件架構(gòu)本身已經(jīng)保證了內(nèi)存可見性(如單核處理器、一致性足夠的內(nèi)存模型等),那么volatile就是一個空標(biāo)記,不會插入相關(guān)語義的內(nèi)存屏障。
如果硬件架構(gòu)本身不進(jìn)行處理器重排序、有更強(qiáng)的重排序語義(能夠分析多核間的數(shù)據(jù)依賴)、或 ?在單核處理器上重排序,那么volatile就是一個空標(biāo)記,不會插入相關(guān)語義的內(nèi)存屏障。
? ? ? ? 如果不保證,以x86架構(gòu)為例,JVM對volatile變量的處理如下:
在寫volatile變量v之后,插入一個sfence。這樣,sfence之前的所有store(包括寫v)不會 ??被重排序到sfence之后,sfence之后的所有store不會被重排序到sfence之前,禁用跨sfence 的store重排序;且sfence之前修改的值都會被寫回緩存,并標(biāo)記其他CPU中的緩存失效。
在讀volatile變量v之前,插入一個lfence。這樣,lfence之后的load(包括讀v)不會被重排 序到lfence之前,lfence之前的load不會被重排序到lfence之后,禁用跨lfence的load重排 ??序;且lfence之后,會首先刷新無效緩存,從而得到最新的修改值,與sfence配合保證內(nèi)存 可見性
在另外一些平臺上,JVM使用mfence代替sfence與lfence,實(shí)現(xiàn)更強(qiáng)的語義。
DCL(雙重檢查加鎖)
public class Singleton {????private?static?volatile?Singleton?instance;?????private?Singleton()?{}?????public?static?Singleton?getInstance()?{???????? if(instance==null)?{???????? synchronized(Singleton.class)?{?????????????????if(instance==null)?{????????????????? instance?=?new?Singleton();????????????????? }??????????????}???????????}??????????return?instance;?????????}?}volatile作用:禁止instance?= new Singleton()這條語句指令重排序,避免getInstance()方法第一個if判斷為false,但instance實(shí)例沒有初始化完成的問題。
instance = new Singleton()執(zhí)行過程
在堆中分配對象內(nèi)存
填充對象必要信息+具體數(shù)據(jù)初始化+末位填充
將引用指向這個對象的堆內(nèi)地址
第二步和第三步可能重排序,導(dǎo)致instance引用不為null,但是實(shí)例沒初始化完成。
happen-before原則
hb原則是對單線程/多線程環(huán)境下數(shù)據(jù)一致性進(jìn)行的約束。hb規(guī)定了禁止編譯器和處理器重排序的8大原則。
hb的含義 |
前面的操作對后面的操作是可見狀態(tài)。 |
如果前后操作沒關(guān)聯(lián),可直接重排序,不受約束。 |
如果前后有關(guān)聯(lián),重排序后結(jié)果不一致,就是不可見狀態(tài),不能重排序。 |
單線程hb原則:在同一線程中,寫在前面的操作hb后面的操作鎖的hb原則:同一個鎖的unlock操作hb此鎖的lock操作
volatile的hb原則:對volatile變量的寫操作hb對此變量的任意操作hb傳遞性原則:A hb B,B hb C,那么A hb C
線程啟動hb原則:同一線程的start方法 hb 此線程的其它方法
線程中斷hb原則:對線程interrupt方法的調(diào)用 hb 被中斷線程的檢測到中斷狀態(tài)線程終結(jié)hb原則:線程所有操作 hb 線程的終止檢測
對象創(chuàng)建hb原則:一個對象的初始化完成 hb ?nalize方法調(diào)用
相關(guān)閱讀
1、SpringBoot源碼深度解析
2、SpringCloudAlibaba基礎(chǔ)入門教程
3、Elasticsearch企業(yè)應(yīng)用實(shí)戰(zhàn)開發(fā)艾編程官網(wǎng)課程搜索項(xiàng)目
給大家整理了一個系列的教程Java架構(gòu)師系列的教程,包含了系統(tǒng)架構(gòu)、Java相關(guān)、編碼規(guī)范、消息隊(duì)列、Maven、Nginx、Redis、MySQL、TomCat相關(guān)、Git等系列的電子書,回復(fù)關(guān)鍵詞就可以下載哦
關(guān)注本公眾號回復(fù)”我愛編程“就能獲取
同時還有精彩教程就、更多視頻+代碼資料文檔等你挖掘
回復(fù)關(guān)鍵詞
?Elasticsearch????分布式限流???消息隊(duì)列?????alibaba?????JVM性能調(diào)優(yōu)??
看更多精彩教程
喜歡本文,記得點(diǎn)擊個在看,或者分享給朋友哦!
總結(jié)
以上是生活随笔為你收集整理的Java并发编程实战_一线大厂架构师整理:java并发编程实践教程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: matplotlib输出图形到网页_必学
- 下一篇: **Java有哪些悲观锁的实现_Redi