進程與線程
- 操作系統(tǒng)是計算機的管理者,負責(zé)任務(wù)的調(diào)度、資源的分配和管理
- CPU是計算機的核心,CPU承擔(dān)了計算機的所有計算任務(wù)
- 應(yīng)用程序是具有某種功能的程序,程序是運行在操作系統(tǒng)之上
進程的概念
進程是一個具有一定功能的程序在一個數(shù)據(jù)集上的一次動態(tài)執(zhí)行的過程
進程的具有的特點
- 動態(tài)性:進程是程序在數(shù)據(jù)集上的一次運行過程,是有生命周期的、動態(tài)的
- 并發(fā)性:任何進程都可以和其他進程一起并發(fā)執(zhí)行
- 獨立性:進程是系統(tǒng)進行資源分配和調(diào)度的獨立單位
- 結(jié)構(gòu)性:進程由程序、數(shù)據(jù)和進程控制塊三部分組成
線程的概念
線程是程序執(zhí)行中一個單一的順序控制流程,是程序執(zhí)行流的最小單元,是處理器調(diào)度和分派的基本單位。
- 一個標(biāo)準的線程由線程ID,當(dāng)前指令指針PC,寄存器和堆棧組成
- 進程的各個線程之間共享進程的部分內(nèi)存空間 比如共享數(shù)據(jù) 進程空間 程序代碼
進程和線程的區(qū)別
1、一個進程由一個或者多個線程組成,線程是一個進程中程序的不同執(zhí)行路線
2、線程是程序執(zhí)行的最小單位,而進程是操作系統(tǒng)分配資源的最小單位
3、線程上下文切換比進程上下文切換要快的多
任務(wù)調(diào)度
? 在早期的操作系統(tǒng)中并沒有線程的概念,進程是能擁有資源和和獨立運行的最小單位,也是程序執(zhí)行的最小單位,它相當(dāng)于一個進程里只有一個線程,進程本身就是線程。所以線程有時被稱為輕量級進程。之后隨著計算機的發(fā)展,對多個任務(wù)之間上下文切換的效率要求越來越高,就抽象出一個更小的概念:線程。
大部分操作系統(tǒng)的任務(wù)調(diào)度是采用時間片輪轉(zhuǎn)的搶占式調(diào)度方式
一個任務(wù)執(zhí)行一小段時間后強制暫停去執(zhí)行下一個任務(wù),每個任務(wù)輪流執(zhí)行。任務(wù)執(zhí)行的一小段時間叫做時間片,任務(wù)正在執(zhí)行時的狀態(tài)叫運行狀態(tài),任務(wù)執(zhí)行一段時間后強制暫停去執(zhí)行下一個任務(wù),被暫停的任務(wù)就處于就緒狀態(tài),等待下一個屬于它的時間片的到來。這樣每個任務(wù)都能得到執(zhí)行,由于CPU的執(zhí)行效率非常高,時間片非常短,在各個任務(wù)之間快速地切換,給人的感覺就是多個任務(wù)在“同時進行”,這也就是我們所說的并發(fā)。

JVM線程與CPU線程

