1. 硬件的效率與一致性
? 由于計(jì)算機(jī)的存儲(chǔ)設(shè)備與處理器的運(yùn)算速度有幾個(gè)數(shù)量級(jí)的差距,所以現(xiàn)代計(jì)算機(jī)系統(tǒng)不得不加入一層讀寫速度盡可能接近處理器運(yùn)算速度的高速緩存來作為內(nèi)存與處理器之間的緩沖:將運(yùn)算需要用到的數(shù)據(jù)復(fù)制到緩存中,當(dāng)運(yùn)算結(jié)束后在從緩存中同步到內(nèi)存中.
? 但這樣就引入了一個(gè)新的問題:緩存一致性.每個(gè)處理器都有自己的高速緩存,而它們又共享同一主內(nèi)存.當(dāng)多個(gè)處理器的運(yùn)算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時(shí),將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致.為了解決一致性問題,需要各個(gè)處理器訪問緩存時(shí)都遵循一些協(xié)議,在讀寫時(shí)根據(jù)協(xié)議來進(jìn)行操作.
? 除了增加高速緩存之外,為了使處理器內(nèi)部的運(yùn)算單元能盡量被充分被利用,處理器可能會(huì)對(duì)輸入代碼進(jìn)行亂序執(zhí)行優(yōu)化,但前提是保證程序運(yùn)行的結(jié)果與順序執(zhí)行的結(jié)果是一致性的,只是各個(gè)語句的執(zhí)行順序可能與輸入代碼不一致.
2. Java內(nèi)存模型
? Java虛擬機(jī)規(guī)范中試圖定義一種Java內(nèi)存模型來屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問效果.這個(gè)模型必須定義的足夠嚴(yán)謹(jǐn),才能讓Java的并發(fā)內(nèi)存訪問操作不會(huì)產(chǎn)生歧義.但同時(shí)也必須定義的足夠?qū)捤?使得虛擬機(jī)的實(shí)現(xiàn)有顧?quán)u的自由空間去利用硬件的各種特性來獲取更好的執(zhí)行速度.
(1). 主內(nèi)存與工作內(nèi)存
? Java內(nèi)存模型的主要目標(biāo)是定義程序中各個(gè)變量的訪問規(guī)則,即在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié).這里的變量包括實(shí)例字段,靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素,不包括局部變量和方法參數(shù),因?yàn)楹笳呤蔷€程私有的,不存在競爭問題.
? Java內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存(類比物理硬件中的主內(nèi)存).同時(shí)每條線程還有自己的工作內(nèi)存(類比高速緩存),線程的工作內(nèi)存中保存了被該線程使用到的變量的主內(nèi)存副本拷貝,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接對(duì)主內(nèi)存進(jìn)行讀寫.不同線程之間不能直接訪問對(duì)方工作內(nèi)存中的變量,線程間變量值的傳遞均需要通過主內(nèi)存來進(jìn)行過渡.
? 主內(nèi)存主要對(duì)應(yīng)于Java對(duì)中的對(duì)象實(shí)例數(shù)據(jù)部分,而工作內(nèi)存則對(duì)應(yīng)于虛擬機(jī)棧中的部分區(qū)域.更低層面上講,主內(nèi)存直接對(duì)應(yīng)于物理硬件的內(nèi)存,工作內(nèi)存優(yōu)先存儲(chǔ)于寄存器和高速緩存中.
(2). 內(nèi)存間交互操作
? 主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,Java內(nèi)存模型中定義了一下8種操作來完成,虛擬機(jī)是現(xiàn)實(shí)必須保證下面提及的每一中操作都是原子的,不可再分的.
- lock(鎖定):作用于主內(nèi)存的變量,將一個(gè)變量標(biāo)識(shí)為一個(gè)線程獨(dú)占的狀態(tài)
- unlock(解鎖):所用于主內(nèi)存的變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來
- read(讀取):作用于主內(nèi)存的變量,把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存
- load(載入):作用于工作內(nèi)存的變量,把read操作傳輸?shù)焦ぷ鲀?nèi)存的變量值放入工作內(nèi)存中的變量副本中
- use(使用):作用于工作內(nèi)存的變量,把工作內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎
- assign(賦值):作用于工作內(nèi)存的變量,把一個(gè)從執(zhí)行引擎接收到的值賦值給工作內(nèi)存中的一個(gè)變量
- store(存儲(chǔ)):作用于工作內(nèi)存的變量,把工作內(nèi)存的一個(gè)變量值傳送到主內(nèi)存
- write(寫入):作用于主內(nèi)存的變量,把store操作傳來的變量值放入主內(nèi)存的變量中
? 而這八種操作在執(zhí)行時(shí)必須滿足一下規(guī)則:
- read和load,store和write必須成對(duì)出現(xiàn)
- 在進(jìn)行assign操作后,必須同步到主內(nèi)存
- 不允許無緣無故的將工作內(nèi)存中的變量值同步回主內(nèi)存
- 新變量只能從主內(nèi)存中誕生,工作內(nèi)存中不允許使用一個(gè)未初始化的變量,所有變量都來自主內(nèi)存
- 一個(gè)變量在同一時(shí)刻只允許一條線成對(duì)其進(jìn)行l(wèi)ock操作,但lock操作可以被一個(gè)線程執(zhí)行多次
- 如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作,將會(huì)清空工作內(nèi)存中此變量的值,使用這個(gè)變量之前要先在主內(nèi)存中復(fù)制
- 如果一個(gè)對(duì)象沒有被執(zhí)行過lock操作,就不能執(zhí)行unlock操作
- 對(duì)一個(gè)變量記性u(píng)nlock操作時(shí),不許先把此變量同步回主內(nèi)存
(3). 對(duì)于volatile型變量的特殊規(guī)則
? 當(dāng)一個(gè)變量定義為volatile后,它將具備兩種特性.
? 第一是保證此變量對(duì)所有線程的可見性,這里的可見性是指當(dāng)一個(gè)線程修改了這個(gè)變量的值,則對(duì)于其他所有線程是可以立即得知的.(在各個(gè)工作內(nèi)存中并不是立即同步,但在使用之前一定會(huì)進(jìn)行同步,所以可以認(rèn)為不存在一致性問題,但是Java中的運(yùn)算不是原子操作,在并發(fā)情況下volatile變量仍然是不安全的)
? 由于volatile變量只能保證可見性,在不符合一下規(guī)則的情況下,仍然需要加鎖來保證原子性:
- 運(yùn)算結(jié)果并不依賴變量的當(dāng)前自,或者說只有單一線程能改變變量的值
- 變量不需要與其他的狀態(tài)變量共同參與不變約束
? 第二個(gè)語義是禁止指令重排序優(yōu)化,更改順序會(huì)導(dǎo)致在其他線程中volatile變量的值的改變時(shí)間變得隨機(jī)
? 有volatile修飾的變量在賦值后會(huì)多執(zhí)行一個(gè)lock add $0x0, (%esp)的操作,這個(gè)操作相當(dāng)于一個(gè)內(nèi)存屏障.只有一個(gè)cpu訪問內(nèi)存時(shí)并不需要內(nèi)存屏障,但如果有多個(gè)cpu訪問同一塊內(nèi)存,就需要內(nèi)存屏障了.
? add $0x0, (%esp)是一個(gè)空操作,關(guān)在在于lock前綴.它的作用是使本cpu的cache寫入了內(nèi)存,相當(dāng)于對(duì)cache中的變量進(jìn)行一次store和write操作.
? 因此這個(gè)指令將修改同步到內(nèi)存時(shí),意味著之前的操作都已經(jīng)執(zhí)行完成,形成了"指令重排序無法越過內(nèi)存屏障"的效果.
(4). 對(duì)于long和double型變量的特殊規(guī)則
? 允許虛擬機(jī)實(shí)現(xiàn)選擇可以不保證64位數(shù)據(jù)類型的load,store,read和write這4中操作的原子性,但目前各種平臺(tái)下的商用虛擬機(jī)幾乎都選擇把64位數(shù)據(jù)的讀寫操作作為原子操作來對(duì)待.
(5). 原子性,可見性和有序性
? Java內(nèi)存模型是圍繞著在開發(fā)過程中如何處理原子性,可見性和有序性這3個(gè)特征來建立的.
- 原子性:由Java內(nèi)存模型來直接保證的原子性變量操作包括read,load,assign,use,store和write.我們大致可以認(rèn)為基本數(shù)據(jù)類型的訪問讀寫是具備原子性的.如果應(yīng)用場景需要一個(gè)更大范圍的原子性保證,Java內(nèi)存模型提供了lock和unlock操作.
- 可見性:可見性是指當(dāng)一個(gè)線程修改了共享變量的值,其他線程能夠立即得知這個(gè)值的修改.
- 有序性:Java程序中天然的有序性可以總結(jié)為一句話,如果在本線程內(nèi)觀察,所有的操作都是有序的,如果在另一個(gè)線程中觀察,所有的操作都是無序的.
(6). 先行發(fā)生原則
? 先行發(fā)生的原則非常重要,它是判斷數(shù)據(jù)是否存在競爭,線程是否安全的主要依據(jù),依靠這個(gè)原則,我們可以通過幾條規(guī)則一攬子地解決并發(fā)環(huán)境下兩個(gè)操作之間是否可能存在沖突的所有問題.
? 先行發(fā)生是Java內(nèi)存模型中定義的兩項(xiàng)操作之間的偏序關(guān)系,如果說操作A先行發(fā)生與操作B,操作A產(chǎn)生的影響能被操作B觀察到.
? 下面是Java內(nèi)存模型下一些天然的先行發(fā)生關(guān)系:
- 程序次序規(guī)則:一個(gè)線程內(nèi)按照程序代碼順序,先寫的操作先行發(fā)生于后寫的操作.
- 管程鎖定規(guī)則:一個(gè)unlock操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作.
- volatile變量規(guī)則:對(duì)一個(gè)volatile變量的寫操作先行發(fā)生于對(duì)這個(gè)變量的讀操作.
- 線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先行發(fā)生于此線程的每一個(gè)動(dòng)作.
- 線程終止規(guī)則:線程中的所有操作都線性發(fā)生于對(duì)此線程的終止檢測.
- 線程終端規(guī)則:對(duì)象成interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷時(shí)間的發(fā)生.
- 對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成先行發(fā)生于它的finalize()方法的開始.
- 傳遞性:如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那么操作A先行發(fā)生于操作C.
3. Java與線程
? Java里面談?wù)摬l(fā),多數(shù)都與線程脫不開關(guān)系.
(1). 線程的實(shí)現(xiàn)
? 線程是比進(jìn)程更輕量級(jí)的調(diào)度執(zhí)行單位,線程的引入,可以吧一個(gè)進(jìn)程的資源分配和執(zhí)行調(diào)度分開,各個(gè)線程既可以共享進(jìn)程資源,又可以獨(dú)立調(diào)度.
? Java語言提供了在不同硬件和操作系統(tǒng)平臺(tái)下對(duì)線程操作的統(tǒng)一處理,每一個(gè)已經(jīng)執(zhí)行start()還未結(jié)束的java.lang.Thread類的實(shí)例就代表了一個(gè)線程.它的所有關(guān)鍵方法都是聲明為Native的.
? 實(shí)現(xiàn)線程主要有3中方式:使用內(nèi)核線程實(shí)現(xiàn),使用用戶線程實(shí)現(xiàn)和使用用戶線程加輕量級(jí)進(jìn)程混合實(shí)現(xiàn).
1). 使用內(nèi)核線程實(shí)現(xiàn)
? 直接用操作系用內(nèi)核支持的線程,這種線程由內(nèi)核來完成線程切換,內(nèi)核通過操縱調(diào)度器對(duì)線程進(jìn)行調(diào)度,并負(fù)責(zé)講線程的任務(wù)映射到各個(gè)處理器上,每一個(gè)內(nèi)核線程可以視為內(nèi)核的一個(gè)分身.
? 程序一般不會(huì)直接去直接使用內(nèi)核線程,而是去使用內(nèi)核線程的一種高級(jí)接口--輕量級(jí)進(jìn)程,就是我們通常意義上的線程,每個(gè)輕量級(jí)進(jìn)程都有一個(gè)內(nèi)核線程支持.
? 由于內(nèi)核線程的支持,每一個(gè)輕量級(jí)進(jìn)程都成為一個(gè)獨(dú)立的調(diào)度單元,即時(shí)有一個(gè)輕量級(jí)進(jìn)程在系統(tǒng)調(diào)度中阻塞了,也不會(huì)影響整個(gè)進(jìn)程繼續(xù)工作.
? 局限性:由于是基于內(nèi)核線程實(shí)現(xiàn)的,所以各種線程操作都需要進(jìn)行需用調(diào)用,代價(jià)較高,需要在用戶態(tài)和內(nèi)核態(tài)中來回切換.其次,每個(gè)輕量級(jí)進(jìn)程都需要有一個(gè)內(nèi)核線程的支持,因此需要消耗一定的內(nèi)核資源.
2). 使用用戶線程實(shí)現(xiàn)
? 從廣義上講,一個(gè)線程只要不是內(nèi)核線程,就可以認(rèn)為是用戶線程,所以輕量級(jí)進(jìn)程也屬于用戶進(jìn)程,但輕量級(jí)進(jìn)程的實(shí)現(xiàn)始終是建立在內(nèi)核之上的,許多操作都要進(jìn)行系統(tǒng)調(diào)用,效率也會(huì)受到限制.
? 狹義上的用戶線程指的是完全建立在用戶空間的線程庫上,系統(tǒng)內(nèi)核不能感知存在的實(shí)現(xiàn).用戶線程的建立,同步,銷毀和調(diào)度完全在用戶態(tài)中完成,不需要內(nèi)核的幫助.
? 這種線程不需要切換到內(nèi)核態(tài),因此操作可以使非??焖俨⑶业秃牡?也可以支持規(guī)模更大的線程數(shù)量,部分高性能數(shù)據(jù)庫就是這樣實(shí)現(xiàn)的.
? 優(yōu)勢(shì)在于不需要系統(tǒng)內(nèi)核支援,快速低耗,缺點(diǎn)在于沒有內(nèi)核的支援,所有的線程操作都需要用戶程序自己處理.
? 使用用戶線程實(shí)現(xiàn)的程序一般都比較復(fù)雜,以致于越來越少.
3). 使用用戶線程加輕量級(jí)進(jìn)程混合實(shí)現(xiàn)
? 這種混合實(shí)現(xiàn)下,既存在用戶線程,也存在輕量級(jí)進(jìn)程.用戶線程還是完全建立在用戶空間中,所以用戶線程的創(chuàng)建,切換,析構(gòu)等操作依然廉價(jià),并且可以支持大規(guī)模的用戶線程并發(fā).而操作系統(tǒng)提供支持的輕量級(jí)進(jìn)程則作為用戶線程和內(nèi)核線程之間的橋梁,這樣可以使用內(nèi)核提供的線程調(diào)度功能及處理器映射,并且用戶線程的系統(tǒng)調(diào)用要通過輕量級(jí)線程來完成,大大降低了整個(gè)進(jìn)程被完全阻塞的風(fēng)險(xiǎn).
4). Java線程的實(shí)現(xiàn)
? 早期JDK 1.2之前,基于用戶線程實(shí)現(xiàn),而在JDK 1.2中替換為基于操作系統(tǒng)原生線程模型來實(shí)現(xiàn),因此目前的JDK版本中線程模型與操作以系統(tǒng)的支持有很大關(guān)系.
(2). Java線程調(diào)度
? 線程調(diào)度是指系統(tǒng)為線程分配處理器使用權(quán)的過程,主要調(diào)度方式有兩種,分別是協(xié)同式線程調(diào)度和搶占式線程調(diào)度.
? 使用協(xié)同式調(diào)度的多線程系統(tǒng),線程的執(zhí)行時(shí)間由線程本身空值,當(dāng)工作內(nèi)容執(zhí)行完成之后主動(dòng)通知系統(tǒng)切換到另外一個(gè)線程上.好處是實(shí)現(xiàn)簡單,切換對(duì)當(dāng)前線程是可知的,沒有什么線程同步問題.壞處是線程執(zhí)行時(shí)間不可控,如果當(dāng)前線程一直不退出,程序就可能崩潰.
? 使用搶占式調(diào)度的多線程系統(tǒng),每個(gè)線程將有系統(tǒng)來分配執(zhí)行時(shí)間,線程的切換不有線程本身來決定.線程的執(zhí)行時(shí)間是系統(tǒng)可控的,也不會(huì)有一個(gè)線程導(dǎo)致整個(gè)進(jìn)程阻塞的問題.這也是Java使用的線程調(diào)度方式.
? Java線程調(diào)度是系統(tǒng)完成的,但可以設(shè)置線程優(yōu)先級(jí)來讓系統(tǒng)偏向于某些線程分配時(shí)間.當(dāng)兩個(gè)線程都處于ready狀態(tài)時(shí),優(yōu)先級(jí)越高的線程越容易被系統(tǒng)線程選擇執(zhí)行.
? 不改線程優(yōu)先級(jí)并不是很靠譜,一是雖然現(xiàn)在很多操作系統(tǒng)都提供線程優(yōu)先級(jí)的概念,但并不一定能和Java線程的優(yōu)先級(jí)一一對(duì)應(yīng).二是優(yōu)先級(jí)可能會(huì)被操作系統(tǒng)自行改變.
(3). 狀態(tài)轉(zhuǎn)換
? Java語言定義了5中線程狀態(tài):
- 新建:創(chuàng)建后未啟動(dòng)的線程
- 運(yùn)行:包括操作系統(tǒng)線程狀態(tài)中的Running和Ready,也就是說,這個(gè)線程可能正在執(zhí)行也可能正在等著CPU給它分配時(shí)間.
- 無限期等待:處于這種狀態(tài)的線程不會(huì)被分配cpu執(zhí)行時(shí)間,需要等待被其他線程顯式喚醒.以下方法可以達(dá)到這種狀態(tài):
- 沒有設(shè)置時(shí)間的Object.wait()方法
- 沒有設(shè)置時(shí)間的Thread.join()方法
- LockSupport.park()方法
- 限期等待:無需其他線程顯式喚醒,一定時(shí)間過后由系統(tǒng)自動(dòng)喚醒.以下方法可以達(dá)到這種狀態(tài):
- 設(shè)置時(shí)間的Object.wait()方法
- 設(shè)置時(shí)間的Thread.join()方法
- Thread.sleep()方法
- LockSupport.parkNanos()方法
- LockSupport.parkUntil()方法
- 阻塞:線程被阻塞了,與等待狀態(tài)的區(qū)別是,阻塞狀態(tài)只是在等待獲取到一個(gè)排他鎖,這個(gè)時(shí)間講在另外一個(gè)線層放棄這個(gè)鎖時(shí)發(fā)生
- 結(jié)束:已終止線程的狀態(tài),線程已經(jīng)結(jié)束執(zhí)行.