硬件效率與一致性:讓計算機并發(fā)執(zhí)行若干任務與更充分利用計算機處理器的效能之間的因果關系看起來順利成章矛紫,實際上它們之間的關系并沒有想象中的那么簡單,由于計算機的存儲設備與處理器的運算速度有幾個數(shù)量級的差距沼溜,所以現(xiàn)代計算機系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運算速度的高速緩存來作為內(nèi)存與處理器之間的緩沖,基于高速緩存的存儲交互很好的解決 了處理器與內(nèi)存的速度矛盾,但是又引入一個新的問題:緩存一致性锣尉。在多處理器系統(tǒng)中,每個處理器都有自己的高速緩存决采,而他們又共享同一主內(nèi)存自沧,為了解決緩存一致性問題,需要各個處理器訪問緩存時都遵循一些協(xié)議树瞭,在讀寫時根據(jù)協(xié)議來進行操作拇厢。處理器、高速緩存晒喷、主內(nèi)存之間的交互關系如下圖所示:
Java內(nèi)存模型
Java虛擬機規(guī)范試圖定義一種Java內(nèi)存模型(Java Memory Model,JMM)來屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異凉敲。這個模型必須足夠嚴謹衣盾,才能讓Java并發(fā)內(nèi)存訪問 不會產(chǎn)生歧義,但是又必須足夠?qū)捤梢ィ沟锰摂M機的實現(xiàn)有足夠的自由空間去利用硬件的各種特性势决。
主內(nèi)存與工作內(nèi)存
Java內(nèi)存模型的主要目的是定義程序中各個變量的訪問規(guī)則,即在虛擬機中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細節(jié)蓝撇。此處的變量與java編程中的所說的變量有所區(qū)別徽龟,它包括了實例字段、靜態(tài)字段和構成數(shù)組對象的元素唉地,但不包括局部變量和方法參數(shù),因為后者是線程私有的传透,不會被共享耘沼,自然就不會存在競爭問題。
java內(nèi)存模型規(guī)定了所有的變量都存儲在主內(nèi)存中朱盐,每條線程還有自己的工作內(nèi)存群嗤,線程工作內(nèi)存保存了被該線程使用的變量的主內(nèi)存副本拷貝,線程對變量所有操作都必須在工作內(nèi)存中進行兵琳,不能直接讀寫主內(nèi)存中的變量狂秘,不同線程之間無法直接訪問對方工作內(nèi)存中的變量骇径,線程間變量值的傳遞均需通過主內(nèi)存來完成,線程者春、主內(nèi)存破衔、工作內(nèi)存三者交互關系如圖所示:
主內(nèi)存工作內(nèi)存與java內(nèi)存區(qū)域中的java堆、棧钱烟、方法區(qū)不是同一個層次的內(nèi)存劃分晰筛,兩者基本沒有關系,從變量拴袭、主內(nèi)存读第、工作內(nèi)存的定義來看,主內(nèi)存主要對應于java堆中的對象實例數(shù)據(jù)部分拥刻、而工作內(nèi)存則對應于虛擬機棧中的部分區(qū)域怜瞒。
在單線程中不會出現(xiàn)線程安全問題,而在多線程編程中般哼,有可能會出現(xiàn)同時訪問同一個資源的情況吴汪,這種資源可以是各種類型的的資源:一個變量、一個對象逝她、一個文件浇坐、一個數(shù)據(jù)庫表等,而當多個線程同時訪問同一個資源的時候黔宛,就會存在一個問題:所以主內(nèi)存可以是一個變量近刘、一個對象、一個文件臀晃、一個數(shù)據(jù)庫表觉渴。
主內(nèi)存與工作內(nèi)存交互操作:主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存徽惋,如果從工作內(nèi)存同步回主內(nèi)存的實現(xiàn)細節(jié)案淋。java內(nèi)存模型定義了八種操作來完成內(nèi)存交互操作,虛擬機實現(xiàn)時必須保證每一種操作都是原子的险绘,不可再分的踢京,
lock:鎖定把變量標示為一條線程獨占;
unlock:解鎖把一個處于鎖定狀態(tài)的變量釋放出倆宦棺,其它線程才可以鎖定瓣距;
read:讀取把變量從主內(nèi)存?zhèn)鬏數(shù)焦ぷ鲀?nèi)存中。
load:載入把read操作從主內(nèi)存中得到的變量值放入到工作內(nèi)存的變量副本中代咸。
use:使用作用工作內(nèi)存變量蹈丸;
assign:賦值作用于工作內(nèi)存變量,把執(zhí)行引擎接收到的值賦值工作內(nèi)存變量。
store:存儲作用工作內(nèi)存變量逻杖,把一個變量值傳送到主內(nèi)容中以便write操作使用奋岁。
write:寫入作用于主內(nèi)存變量,把store操作從工作內(nèi)存得到的變量存放到主內(nèi)存變量中荸百。
除了以上八種操作來完成內(nèi)存交互操作闻伶,還規(guī)定了8種基本操作必須滿足一些規(guī)則。
volatile關鍵字
當變量被定位volatile之后管搪,它將具備兩種特性:
可見性:保證此變量對所有線程的可見性虾攻,可見性指當一條線程修改了這個變量的值之后,新值對于其他線程來說是可以立即得知的更鲁。而普通變量的值在線程之間傳遞需要通過主內(nèi)存來完成霎箍。
指令重排序:第二個語義就是禁止指令重排序。volatile關鍵字禁止指令重排序有兩層意思:
1)當程序執(zhí)行到volatile變量的讀操作或者寫操作時澡为,在其前面的操作的更改肯定全部已經(jīng)進行漂坏,且結果已經(jīng)對后面的操作可見;在其后面的操作肯定還沒有進行媒至;
2)在進行指令優(yōu)化時顶别,不能將在對volatile變量訪問的語句放在其后面執(zhí)行,也不能把volatile變量后面的語句放到其前面執(zhí)行拒啰。
問題一:volatile如何保證可見性驯绎?
即一個線程修改了值,另外一個線程可以立馬可見谋旦,java內(nèi)存模型對volatile變量的操作指定如何規(guī)則來保證可見性:
(1)每次使用變量前都必須先從主內(nèi)存刷新最新的值剩失,用于保證能看見其他線程對變量v所做的修改;(這個寫回內(nèi)存的操作會導致在其它CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效)
(2)在工作內(nèi)存中册着,每次修改變量都必須立即同步回主內(nèi)存中拴孤,用于保證其他線程可以看到自己對變量的修改;
問題二:volatile如何禁止指令重排序甲捏?
觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的字節(jié)碼發(fā)現(xiàn)演熟,加入volatile關鍵字時,會多出一個lock前綴指令司顿,lock前綴指令實際上相當于一個內(nèi)存屏障芒粹,在volatile修飾的變量進行賦值后,會添加lock操作(內(nèi)存屏障)大溜,內(nèi)存屏障會提供3個功能:
1)它確保指令重排序時不會把其后面的指令排到內(nèi)存屏障之前的位置化漆,也不會把前面的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時猎提,在它前面的操作已經(jīng)全部完成;
2)它會強制將對緩存的修改操作立即寫入主存;
3)如果是寫操作锨苏,它會導致其他CPU中對應的緩存行無效疙教。
簡單例子:
//x、y為非volatile變量
//flag為volatile變量
x = 2; //語句1
y = 0; //語句2
flag = true; //語句3
x = 4; //語句4
y = -1; //語句5
由于flag變量為volatile變量伞租,那么在進行指令重排序的過程的時候贞谓,不會將語句3放到語句1、語句2前面葵诈,也不會講語句3放到語句4裸弦、語句5后面。但是要注意語句1和語句2的順序作喘、語句4和語句5的順序是不作任何保證的理疙。并且volatile關鍵字能保證,執(zhí)行到語句3時泞坦,語句1和語句2必定是執(zhí)行完畢了的窖贤,且語句1和語句2的執(zhí)行結果對語句3、語句4贰锁、語句5是可見的赃梧。
新例子:
//線程1:
context = loadContext(); //語句1
inited = true; //語句2
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
指令重排序會導致語句2會在語句1之前執(zhí)行,那么久可能導致context還沒被初始化豌熄,而線程2中就使用未初始化的context去進行操作授嘀,導致程序出錯。這里如果用volatile關鍵字對inited變量進行修飾锣险,就不會出現(xiàn)這種問題了蹄皱,因為當執(zhí)行到語句2時,必定能保證context已經(jīng)初始化完畢囱持。
原子性夯接、可見性于有序性
Java內(nèi)存模型相關操作和規(guī)則主要圍繞并發(fā)過程中如何處理原子性、可見性纷妆、有序性三個特征來建立的盔几。
(1)原子性:在Java中,對基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性操作掩幢,即這些操作是不可被中斷的逊拍,要么執(zhí)行,要么不執(zhí)行际邻。
x = 10;是原子操作芯丧;
y = x;它先要去讀取x的值,再將x的值寫入工作內(nèi)存世曾。
x++和x = x+1:讀取x的值缨恒,進行加1操作,寫入新的值。
Java內(nèi)存模型只保證了基本讀取和賦值是原子性操作骗露,如果要實現(xiàn)更大范圍操作的原子性岭佳,可以通過synchronized和Lock來實現(xiàn)。由于synchronized和Lock能夠保證任一時刻只有一個線程執(zhí)行該代碼塊萧锉,那么自然就不存在原子性問題了珊随,從而保證了原子性。synchronized同步代碼塊之間的操作具備原子性柿隙;并發(fā)包中的原子操作類(Atomic系列)java.util.concurrent.atomic包叶洞,在該包中提供了許多基于CAS實現(xiàn)的原子操作類。
(2)可見性:
Java提供了volatile關鍵字來保證可見性禀崖。一個共享變量被volatile修飾時衩辟,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時帆焕,它會去內(nèi)存中讀取新值惭婿。synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執(zhí)行同步代碼,并且在釋放鎖之前會將對變量的修改刷新到主存當中叶雹。因此可以保證可見性财饥。
(3)有序性
在Java內(nèi)存模型中,允許編譯器和處理器對指令進行重排序折晦,但是重排序過程不會影響到單線程程序的執(zhí)行钥星,卻會影響到多線程并發(fā)執(zhí)行的正確性。在本線程內(nèi)觀察满着,所有操作都是有序的谦炒,在一個線程橫縱觀察另外一個線程,所有的操作都是無序的风喇,前半句指線程內(nèi)表現(xiàn)為串行的語義宁改,后半句指指令重排序的現(xiàn)象。volatile和synchronized關鍵字可以保證線程間操作的有序性魂莫,volatile關鍵字包含禁止指令重排序还蹲;synchronized由一個變量在用一個時刻只允許一個線程對其進行l(wèi)ock操作獲得,這條規(guī)則決定了持有同一個鎖的兩個同步塊執(zhí)行串行進入耙考。synchronized在需要原子性谜喊、可見性、有序性時都可以作為一種解決方案倦始。
先行發(fā)生原則
先行發(fā)生原則(happens-before)與有序性有關系:java內(nèi)存模型所有有序性僅靠volatile和synchronized來完成會很繁瑣斗遏,先行發(fā)生原則是判斷數(shù)據(jù)是否存在競爭、線程是否安全的主要依據(jù)鞋邑。在衡量并發(fā)安全問題時不要受到時間順訊的干擾诵次,一些以先行發(fā)生原則為準账蓉。
Java內(nèi)存模型總結
Jvm內(nèi)存模型內(nèi)存模型與并發(fā)有關,定義程序中各個變量的訪問規(guī)則來保證并發(fā)安全問題逾一,主要圍繞原子性剔猿、可見性、有序性來建立的嬉荆。
原子性:volatile保證不了原子性,通過synchronized酷含、Lock鄙早、原子操作類(AtomicInteger,基于CAS實現(xiàn))這三種方式可以保證原子性椅亚。
可見性:volatile限番、synchronized、final呀舔、Lock保證可見性弥虐。
有序性:volatile(指令重排序)、synchronized可以保證有序性媚赖,和先行發(fā)生原則也是來解決有序性問題霜瘪。
Java與線程
實現(xiàn)線程的三種方式:使用內(nèi)核線程實現(xiàn)、使用用戶線程實現(xiàn)惧磺、使用用戶線程加輕量級進程實現(xiàn)颖对。
Java線程實現(xiàn):Java線程模型是基于操作系統(tǒng)原生線程模型來實現(xiàn),操作系統(tǒng)支持什么樣的線程模型磨隘,在很大程度上決定了Java虛擬機的線程是如何映射的缤底。
Java線程調(diào)度:線程調(diào)度是指系統(tǒng)為線程分配處理器使用權的過程,調(diào)度方式有兩種:
(1)協(xié)同式調(diào)度:線程的執(zhí)行時間由線程本身來控制番捂,線程把自己執(zhí)行完之后个唧,要主動通知系統(tǒng)切換到另外一個線程。缺點:線程執(zhí)行時間不可控设预,如果一個線程有問題徙歼,一直不停止,那么程序就會阻塞在哪里絮缅。就是自己不執(zhí)行完一直執(zhí)行鲁沥。
(2)搶占式調(diào)度: 每個線程由系統(tǒng)來分配執(zhí)行時間,線程的切換不是由線程本身決定(可以通過Thread.yield()讓出執(zhí)行時間)耕魄,在這種調(diào)度方式下画恰,線程執(zhí)行時間是系統(tǒng)可控的,也就不會有一個線程導致整個進程阻塞的問題吸奴,Android系統(tǒng)的線程調(diào)度使用搶占式調(diào)度允扇。JVM 采用的是搶占式調(diào)度模型缠局,也就是先讓優(yōu)先級高的線程占用 CPU,如果線程的優(yōu)先級都一樣考润,那就隨機選擇一個線程狭园,并讓該線程占用 CPU。
雖然java線程調(diào)度是系統(tǒng)自動完成的糊治,但是我們還是建議系統(tǒng)給某些線程多分配一點執(zhí)行時間唱矛,這個可以通過線程優(yōu)先級來完成。如果我們的程序想干預線程的調(diào)度過程井辜,最簡單的辦法就是給每個線程設定一個優(yōu)先級绎谦。線程優(yōu)先級并不是太靠譜,原因是java線程是通過映射到系統(tǒng)的原生線程上來實現(xiàn)的粥脚,所以線程調(diào)度最重還是取決于操作系統(tǒng)窃肠,雖然很多操作系統(tǒng)都提供線程優(yōu)先級概念,但是并不見得能與java線程優(yōu)先級一一對應刷允。