先介紹下多進(jìn)程多線程在linux幾種通信方式
- 管道:管道的實(shí)質(zhì)是一個(gè)內(nèi)核緩沖區(qū),需要通信的兩個(gè)進(jìn)程各在管道的兩端,進(jìn)程利用管道傳遞信息
- 信號(hào):信號(hào)是軟件層次上對(duì)中斷機(jī)制的一種模擬,進(jìn)程不必阻塞等待信號(hào)的到達(dá),信號(hào)可以在用戶空間進(jìn)程和內(nèi)核之間直接交互
- 消息隊(duì)列:消息隊(duì)列是消息的鏈表,存放在內(nèi)存中并由消息隊(duì)列標(biāo)識(shí)符標(biāo)識(shí),允許多個(gè)進(jìn)程向它寫入與讀取消息
- 共享內(nèi)存:多個(gè)進(jìn)程可以可以直接讀寫同一塊內(nèi)存空間,是針對(duì)其他通信機(jī)制運(yùn)行效率較低而設(shè)計(jì)的
- 信號(hào)量:信號(hào)量實(shí)質(zhì)上就是一個(gè)標(biāo)識(shí)可用資源數(shù)量的計(jì)數(shù)器,它的值總是非負(fù)整數(shù)
- 套接字:套接字可用于不同機(jī)器之間的進(jìn)程間通信。有兩種類型的套接字:基于文件的和面向網(wǎng)絡(luò)(socket)
<font color='red'>java設(shè)計(jì)上則是基于共享內(nèi)存來實(shí)現(xiàn)進(jìn)程,線程的通信</font>
1 java內(nèi)存模型,JMM(JAVA Memory Model)
- <font color='red'>線程A需要和線程B交互,則需要更新工作內(nèi)存的共享變量副本到主存,然后線程B去主存讀取更新后的變量</font>
- java線程之間的通信是由JMM控制的,JMM決定線程對(duì)共享變量的寫入何時(shí)對(duì)另一線程可見。共享變量存在主存,線程擁有自己的工作內(nèi)存(一個(gè)抽象的概念,它覆蓋了緩存,寫緩沖區(qū),寄存器等)
2 CPU高速緩存、MESI協(xié)議
處理器的高速發(fā)展,CPU的性能和內(nèi)存性能差距拉大,為了解決問題,CPU設(shè)置多級(jí)緩存,例如L1、L2、L3高速緩存(Cache)。
和JMM的內(nèi)存布局相似,前者是系統(tǒng)級(jí)別,解決緩存一致性問題;后者是應(yīng)用級(jí)別的,解決的是內(nèi)存一致性問題
-
這些高速緩存一般都是獨(dú)屬于CPU內(nèi)部的,對(duì)其他CPU不可見,此時(shí)又會(huì)出現(xiàn)緩存和主存的數(shù)據(jù)不一致現(xiàn)象,CPU的解決方案有兩種
- 總線鎖定:當(dāng)某個(gè)CPU處理數(shù)據(jù)時(shí),通過鎖定系統(tǒng)總線或者是內(nèi)存總線,讓其他CPU不具備訪問內(nèi)存的訪問權(quán)限,從而保證了緩存的一致性
- <font color='red'>緩存一致性協(xié)議(MESI):緩存一致性協(xié)議也叫緩存鎖定,緩存一致性協(xié)議會(huì)阻止兩個(gè)以上CPU同時(shí)修改映射相同主存數(shù)據(jù)的緩存副本</font>
MESI實(shí)現(xiàn)是依靠處理器使用嗅探技術(shù)保證它的內(nèi)部緩存、系統(tǒng)主內(nèi)存和其他處理器的緩存的數(shù)據(jù)在總線上保持一致
例:處理器打算回寫臟內(nèi)存地址,而此內(nèi)存處于共享狀態(tài)(Share);那么其他處理器會(huì)嗅探到,并將使自身的對(duì)應(yīng)的緩存行無效,在下次訪問相應(yīng)內(nèi)存地址時(shí),刷新該緩存行-
緩存數(shù)據(jù)狀態(tài)有如下四種(MESI):
緩存狀態(tài) 描述 M(Modifed) 在緩存行中被標(biāo)記為Modified的值,與主存的值不同,這個(gè)值將會(huì)在它被其他CPU讀取之前寫入內(nèi)存,并設(shè)置為Shared E(Exclusive) 該緩存行對(duì)應(yīng)的主存內(nèi)容只被該CPU緩存,值和主存一致,被其他CPU讀取時(shí)置為Shared,被其他CPU寫時(shí)置為Modified S(Share) 該值也可能存在其他CPU緩存中,但是它的值和主存一致 I(Invalid) 該緩存行數(shù)據(jù)無效,需要時(shí)需重新從主存載入
3 指令重排序和內(nèi)存屏障指令
- 為提高程序性能,編譯器和處理器經(jīng)常會(huì)對(duì)指令做重排序,分別是編譯器優(yōu)化的重排序、指令并行級(jí)別的重排序,內(nèi)存系統(tǒng)的重排序。指令并行級(jí)別的重排序和內(nèi)存系統(tǒng)的重排序又可以歸為處理器重排序
image - 編譯器級(jí)別的指令重排序,可由JMM規(guī)則禁止特定類型的指令重排;對(duì)于處理器重排序則是插入特定類型的內(nèi)存屏障指令,以此禁止特定類型的重排序
- CPU的設(shè)計(jì)者提供內(nèi)存屏障機(jī)制,是將對(duì)共享變量讀寫的高速緩存的強(qiáng)一致性控制權(quán)(MESI的機(jī)制)交給了程序員或編譯器
- 介紹兩種處理器級(jí)別的內(nèi)存屏障指令
- 寫內(nèi)存屏障:該屏障之前的寫操作先于之后的寫操作;在指令后插入StoreBarrier,能讓寫入緩存中的最新數(shù)據(jù)更新寫入主內(nèi)存,讓其他線程可見
- 讀內(nèi)存屏障:該屏障之前的讀操作先于之后的讀操作;在指令前插入LoadBarrier,讓高速緩存中的數(shù)據(jù)失效,強(qiáng)制從主內(nèi)存加載數(shù)據(jù)
- 內(nèi)存屏障有兩個(gè)作用:<font color='red'>阻止屏障兩側(cè)的指令重排序;強(qiáng)制把寫緩沖區(qū)/高速緩存中的臟數(shù)據(jù)等寫回主內(nèi)存,讓緩存中相應(yīng)的數(shù)據(jù)失效</font>
- JAVA的內(nèi)存屏障指令,基本可以理解為在CPU內(nèi)存屏障指令上二次封裝
| JAVA內(nèi)存屏障指令 | 作用描述 |
|---|---|
| Store1;StoreStore;Store2 | 確保Store1數(shù)據(jù)對(duì)其他處理器可見(刷新到內(nèi)存),先于Store2及所有后續(xù)存儲(chǔ)指令的存儲(chǔ)。 |
| Load1;LoadStore;Store2 | 確保Load1數(shù)據(jù)裝載先于Store2及所有后續(xù)存儲(chǔ)指令的存儲(chǔ)。 |
| Store1;StoreLoad;Load2 | 確保Store1數(shù)據(jù)對(duì)其他處理器可見(刷新到內(nèi)存)先于Load2及所有后續(xù)裝載指令的裝載。 |
| Load1;LoadLoad;Load2 | 確保Load1數(shù)據(jù)的裝載,先于Load2及所有后續(xù)裝載指令的裝載。 |
特殊的是StoreLoad,會(huì)使該屏障之前的所有內(nèi)存訪問指令(裝載和存儲(chǔ)指令)完成之后,才執(zhí)行該屏障之后的內(nèi)存訪問指令;是一個(gè)”全能型”的屏障,它同時(shí)具有其他三個(gè)屏障的效果
- 用一句話描述java內(nèi)存屏障的目的:<font color='red'>把當(dāng)前工作內(nèi)存的數(shù)據(jù)全部刷新到主內(nèi)存,并且其他工作內(nèi)存的共享變量全部失效,真正需要用時(shí)再讀取主存最新的值</font>
4 happen-before原則
- 內(nèi)存屏障是相對(duì)于jvm,cpu級(jí)別的內(nèi)存一致性(內(nèi)存可見性)的解決方案;為了讓java程序員更容易理解,jsr-133使用happens-before的概念來說明不同操作之間的內(nèi)存可見性
- 程序次序規(guī)則:同一個(gè)線程,任意一操作happens-before同線程之后的全部操作
- 監(jiān)視器鎖(synchronized)規(guī)則:<font color='red'>對(duì)一個(gè)監(jiān)視器鎖的解鎖,happens-before隨后對(duì)這個(gè)鎖的加鎖</font>
- volatile變量規(guī)則:<font color='red'>對(duì)volatile變量的寫操作,happens-before該volatile變量之后的任意讀操作</font>
- 傳遞性:如果A先于B;B先于C;則A先于C
- happens-before部分規(guī)則是基于內(nèi)存屏障實(shí)現(xiàn)的
5 synchronized內(nèi)存語義
class Count{
int a = 0;
public synchronized void writer(){// 1
a++; //2
} //3
public synchronized void reader(){// 4
int i = a; //5
} //6
}
- 根據(jù)程序次序規(guī)則,1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happens- before 6。 根據(jù)監(jiān)視器鎖規(guī)則,3 happens-before 4。根據(jù)happens-before的傳遞性得 2 happens-before 5。執(zhí)行結(jié)果如下圖
- <font color='red'>線程釋放鎖時(shí)內(nèi)存語義:JMM會(huì)把該線程對(duì)應(yīng)的工作內(nèi)存中的共享變量刷新到主內(nèi)存中</font>
- <font color='red'>線程獲取鎖時(shí)內(nèi)存語義:JMM會(huì)把該線程對(duì)應(yīng)的工作內(nèi)存置為無效</font>
6 volatile的內(nèi)存語義
- volatile變量具有可見性,Java線程內(nèi)存模型確保所有線程看到這個(gè)變量的值是最新的,并且單個(gè)volatile變量的讀/寫具有原子性。
- 注意i++是復(fù)合操作,即使 i 是volatile變量,也不保證i++是原子操作
volatile Object instance;
instance = new Object();
//相應(yīng)匯編代碼
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
- 當(dāng)volatile變量修飾的共享變量進(jìn)行寫操作的反匯編代碼會(huì)出現(xiàn)
0x01a3de24: lock addl $0×0,(%esp),其實(shí)就是插入了內(nèi)存屏障導(dǎo)致的結(jié)果,lock表示volatile變量寫時(shí)被緩存鎖定了(MESI協(xié)議),作用如下- 禁止指令重排序
- 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存
- 這個(gè)寫回內(nèi)存的操作會(huì)使在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效
int a = 0; volatile boolean v = false;
線程A
a = 1; //1
v = true; //2
線程B
v = true; //3
System.out.println(a);//4
- 根據(jù)程序次序規(guī)則,1 happens-before 2;3 happens-before 4。根據(jù)volatile變量規(guī)則,2 happens-before 3。 根據(jù)happens-before的傳遞性規(guī)則,1 happens-before 4。程序的執(zhí)行結(jié)果表現(xiàn)如下圖
- <font color='red'>volatile寫的內(nèi)存語義:寫volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的工作內(nèi)存中的共享變量值刷新到主內(nèi)存</font>
- <font color='red'>volatile讀的內(nèi)存語義:讀一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的工作內(nèi)存置為無效。線程接下來將從主內(nèi)存中讀取共享變量</font>
- 非基本字段不應(yīng)該用volatile修飾。其原因是volatile修飾對(duì)象或數(shù)組時(shí),只能保證他們的引用地址的可見性
7 final內(nèi)存語義
- final寫內(nèi)存語義:
- <font color='red'>在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)final域的寫入,與隨后把這個(gè)被構(gòu)造對(duì)象的引用賦值給一個(gè)引用變量,這兩個(gè)操作之間不能重排序。保障對(duì)象被引用之前,fianl域里的變量都是被初始化的</font>
- 實(shí)現(xiàn)原理:編譯器會(huì)在final域的寫之后,構(gòu)造函數(shù)return之前,插入一個(gè)StoreStore屏障。這個(gè)屏障禁止處理器把final域的寫重排序到構(gòu)造函數(shù)之外。
public class Example { int i; //普通類型 final int j; // 引用類型 public Example () { // 構(gòu)造函數(shù) i = 0; j = 1; } public static void writer () { // 寫線程A執(zhí)行 obj = new Example (); } public static void reader () { // 讀線程B執(zhí)行 Example object = obj; // 讀對(duì)象引用 int a = object.i; // 讀普通域 int b = object.j; // 讀final域 } }- final只會(huì)禁止對(duì)其修飾變量的寫操作,被重排序到構(gòu)造函數(shù)之外;普通變量 i 的賦值可能會(huì)被重排到序構(gòu)造函數(shù)之外
- A線程創(chuàng)建obj,可能讓線程B拿到初始化一半的obj;final變量 j 被初始化,而普通變量 i 還沒初始化
疑問:內(nèi)存屏障不是會(huì)禁止指令重排嗎?個(gè)人猜想應(yīng)該是編譯器先重排序,此時(shí)普通變量已經(jīng)在構(gòu)造器外了,再根據(jù)final類型插入內(nèi)存屏障。上面的代碼執(zhí)行可能有如下情況:
image
- final讀內(nèi)存語義
- <font color='red'>初次讀一個(gè)包含final域的對(duì)象的引用,與隨后初次讀這個(gè)final域,這兩個(gè)操作之間不能重排序</font>
- 實(shí)現(xiàn)原理:要求編譯器在讀final域的操作前面插入一個(gè)LoadLoad屏障
- 當(dāng)使用final修飾引用對(duì)象或者數(shù)組時(shí),final只保證在構(gòu)造器返回之前對(duì)引用對(duì)象的操作先于構(gòu)造器返回之后的操作
public class Example { final int[] intArray; // intArray 是引用類型 public Example () { // 構(gòu)造函數(shù) intArray = new int[1]; intArray[0] = 1; //此操作對(duì)獲取該對(duì)象引用的線程是可見的 } }
8 synchronized,volatile內(nèi)存語義的原理梳理
9 練習(xí)題:延遲加載雙重鎖定是否真的安全
- 延遲加載雙重鎖定代碼分析
public class Instance { // 1
private static Instance instance; // 2
public static Instance getInstance() { // 3
if (instance == null) { // 4:第一次檢查
synchronized (Instance.class) { // 5:加鎖
if (instance == null) // 6:第二次檢查
instance = new Instance(); // 7:問題的根源出在這里
} // 8
} // 9
return instance; // 10
} // 11
}
代碼第7行instance=new Singleton();創(chuàng)建了一個(gè)對(duì)象。這一行代碼可以分解為如下的3行偽代碼
memory = allocate(); // A1:分配對(duì)象的內(nèi)存空間
ctorInstance(memory); // A2:初始化對(duì)象
instance = memory; // A3:設(shè)置instance指向剛分配的內(nèi)存地址
假如2和3之間重排序之后的順序如下
memory = allocate(); // A1:分配對(duì)象的內(nèi)存空間
instance = memory; //A3:instance指向剛分配的內(nèi)存地址,此時(shí)對(duì)象還沒有被初始化
ctorInstance(memory); // A2:初始化對(duì)象
假如發(fā)生A3、A2重排序,線程是不保障賦值和初始化對(duì)象兩步驟操作結(jié)果會(huì)一起同步到主存。
<font color='red'>因此第二個(gè)線程執(zhí)行到if (instance == null);// 4:第一次檢查時(shí),可能會(huì)得到一個(gè)剛分配的內(nèi)存而沒初始化的對(duì)象(此時(shí)沒有加鎖,鎖的happens-before規(guī)則不適用)</font>
- 相應(yīng)的兩個(gè)解決方法
在鎖內(nèi)使用volatile修飾instance,volatile禁止指令重排序,并且保障變量的內(nèi)存可見性:
private volatile static Instance instance;使用類加載器的全局鎖,在執(zhí)行類的初始化期間,JVM會(huì)去獲取一個(gè)鎖;這個(gè)鎖可以同步多個(gè)線程對(duì)同一個(gè)類的初始化,每個(gè)線程都會(huì)試圖獲取該類的全局鎖去初始化類
public class InstanceFactory { private static class InstanceHolder { public static Instance instance = new Instance(); } public static Instance getInstance() { // 這里將導(dǎo)致InstanceHolder類被初始化 return InstanceHolder.instance ; } }
10 練習(xí)題:偽共享(false sharing)
- 偽共享
- 前面介紹到每個(gè)CPU都有屬于自己的高速緩存,但是緩存數(shù)據(jù)大小是怎樣的呢?
- 這個(gè)大小并不是我們需求存多大就存多大的,而是一個(gè)固定的大小-64字節(jié),緩存的加載更新都是以連續(xù)的64字節(jié)內(nèi)存為單位,稱之為緩存行
- 一緩存行是可以存在多個(gè)變量的,比如long類型(64位==8字節(jié)),可以存入8個(gè)
- <font color='red'>假如變量A和變量B是在同一連續(xù)的內(nèi)存,CPU緩存加載A時(shí),B也會(huì)被讀??;反之亦然,A的臟回寫導(dǎo)致在其他CPU相應(yīng)內(nèi)存失效的同時(shí),同一緩存行的B內(nèi)存也被標(biāo)識(shí)為Modified(同舟共渡,一起翻船)</font>
- 設(shè)想變量A和B沒有關(guān)聯(lián),卻剛好在同一緩存行;然后A被CPU-X處理,B被CPU-Y處理;因?yàn)镃PU-X對(duì)A的緩存更新而導(dǎo)致B的緩存失效;CPU-Y要處理B,則要讀取更新后的緩存行(B實(shí)際是沒被更新),造成沒必要的內(nèi)存讀取開銷。這就是偽共享
- 偽共享的解決方法:
- 填充字節(jié),將對(duì)應(yīng)的變量填充到緩存行的大小。如下面定義的類,聲明額外的屬性
public final static class FilledLong { /**value 加 p1 - p6;加對(duì)象頭8個(gè)字節(jié)正好等于一緩存行的大小 */ //markWord + klass (32位機(jī),64位是16字節(jié)) 8字節(jié) public volatile long value = 0L; // 8字節(jié) public long p1, p2, p3, p4, p5, p6; //48字節(jié) }- 使用jdk的注解@Contended修飾變量,jvm會(huì)自動(dòng)將變量填充到緩存行的大小。注意的是需要加入啟動(dòng)參數(shù) -XX:-RestrictContended
參考文章
- java并發(fā)編程的藝術(shù)(書籍)
- Linux進(jìn)程間通信的幾種方式
- java內(nèi)存屏障
- 搞懂內(nèi)存屏障-指令與JMM
- 雜談什么是偽共享