總結(jié)
? 程序是實現(xiàn)某種功能的代碼序列(靜態(tài))
? 線程是占有資源的獨立單元
? 進程是程序在某些數(shù)據(jù)集上的一次運行過程(動態(tài))
JVM內(nèi)存區(qū)域
- 堆內(nèi)存(Heap) 堆內(nèi)存是所有線程共享的數(shù)據(jù)區(qū),可分為年輕代(Young Gen)和老年代(Old Memory),是GC的主要工作區(qū)域,當(dāng)對象在堆中申請不到足夠的空間時,將拋出OutOfMemoryError異常
- 方法區(qū)(Method Area) 方法區(qū)是線程的的共享數(shù)據(jù)區(qū)域,它用于存儲被虛擬機加載的類信息,常量,靜態(tài)變量,即時編譯(JIT)后的代碼等數(shù)據(jù)。相對而言,垃圾收集行為在這個區(qū)域是比較少出現(xiàn)的。這個區(qū)域的內(nèi)存回收目標(biāo)主要是針對常量池的回收和對類型的卸載,一般來說這個區(qū)域的回收“成績”比較難以令人滿意,尤其是類型的卸載,條件相當(dāng)苛刻,但是這部分區(qū)域的回收確實是有必要的。根據(jù)Java虛擬機規(guī)范的規(guī)定,當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時,將拋出OutOfMemoryError異常
- 運行時常量池(RuntimeConstantPool) 是方法區(qū)的一部分。這里的常量池并不僅僅是Class文件中的常量池,JVM在進行編譯優(yōu)化時,會將部分常量載入到常量池中。JAVA語言并不要求常量一定在編譯時期產(chǎn)生,允許開發(fā)人員在程序運行期間向常量池中放入新的常量。比如String的intern方法。另外當(dāng)常量池?zé)o法申請到足夠的內(nèi)存時,將會拋出OutOfMemoryError異常
- 程序計數(shù)器:JVM多線程是通過CPU時間片輪轉(zhuǎn)(即線程的輪流切換并分配處理器執(zhí)行時間)算法來實現(xiàn)。也就是說,某個線程在執(zhí)行過程中可能會因為時間片耗盡而被掛起,而另一個線程獲取到時間片開始執(zhí)行。當(dāng)被掛起的線程重新獲取到時間片時,它想要從被掛起的地方繼續(xù)執(zhí)行,就必須知道上次執(zhí)行的位置。在JVM中,程序計數(shù)器就是用來記錄線程的字節(jié)碼執(zhí)行位置,因此程序計數(shù)器應(yīng)當(dāng)具有線程隔離的特性,也就是說在JVM中每一條線程都擁有屬于自己的程序計數(shù)器。沒條線程之間的計數(shù)器互不影響,獨立存儲,我們稱這類內(nèi)存區(qū)域為“線程私有”的內(nèi)存。
- 如果線程正在執(zhí)行的是一個Java方法,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址
- 如果正在執(zhí)行的是Natvie方法,這個計數(shù)器值則為空(Undefined)。因為native方法是java通過JNI直接調(diào)用本地C/C++庫,可以近似的認為native方法相當(dāng)于C/C++暴露給java的一個接口,java通過調(diào)用這個接口從而調(diào)用到C/C++方法。由于該方法是通過C/C++而不是java進行實現(xiàn)。那么自然無法產(chǎn)生相應(yīng)的字節(jié)碼,并且C/C++執(zhí)行時的內(nèi)存分配是由自己語言決定的,而不是由JVM決定的。
- 程序計數(shù)器占用內(nèi)存很小,在進行JVM內(nèi)存計算時,可以忽略不計。
- 程序計數(shù)器區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域
- JAVA虛擬機棧:虛擬機棧與程序計數(shù)器一樣,是線程私有的,每一個線程對應(yīng)一個虛擬機棧(也稱為線程棧)。虛擬機棧是描述JAVA方法執(zhí)行的內(nèi)存模型,線程中每一個方法在執(zhí)行的同時會創(chuàng)建一個棧針(Stack Frame)用于存儲局部變量表等數(shù)據(jù)。每一個方法從調(diào)用至出棧的過程,就對應(yīng)著棧幀在虛擬機中從入棧到出棧的過程。
JAVA虛擬機棧.JPG
在這個區(qū)域如果線程請求深度大于虛擬機允許的最大深度,將會拋出StackOverflowError異常。如果虛擬機允許動態(tài)擴展(當(dāng)前大部分虛擬機都可動態(tài)擴展,只不過java虛擬機也允許固定長度的虛擬機棧),當(dāng)擴展無法申請到足夠內(nèi)存時會拋出OutOfMemoryError異常。

-
本地方法棧:本地方法棧是為虛擬機使用到的Native方法服務(wù)。與虛擬機棧一樣,本地方法棧區(qū)域也會拋出StackOverflowError和OutOfMemoryError異常。
本地方法棧.png
JAVA內(nèi)存模型
JAVA內(nèi)存模型規(guī)范了JAVA虛擬機與計算機內(nèi)存是如何協(xié)同工作的,JAVA虛擬機是一個完整的計算機模型,因此這個模型的內(nèi)存模型就是JAVA內(nèi)存模型(規(guī)范抽象的模型)
- 共享數(shù)據(jù)內(nèi)存:線程共享數(shù)據(jù)
- 工作內(nèi)存:線程私有信息
- 基本數(shù)據(jù)類型,直接分配到工作內(nèi)存
- 引用類型存在在堆中,工作內(nèi)存存儲引用類型的地址
- 工作方式:
- 線程修改私有數(shù)據(jù),直接在工作空間修改
- 線程修改共享數(shù)據(jù),先將數(shù)據(jù)復(fù)制到工作空間中,在工作空間中修改,修改完成以后,刷新內(nèi)存中的數(shù)據(jù)
硬件內(nèi)存架構(gòu)
每一個CPU都包含一系列的寄存器,CPU在寄存器上的執(zhí)行速度遠大于在主存上執(zhí)行的速度,因為CPU訪問寄存器的速度遠快于訪問主存的速度。
當(dāng)代絕大多數(shù)CPU都有一定大小的緩存層,CPU訪問緩存層的速度快于訪問主存的速度,但通常比訪問內(nèi)部寄存器的速度還要慢一點。一些CPU還有多層緩存,比如一級緩存>二級緩存>三級緩存
通常情況下,當(dāng)一個CPU要去讀取主存時,它會事先將主存中的數(shù)據(jù)讀到CPU緩存中,將緩存中的數(shù)據(jù)讀到寄存器中執(zhí)行操作。當(dāng)CPU需要將結(jié)果寫回到主存時,它會將寄存器中的值刷新到緩存中,然后在某個時間節(jié)點將值刷新會主存。
當(dāng)cache帶來性能飛躍的同時,也引入了新的“緩存一致性問題”。
比如CPU執(zhí)行i++操作
(1)讀取主存中的變量i的值到cache中
(2)在寄存器中對i進行自增操作
(3)將結(jié)果寫會cache,最終同步到主存
一次步驟如果是單線程執(zhí)行將不會存在問題
如果多個線程都去執(zhí)行i++這個操作,變量i就會在不同的CPU緩存中存在
這時如果不保證CPU緩存數(shù)據(jù)的一致性,那么每一個CPU計算出來的結(jié)果就會不同,導(dǎo)致最終的計算錯誤
對于緩存一致性問題的解決方案
- 總線鎖定 前端總線(也叫CPU總線)是所有CPU與芯片組連接的主干道,負責(zé)CPU與外界所有部件的通信,包括高速緩存、內(nèi)存、北橋,其控制總線向各個部件發(fā)送控制信號、通過地址總線發(fā)送地址信號指定其要訪問的部件、通過數(shù)據(jù)總線雙向傳輸。在CPU1要做 i++操作的時候,其在總線上發(fā)出一個LOCK#信號,其他處理器就不能操作緩存了該共享變量內(nèi)存地址的緩存數(shù)據(jù),也就是阻塞了其他CPU,使該處理器可以獨享此共享數(shù)據(jù)內(nèi)存。
-
MESI協(xié)議
- MESI是內(nèi)存中緩存的四種狀態(tài)的縮寫,分別是M(Modified 修改)、E(Exclusive 互斥/獨占)、S(Shared 共享)、I(Invalid 無效)。每個cache line有四種狀態(tài),可用2bit表示
M 描述:cache line有效,數(shù)據(jù)被修改了,與主內(nèi)存中的數(shù)據(jù)不一致,只存在與本cache中
監(jiān)聽任務(wù):一個處于M狀態(tài)的緩存行,必須時刻監(jiān)聽所有試圖讀取該緩存行對應(yīng)的主存地址的操作,如果監(jiān)聽到,必須在讀取操作之前將緩存更新到主存中
E 描述:cache line有效,數(shù)據(jù)和內(nèi)存中的數(shù)據(jù)一致,且數(shù)據(jù)只存在于本CPU緩存中
監(jiān)聽任務(wù):一個處于E狀態(tài)的緩存行,必須時刻監(jiān)聽其他試圖讀取該緩存行對應(yīng)的主存地址的操作,如果監(jiān)聽到,則必須將緩存行的狀態(tài)置位S
S 描述:cache line有效,數(shù)據(jù)與內(nèi)存中一致,數(shù)據(jù)存在與很多CPU緩存中
監(jiān)聽任務(wù):一個處于E狀態(tài)的緩存行,必須時刻監(jiān)聽使緩存行無效和獨享該緩存行的請求,如果監(jiān)聽到則必須將緩存行狀態(tài)設(shè)置為I
I 描述:cache line無效,需要到主存中重新加載數(shù)據(jù)
- 對于M和E的狀態(tài)總是精確的,他們和所在緩存行的真正狀態(tài)是一致的,而S狀態(tài)是非一致的(有的線程工作內(nèi)存中是S,實際上主內(nèi)存中是E,因為消息傳遞的時間問題,可能存在不一致)
優(yōu)點:解決了CPU緩存一致性問題
缺點:緩存消息的傳遞需要消耗時間,CPU需要等待緩存的響應(yīng)從而引起各種各樣的性能問題。從而引入了存儲緩存 — Store Bufferes
處理器把它想要寫入到主存的值寫到存儲緩存,然后繼續(xù)去處理其他事情。當(dāng)所有失效確認(Invalidate Acknowledge)都接收到時,數(shù)據(jù)才會最終被提交。
Store Bufferes的問題
處理器會嘗試從存儲緩存中讀取值,如果存儲緩存中存在值,則會將值返回,無論這個值是否已經(jīng)提交
- 數(shù)據(jù)什么時候會保存完成,并沒有一定的保證
value = 3;
void exeToCPUA(){
value = 10;
isFinsh = true;
}
void exeToCPUB(){
if(isFinsh){
//value一定等于10?! isFinsh的存儲緩存很有可能先于value儲存緩存提交 就可能存在isFinsh的值為true,而value卻不等于10
assert value == 10;
}
}
硬件內(nèi)存對于處理緩存失效有以下約束
- 所有受到Invalidate請求的緩存行,必須立即發(fā)送Invalidate Acknowlege消息
- Invalidate并不真正執(zhí)行,而是被放在一個特殊的隊列(失效隊列)中,在方便的時候才會去執(zhí)行
- 處理器不會發(fā)送任何消息給所處理的緩存條目,直到它處理Invalidate
即使是這樣,處理器也不知道什么時候優(yōu)化是允許的,什么時候是不允許的,所以就將這個任務(wù)交給了寫程序的人。 這就是內(nèi)存屏障的由來(Memory Barriers)
寫屏障:Store Memory Barrier 是一條告訴處理器在執(zhí)行這之后的指令之前,應(yīng)該將所有存儲在(store bufferers)中的值刷新到主內(nèi)存。
讀屏障:Load Memory Barrier 是一條告訴處理器在執(zhí)行任何的加載前,應(yīng)該將失效隊列中的Invalidate處理完
void executedOnCpu0() {
value = 10;
// 在更新數(shù)據(jù)之前必須將所有存儲緩存(store buffer)中的指令執(zhí)行完畢。
storeMemoryBarrier();
finished = true;
}
void executedOnCpu1() {
while(!finished);
// 在讀取之前將所有失效隊列中關(guān)于該數(shù)據(jù)的指令執(zhí)行完畢。
loadMemoryBarrier();
assert value == 10;
}
這樣就可以完美的解決問題了。
JAVA內(nèi)存模型與硬件內(nèi)存架構(gòu)的關(guān)系
從上圖中可以看出部分線程和堆數(shù)據(jù)有時會出現(xiàn)在CPU緩存和CPU內(nèi)部寄存器中,當(dāng)對象和變量被存放在計算機各種不同的內(nèi)存區(qū)域時,就可能出現(xiàn)以下問題:
- 線程對共享變量修改的可見性
- 多線程讀、寫和檢查共享變量時,出現(xiàn)race conditions
共享變量的可見性
比如一個對象初始化在共享數(shù)據(jù)內(nèi)存,當(dāng)運行在CPU上的一個線程將這個對象讀入CPU緩存中,并在寄存器中對該對象進行了修改。只要這個對象沒有被刷新會主存,對象修改后的版本對于其他線程是不可見的。這種情況可能導(dǎo)致多個線程擁有這個共享對象的私有拷貝,每個拷貝停留在不同CPU緩存中。出現(xiàn)這種問題的主要原因還是線程之間的數(shù)據(jù)不可見性。
Race Conditions
如果存在兩個或者多個線程共享一個對象,多個線程在這個共享對象更新屬性,就有可能發(fā)生爭用條件(Race Conditions)
比如線程A讀取了一個共享對象的屬性count到CPU緩存中,同時,線程B也做了同樣的事情,但是是在一個不同的CPU緩存中?,F(xiàn)在線程A和線程B都想去將count的值+1,如果這些增加操作是被順序執(zhí)行的,那么屬性count應(yīng)該被增加兩次,然后再將結(jié)果寫回到主存中。
然而,兩次增加都是在沒有適當(dāng)?shù)耐较虏l(fā)執(zhí)行的。無論是線程A還是線程B將count修改后的版本寫回到主存中取,修改后的值僅會被原值大1,盡管增加了兩次。
并發(fā)編程的特性
JMM的作用:
屏蔽硬件平臺和操作系統(tǒng)訪問內(nèi)存的差異
規(guī)范內(nèi)存數(shù)據(jù)和工作空間數(shù)據(jù)的交互,定義了程序中變量的訪問規(guī)則
- 原子性:即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行
例:銀行轉(zhuǎn)賬 或者 執(zhí)行int i =10;
- 可見性:當(dāng)多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值
- 有序性:程序執(zhí)行的順序按照代碼的先后順序執(zhí)行,JVM在保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的前提下(as-if-serial),為了提高程序運行效率,會進行指令重排序
// 示例代碼片段
int a = 10; //語句1
int r = 2; //語句2
a = a + 3; //語句3
r = a*a; //語句4
在上述程序片段中,語句1和語句2執(zhí)行的先后順序并不會影響程序你最終結(jié)果,語句2和語句3的執(zhí)行順序也不會影響程序執(zhí)行的結(jié)果
在考慮指令之間數(shù)據(jù)依賴的前提下進行指令重排。比如語句4就不會在語句1之前執(zhí)行,因為語句4依賴與語句1的數(shù)據(jù)
在多線程的運行環(huán)境下出現(xiàn)了指令重排呢?
// 線程1
context = loadContext(); // 語句1
inited = true;// 語句2
// 線程2
while(!inited){
sleep(3000);
}
doSomethings(context);
存在這樣的一種情況:因為語句1和語句2并沒有數(shù)據(jù)關(guān)聯(lián)性,因此執(zhí)行的指令會被重排序,如果線程1先執(zhí)行了語句2,此時線程1因為失去了CPU時間片而停止運行,線程2就會以為初始化工作已經(jīng)完成,那么就會跳出循環(huán),去執(zhí)行doSomethings方法,而此時的context并沒有被初始化,就會導(dǎo)致程序的錯誤。
- 由以上案例可以看出要想并發(fā)程序正確執(zhí)行,必須要保證原子性、可見性和有序性
JMM對與并發(fā)特征的保證
JMM與原子性
在java中,對基本數(shù)據(jù)類型變量的讀取和賦值操作是原子操作。
int x = 10;
int y = x;
x++;
y = x + 1;
多個原子性的操作合并到一起沒有原子性
在JVM中保證操作的原子性,可以使用Synchronized和Lock來實現(xiàn)
JMM與可見性
JVM提供volatile關(guān)鍵字來保證數(shù)據(jù)的可見性,可以理解為在JMM模型上實現(xiàn)的MESI協(xié)議
還可以使用Synchronized和Lock實現(xiàn)可見性,保證同一個時刻只允許一個線程獲取鎖操作共享數(shù)據(jù),并且在釋放鎖之前將共享數(shù)據(jù)更新到主存中,保證數(shù)據(jù)的可見性。
JMM與有序性
在執(zhí)行程序時,為了提高性能,編譯器和處理器常常會對指令做重排序。重排序分3種類型。
1)編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序。
2)指令級并行的重排序。現(xiàn)代處理器采用了指令級并行技術(shù)(Instruction-LevelParallelism,ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對應(yīng)機器指令的執(zhí)行順序。
3)內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲操作看上去可能是在亂序執(zhí)行。
從Java源代碼到最終實際執(zhí)行的指令序列,會分別經(jīng)歷以下3種重排序:

以上的重排序可能會導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見性問題。
對于編譯器,JMM的編譯器重排序規(guī)則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。
對于處理器重排序,JMM的處理器重排序規(guī)則會要求Java編譯器在生成指令序列時,插入特定類型的內(nèi)存屏障(Memory Barriers)指令,通過內(nèi)存屏障指令來禁止特定類型的處理器重排序。
JMM屬于語言級的內(nèi)存模型,它確保在不同的編譯器和不同的處理器平臺之上,通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內(nèi)存可見性保證。
Volatile關(guān)鍵字會禁止進行指令的重排序:在進行指令優(yōu)化時,不能將對volatile變量訪問之前的語句放在其后面執(zhí)行,也不能把volatile變量后面的語句放到其前面執(zhí)行。因此只能在一定程度上保證有序性,比如volatile之前執(zhí)行的執(zhí)行的指令就可以重排序。
//volatile boolean flag = false;
int x = 2;
int y = 0;
flag = true;
// int x=2 和int y=0不能在flag = true;之后執(zhí)行
// int x = 4和int y=-1也不能在flag = true;之前執(zhí)行
// 但是int x=2 和int y=0的指令有可能重排序
int x = 4;
int y = -1
另外可以使用Synchronized和Lock來實現(xiàn)有序性,變多線程為單線程,自然保證了有序性
JMM先天存在的有序規(guī)則:happens-before原則
程序次序規(guī)則:一個線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作
鎖定規(guī)則:一個unLock操作先行發(fā)生于后面對同一個鎖的lock操作
Volatile規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作
傳遞規(guī)則:如果操作A先于操作B,而操作B先于操作C,那么操作A先于操作C
啟動規(guī)則(線程的start先于此線程的一切動作)
中斷規(guī)則(線程interrupt方法的調(diào)用先于檢測到中斷事件的發(fā)生
線程終結(jié)規(guī)則(線程的所有操作都先于線程的終止檢測)
對象終結(jié)規(guī)則(對象的初始化完成先于finalize的開始)</pre>
參考:
進程與線程:https://www.cnblogs.com/qianqiannian/p/7010909.html
jvm內(nèi)存區(qū)域:https://www.cnblogs.com/junzi2099/p/8418009.html
java內(nèi)存模型:http://ifeve.com/java-memory-model-6/
總線鎖:https://blog.csdn.net/qq_35642036/article/details/82801708
