偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

深入理解Java虛擬機(jī)(高效并發(fā))

云計(jì)算 虛擬化
高效并發(fā)是 JVM 系列的最后一篇,本篇主要介紹虛擬機(jī)如何實(shí)現(xiàn)多線程、多線程間如何共享和競(jìng)爭(zhēng)數(shù)據(jù)以及共享和競(jìng)爭(zhēng)數(shù)據(jù)帶來(lái)的問(wèn)題及解決方案。

 高效并發(fā)是 JVM 系列的最后一篇,本篇主要介紹虛擬機(jī)如何實(shí)現(xiàn)多線程、多線程間如何共享和競(jìng)爭(zhēng)數(shù)據(jù)以及共享和競(jìng)爭(zhēng)數(shù)據(jù)帶來(lái)的問(wèn)題及解決方案。

[[271680]]

一. Java 內(nèi)存模型與線程

讓計(jì)算機(jī)同時(shí)執(zhí)行多個(gè)任務(wù),不只是因?yàn)樘幚砥鞯男阅芨訌?qiáng)大了,更重要是因?yàn)橛?jì)算機(jī)的運(yùn)算速度和它的存儲(chǔ)以及通信子系統(tǒng)速度差距太大,大量的時(shí)間都花費(fèi)在磁盤 I/O 、網(wǎng)絡(luò)通信和數(shù)據(jù)庫(kù)訪問(wèn)上。為了不讓處理器因?yàn)榈却渌Y源而浪費(fèi)處理器的資源與時(shí)間,我們就必須采用讓計(jì)算機(jī)同時(shí)執(zhí)行多任務(wù)的方式去充分利用處理器的性能;同時(shí)也是為了應(yīng)對(duì)服務(wù)端高并發(fā)的需求。而 Java 內(nèi)存模型的設(shè)計(jì)和線程的存在正是為了更好、更高效的實(shí)現(xiàn)多任務(wù)。

1.硬件與效率的一致性

計(jì)算機(jī)中絕大多數(shù)的任務(wù)都不可能只靠處理器計(jì)算就能完成,處理器至少要和內(nèi)存交互,如讀取數(shù)據(jù)、存儲(chǔ)結(jié)果等等,這個(gè) I/O 操作是很難消除的。由于計(jì)算器的存儲(chǔ)設(shè)備和處理器的運(yùn)算速度有幾個(gè)量級(jí)的差距,所以計(jì)算機(jī)不得不加入一層讀寫速度盡可能接近處理器運(yùn)算速度的高速緩存來(lái)作為內(nèi)存與處理器之間的緩沖:將運(yùn)算需要用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能快速進(jìn)行,當(dāng)運(yùn)算結(jié)束后再?gòu)木彺嫱交貎?nèi)存中,這樣處理器就無(wú)需等待緩慢的內(nèi)存讀寫了。

基于高速緩存的存儲(chǔ)交互很好的解決了處理器與內(nèi)存的速度矛盾,但是也為計(jì)算機(jī)系統(tǒng)帶來(lái)更高的復(fù)雜度,因?yàn)樗肓艘粋€(gè)新的問(wèn)題:緩存一致性。在多處理器中,每個(gè)處理器都有自己的高速緩存,而它們又共享同一主內(nèi)存。當(dāng)多個(gè)處理器的運(yùn)算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時(shí),將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致。為了解決一致性的問(wèn)題,需要各個(gè)處理器的訪問(wèn)緩存時(shí)都遵循一些協(xié)議,在讀寫時(shí)要根據(jù)協(xié)議來(lái)進(jìn)行操作。

 

除了增加高速緩存外,為了使處理器內(nèi)部的運(yùn)算單元能盡量被充分利用,處理器可能會(huì)對(duì)輸入的代碼進(jìn)行亂序執(zhí)行優(yōu)化,處理器會(huì)在計(jì)算之后將亂序執(zhí)行的結(jié)果重組,保證該結(jié)果與順序執(zhí)行的結(jié)果一致,但不保證程序中各個(gè)語(yǔ)句計(jì)算的先后順序與輸入代碼中的順序一致,因此,如果存在一個(gè)計(jì)算任務(wù)依賴另一個(gè)計(jì)算任務(wù)的中間結(jié)果,那么其順序性并不能靠代碼的先后順序來(lái)保證。與處理器的亂象執(zhí)行優(yōu)化類似,JIT 編譯器中也有類似的指令重排優(yōu)化。

2.Java 內(nèi)存模型

Java 虛擬機(jī)規(guī)范中定義了 Java 內(nèi)存模型,用來(lái)屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異,以實(shí)現(xiàn)讓 Java 程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問(wèn)效果。像 C/C++ 這類語(yǔ)言直接使用了物理硬件和操作系統(tǒng)的內(nèi)存模型,因此會(huì)由于不同平臺(tái)上內(nèi)存模型的差異,需要針對(duì)不同平臺(tái)來(lái)編寫代碼。

主內(nèi)存與工作內(nèi)存

Java 內(nèi)存模型的主要目標(biāo)是定義程序中各個(gè)變量的訪問(wèn)規(guī)則,即在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中讀取變量這樣的底層細(xì)節(jié)。這里說(shuō)的變量和 Java 代碼中的變量有所區(qū)別,它包括了實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素,但不包括變量和方法參數(shù),因?yàn)楹笳呤蔷€程私有的,不會(huì)被共享。為了獲得較好的執(zhí)行性能,Java 內(nèi)存模型并沒(méi)有限制執(zhí)行引擎使用處理器的特定寄存器或緩存來(lái)和主內(nèi)存進(jìn)行交互,也沒(méi)有限制 JIT 編譯器進(jìn)行代碼執(zhí)行順序這類優(yōu)化措施。

Java 內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存,每條線程都有自己?jiǎn)为?dú)的工作內(nèi)存,線程的工作內(nèi)存中保存了被該線程使用到的變量的主內(nèi)存的副本拷貝,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存,線程間變量值的傳遞均需要通過(guò)主內(nèi)存來(lái)完成。

 

內(nèi)存間交互操作

關(guān)于主內(nèi)存與工作內(nèi)存間具體的交互協(xié)議,即一個(gè)變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步回主內(nèi)存之類的細(xì)節(jié),Java 內(nèi)存模型定義了以下 8 種操作來(lái)完成,虛擬機(jī)實(shí)現(xiàn)時(shí)必須保證下面的每一種操作都是原子的、不可再分的。

這 8 種操作分別是:lock(鎖定)、unlock(解鎖)、read(讀取)、load(載入)、use(使用)、assign(賦值)、store(存儲(chǔ))、write(寫入)。

對(duì) volatile 型變量的特殊規(guī)則

volatile 是 Java 虛擬機(jī)提供的最輕量級(jí)的同步機(jī)制。當(dāng)一個(gè)變量被定義為 volatile 后,它將具備兩種特性:

第一是保證此變量對(duì)所有線程的可見(jiàn)性,這里的「可見(jiàn)性」是指當(dāng)一條線程修改了這個(gè)變量的值,新值對(duì)于其他線程來(lái)說(shuō)是可以立即得知的。普通變量則做不到這一點(diǎn),需要通過(guò)主內(nèi)存來(lái)在線程間傳遞數(shù)據(jù)。比如,線程 A 修改了一個(gè)普通的變量值,然后向主內(nèi)存進(jìn)行回寫,另一條線程 B 在 A 線程回寫完成之后再?gòu)闹鲀?nèi)存進(jìn)行讀寫操作,新變量值才會(huì)對(duì)線程 B 可見(jiàn)。

第二是禁止指令重排優(yōu)化。普通變量?jī)H僅會(huì)保證方法的執(zhí)行過(guò)程中所有依賴賦值結(jié)果的地方能夠獲取到正確的結(jié)果,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致。因?yàn)樵谝粋€(gè)線程的方法執(zhí)行過(guò)程中無(wú)法感知到這點(diǎn),這也就是 Java 內(nèi)存模型中描述的所謂的「線程內(nèi)表現(xiàn)為串行的語(yǔ)義」。

對(duì) long 和 double 型變量的特殊規(guī)則

Java 內(nèi)存模型要求 lock、unlock、read、load、assign、use、store、writer 這 8 個(gè)操作都具有原子性,但對(duì)于 64 位數(shù)據(jù)類型(long 和 double),在模型中特別定義了一條相對(duì)寬松的規(guī)定:允許虛擬機(jī)將沒(méi)有被 volatile 修飾的 64 位數(shù)據(jù)的讀寫操作劃分為兩次 32 位的操作來(lái)進(jìn)行,即允許虛擬機(jī)實(shí)現(xiàn)選擇可以不保證 64 位數(shù)據(jù)類型的 load、store、read 和 write 這 4 個(gè)操作的原子性。這點(diǎn)就是所謂的 long 和 double 的非原子協(xié)定。

如果有多個(gè)線程共享一個(gè)未聲明為 volatile 的 long 或 double 類型的變量,并且同時(shí)對(duì)它們進(jìn)行讀取和修改操作,那么某些線程可能會(huì)讀取到一個(gè)錯(cuò)誤的值。好在這種情況非常罕見(jiàn),主流商業(yè)虛擬機(jī)中也都把對(duì) long 和 double 的操作視為原子性,因此在實(shí)際開(kāi)發(fā)中無(wú)需使用 volatile 來(lái)修飾變量。

原子性、可見(jiàn)性和有序性

Java 內(nèi)存模型是圍繞著在并發(fā)過(guò)程中如何處理原子性、可見(jiàn)性和有序性 3 個(gè)特質(zhì)來(lái)建立的。

原子性(Atomicity)

由 Java 內(nèi)存模型來(lái)直接保證原子性變量操作,包括 read、load、assign、use、store 和 write ,我們大致可以認(rèn)為基本數(shù)據(jù)類型的訪問(wèn)讀寫是具備原子性的。如果應(yīng)用場(chǎng)景需要一個(gè)更大范圍的原子性保證,Java 內(nèi)存模型還提供了 lock 和 unlock 操作來(lái)滿足這種需求,盡管虛擬機(jī)未把 lock 和 unlock 操作直接開(kāi)放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令 monitorenter 和 monitorexit 來(lái)隱式地使用這兩個(gè)操作,這兩個(gè)字節(jié)碼指令反映到 Java 代碼中就是 synchronized 關(guān)鍵字,因此被 synchronize 修飾的方法或代碼塊之間的操作是具備原子性的。

可見(jiàn)性(Visibility)

可見(jiàn)性是指當(dāng)一個(gè)線程修改了共享變量的值,其它線程能夠立即得知這個(gè)修改。Java 內(nèi)存模型是通過(guò)在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式來(lái)實(shí)現(xiàn)可見(jiàn)性的,無(wú)論是普通變量還是 volatile 變量都是如此,普通變量與 volatile 變量的區(qū)別是, volatile 的規(guī)則保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新。因此,可以說(shuō) volatile 保證了多線程操作變量的可見(jiàn)性,而普通變量則不能保證這一點(diǎn)。除了 volatile 外,Java 還有兩個(gè)關(guān)鍵字 synchronized 和 final 。synchronized 同步塊的可見(jiàn)性是由「對(duì)一個(gè)變量執(zhí)行 unlock 操作前,必須先把此變量同步回主內(nèi)存中(執(zhí)行 store、write 操作)」這條規(guī)則獲得的;final 的可見(jiàn)性是指“:被 final 修飾的字段在構(gòu)造器中一旦初始化完成,并且構(gòu)造器沒(méi)有「this」的引用傳遞出去,那在其他線程中就能看見(jiàn) final 字段的值。

有序性(Ordering)

Java 程序中天然的有序性可以總結(jié)為:如果在本線程內(nèi),所有操作都是有序的;如果在一個(gè)線程中觀察另一個(gè)線程,所有的操作都是無(wú)序的。前半句是指「線程內(nèi)表現(xiàn)為串行的語(yǔ)義」,后半句是指「指令重排序」現(xiàn)象和「工作內(nèi)存和主內(nèi)存同步延遲」現(xiàn)象。Java 語(yǔ)言提供了 volatile 和 synchronized 兩個(gè)關(guān)鍵字來(lái)保證線程之間操作的有序性,volatile 關(guān)鍵字本身就包含了禁止指令重排的語(yǔ)義,而 synchronized 則是由「一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行 lock 操作」這條規(guī)則獲得的,這條規(guī)則決定了持有同一個(gè)鎖的兩個(gè)同步塊只能串行的進(jìn)入。

先行發(fā)生原則

如果 Java 內(nèi)存模型中所有的有序性都僅僅靠 volatile 和 synchronized 來(lái)保證,那么有一些操作就會(huì)變得很繁瑣,但是我們?cè)诰帉? Java 并發(fā)代碼的時(shí)候并沒(méi)有感覺(jué)到這一點(diǎn),這是因?yàn)?Java 語(yǔ)言中有一個(gè)「先行發(fā)生」(happens-before)原則。這個(gè)原則非常重要,它是判斷數(shù)據(jù)是否存在競(jìng)爭(zhēng)、線程是否安全的主要依據(jù),依靠這個(gè)原則,我們可以通過(guò)幾條規(guī)則一攬子解決并發(fā)環(huán)境下兩個(gè)操作之間是否可能存在沖突的所有問(wèn)題。

先行發(fā)生是 Java 內(nèi)存模型中定義的兩項(xiàng)操作之間的偏序關(guān)系,如果說(shuō)操作 A 先行發(fā)生于操作 B,其實(shí)就是說(shuō)在發(fā)生操作 B 之前,操作 A 產(chǎn)生的影響能被操作 B 觀察到,「影響」包括修改了內(nèi)存中共享變量的值、發(fā)送了消息、調(diào)用了方法等。

Java 內(nèi)存模型下有一些天然的先行發(fā)生關(guān)系,這些先行發(fā)生關(guān)系無(wú)需任何同步器協(xié)助就已存在,可以在編碼中直接使用。如果兩個(gè)兩個(gè)操作之間的關(guān)系不在此列,并且無(wú)法從下列規(guī)則推導(dǎo)出來(lái),它們就沒(méi)有順序性保障,虛擬機(jī)就可以隨意的對(duì)它們進(jìn)行重排序。

  • 程序次序規(guī)則:在一個(gè)線程內(nèi),按照程序代碼順序,寫在前面的代碼先行發(fā)生寫在后面的代碼。準(zhǔn)確的講,應(yīng)該是控制流順序而不是程序代碼順序,因?yàn)橐紤]分支、循環(huán)等結(jié)構(gòu);
  • 管程鎖定規(guī)則:一個(gè) unlock 操作先行發(fā)生于后面對(duì)于同一個(gè)鎖的 lock 操作;
  • volatile 變量規(guī)則:對(duì)一個(gè) volatile 變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作,理解了這個(gè)原則我們就能理解為什么 DCL 單例模式中為什么要用 volatile 來(lái)標(biāo)識(shí)實(shí)例對(duì)象了;
  • 線程啟動(dòng)規(guī)則:線程的 start() 方法先行發(fā)生于此線程的所有其它動(dòng)作;
  • 線程終止規(guī)則:線程中所有的操作都先行發(fā)生于對(duì)此線程的終止檢測(cè);
  • 程序中斷規(guī)則:對(duì)線程 interrupt() 的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷時(shí)間的發(fā)生;
  • 對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成先行發(fā)生于它的 finalize() 的開(kāi)始;
  • 傳遞性:操作 A 先行發(fā)生于 B,B 先行發(fā)生于 C,那么 A 就先行發(fā)生于 C。

3.Java 與線程

談?wù)?Java 中的并發(fā),通常都是和多線程相關(guān)的。這一小節(jié)我們就講講 Java 線程在虛擬機(jī)中的實(shí)現(xiàn)。

線程的實(shí)現(xiàn)

主流的操作系統(tǒng)都提供了線程實(shí)現(xiàn),Java 語(yǔ)言則提供了在不同硬件和操作系統(tǒng)平臺(tái)下對(duì)線程操作的統(tǒng)一處理,每個(gè)已經(jīng)執(zhí)行 start() 且還未結(jié)束的 Thread 類的實(shí)例就代表了一個(gè)線程。Thread 類所有關(guān)鍵方法都是 Native 的。Java API 中,一個(gè) Native 方法往往意味著這個(gè)方法沒(méi)有使用或者無(wú)法使用平臺(tái)無(wú)關(guān)的手段來(lái)實(shí)現(xiàn)(當(dāng)然也可能是為了執(zhí)行效率而使用 Native 方法,不過(guò),通常最高效率的手段就是平臺(tái)相關(guān)的手段)。

實(shí)現(xiàn)線程主要有 3 種方式:使用內(nèi)核線程實(shí)現(xiàn)、使用用戶線程實(shí)現(xiàn)、使用用戶線程加輕量級(jí)進(jìn)程混合實(shí)現(xiàn)。

Java 線程的實(shí)現(xiàn)

Java 線程在 JDK 1.2 之前是基于稱為「綠色線程」的用戶線程實(shí)現(xiàn)的。而在 JDK 1.2 中,線程模型替換為基于操作系統(tǒng)原生線程模型來(lái)實(shí)現(xiàn)。因此,在目前的 JDK 版本中,操作系統(tǒng)支持怎樣的線程模型,在很大程度上決定了 Java 虛擬機(jī)的線程是怎樣映射的,這點(diǎn)在不同的平臺(tái)上沒(méi)有辦法達(dá)成一致,虛擬機(jī)規(guī)范中也沒(méi)有限定 Java 線程需要使用哪種線程模型來(lái)實(shí)現(xiàn)。線程模型只對(duì)線程的并發(fā)規(guī)模和操作成本產(chǎn)生影響,對(duì) Java 程序的編碼和運(yùn)行過(guò)程來(lái)說(shuō),這些差異都透明的。

4.Java 線程調(diào)度

線程調(diào)度是指系統(tǒng)為線程分配處理器使用權(quán)的過(guò)程,主要調(diào)度方式有兩種,分別是協(xié)同式線程調(diào)度和搶占式線程調(diào)度。

協(xié)同式線程調(diào)度

如果是使用協(xié)同式調(diào)度的多線程系統(tǒng),線程的執(zhí)行時(shí)間由線程本身來(lái)控制,線程把自己的工作執(zhí)行完之后,要主動(dòng)通知系統(tǒng)切換到另外一個(gè)線程上。協(xié)同式多線程的最大好處是實(shí)現(xiàn)簡(jiǎn)單,而且由于線程要把自己的事情做完后才會(huì)進(jìn)行線程切換,切換操作對(duì)線程自己是可知的,所有沒(méi)有線程同步的問(wèn)題。但是它的壞處也很明顯:線程執(zhí)行時(shí)間不可控,甚至如果一個(gè)線程編寫有問(wèn)題,一直不告訴操作系統(tǒng)進(jìn)行線程切換,那么程序就會(huì)一直阻塞在那里。很久以前的 Windows 3.x 系統(tǒng)就是使用協(xié)同式來(lái)實(shí)現(xiàn)對(duì)進(jìn)程多任務(wù),相當(dāng)不穩(wěn)定,一個(gè)進(jìn)程堅(jiān)持不讓出 CPU 執(zhí)行時(shí)間就可能導(dǎo)致整個(gè)系統(tǒng)崩潰。

搶占式線程調(diào)度

如果是使用搶占式調(diào)度的多線程系統(tǒng),那么每個(gè)線程將由系統(tǒng)來(lái)分配執(zhí)行時(shí)間,線程的切換不由線程本身來(lái)決定。在這種實(shí)現(xiàn)線程調(diào)度的方式下,線程的執(zhí)行實(shí)現(xiàn)是系統(tǒng)可控的,也不會(huì)有一個(gè)線程導(dǎo)致整個(gè)進(jìn)程阻塞的問(wèn)題,Java 使用的線程調(diào)度方式就是搶占式的。和前面所說(shuō)的 Windows 3.x 的例子相對(duì),在 Windows 9x/NT 內(nèi)核中就是使用搶占式來(lái)實(shí)現(xiàn)多進(jìn)程的,當(dāng)一個(gè)進(jìn)程出了問(wèn)題,我們還可以使用任務(wù)管理器把這個(gè)進(jìn)程「殺掉」,而不至于導(dǎo)致系統(tǒng)崩潰。

5.狀態(tài)轉(zhuǎn)換

Java 語(yǔ)言定義了 5 種線程狀態(tài),在任意一個(gè)時(shí)間點(diǎn),一個(gè)線程只能有且只有其中一種狀態(tài),它們分別是:

  • 新建(New):創(chuàng)建后尚未啟動(dòng)的線程處于這種狀態(tài);
  • 運(yùn)行(Runnable):Runnable 包括了操作系統(tǒng)線程狀態(tài)中的 Running 和 Ready,也就是處于此狀態(tài)的線程有可能正在執(zhí)行,也有可能正在等待著 CPU 為它分配執(zhí)行時(shí)間;
  • 無(wú)限期等待(Waiting):處于這種狀態(tài)的線程不會(huì)被分配 CPU 執(zhí)行時(shí)間,它們要等待被其它線程顯式地喚醒;以下三種方法會(huì)讓線程進(jìn)入無(wú)限期等待狀態(tài):
    • 沒(méi)有設(shè)置 TimeOut 參數(shù)的 Object.wait();
    • 沒(méi)有設(shè)置 TimeOut 參數(shù)的 Thread.join();
    • LockSupport.park()。
  • 限期等待(Timed Waiting):處于這種狀態(tài)的線程也不會(huì)被分配 CPU 執(zhí)行時(shí)間,不過(guò)無(wú)需等待被其它線程顯式地喚醒,在一定時(shí)間之后它們會(huì)由系統(tǒng)自動(dòng)喚醒;以下方法會(huì)讓線程進(jìn)入限期等待狀態(tài):
    • Thread.sleep();
    • 設(shè)置了 TimeOut 參數(shù)的 Object.wait();
    • 設(shè)置了 TimeOut 參數(shù)的 Thread.join();
    • LockSupport.parkNanos();
    • LockSupport.parkUntil()。
  • 阻塞(Blocked):線程被阻塞了,「阻塞狀態(tài)」和「等待狀態(tài)」的區(qū)別是:「阻塞狀態(tài)」在等待著獲取一個(gè)排他鎖,這個(gè)事件將在另一個(gè)線程放棄這個(gè)鎖的時(shí)候發(fā)生;而「等待狀態(tài)」則是在等待一段時(shí)間,或者喚醒動(dòng)作的發(fā)送。在程序等待進(jìn)入同步區(qū)域時(shí),線程將進(jìn)入這種狀態(tài);
  • 結(jié)束(Terminated):線程已經(jīng)結(jié)束執(zhí)行。

上述 5 中狀態(tài)遇到特定事件發(fā)生的時(shí)候?qū)?huì)互相轉(zhuǎn)換,如下圖:

 

二、線程安全與鎖優(yōu)化

本文的主題是高效并發(fā),但高效的前提是首先要保證并發(fā)的正確性和安全性,所以這一小節(jié)我們先從如何保證線程并發(fā)安全說(shuō)起。

1.Java 線程安全

那么什么是線程安全呢?可以簡(jiǎn)單的理解為多線程對(duì)同一塊內(nèi)存區(qū)域操作時(shí),內(nèi)存值的變化是可預(yù)期的,不會(huì)因?yàn)槎嗑€程對(duì)同一塊內(nèi)存區(qū)域的操作和訪問(wèn)導(dǎo)致內(nèi)存中存儲(chǔ)的值出現(xiàn)不可控的問(wèn)題。

Java 語(yǔ)言中的線程安全

如果我們不把線程安全定義成一個(gè)非此即彼的概念(要么線程絕對(duì)安全,要么線程絕對(duì)不安全),那么我們可以根據(jù)線程安全的程度由強(qiáng)至弱依次分為如下五檔:

  • 不可變;
  • 絕對(duì)線程安全;
  • 相對(duì)線程安全;
  • 線程兼容;
  • 線程對(duì)立。

線程安全的實(shí)現(xiàn)方法

雖然線程安全與否與編碼實(shí)現(xiàn)有著莫大的關(guān)系,但虛擬機(jī)提供的同步和鎖機(jī)制也起到了非常重要的作用。下面我們就來(lái)看看虛擬機(jī)層面是如何保證線程安全的。

同步互斥

互斥同步是常見(jiàn)的一種并發(fā)正確性保障的手段。同步是指在多個(gè)線程并發(fā)訪問(wèn)共享數(shù)據(jù)時(shí),保證共享數(shù)據(jù)在同一時(shí)間只被一個(gè)線程使用。而互斥是實(shí)現(xiàn)同步的一種手段。Java 中最基本的互斥同步手段就是 synchronized 關(guān)鍵字,synchronized 關(guān)鍵字在經(jīng)過(guò)編譯之后,會(huì)在同步塊的前后分別形成 monitorenter 和 monitorexit 這兩個(gè)字節(jié)碼指令,這兩個(gè)字節(jié)碼都需要一個(gè) reference 類型的參數(shù)來(lái)指明要鎖定和解鎖的對(duì)象。如果 Java 程序中的 synchronized 明確指明了對(duì)象參數(shù),那就是這個(gè)對(duì)象的 reference;如果沒(méi)有,那就根據(jù) synchronized 修飾的是實(shí)例方法還是類方法,去取對(duì)應(yīng)的對(duì)象實(shí)例或 class 對(duì)象來(lái)作為鎖對(duì)象。

根據(jù)虛擬機(jī)規(guī)范的要求,在執(zhí)行 monitorenter 指令時(shí),首先要嘗試獲取對(duì)象的鎖。如果這個(gè)對(duì)象沒(méi)被鎖定,或者當(dāng)前線程已經(jīng)擁有了那個(gè)對(duì)象的鎖,就把鎖的計(jì)數(shù)器加 1;相應(yīng)的,在執(zhí)行monitorexit 指令時(shí)將鎖計(jì)數(shù)器減 1,當(dāng)鎖計(jì)數(shù)器為 0 時(shí),鎖就被釋放。如果獲取鎖對(duì)象失敗,當(dāng)前線程就要阻塞等待,直到對(duì)象鎖被另一個(gè)線程釋放為止。

另外要說(shuō)明的一點(diǎn)是,同步塊在已進(jìn)入的線程執(zhí)行完之前,會(huì)阻塞后面其它線程的進(jìn)入。由于 Java 線程是映射到操作系統(tǒng)原生線程之上的,如果要阻塞或者喚醒一個(gè)線程,都需要操作系統(tǒng)來(lái)幫忙完成,這就需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài),線程狀態(tài)轉(zhuǎn)換需要耗費(fèi)很多的處理器時(shí)間。對(duì)于簡(jiǎn)單的同步塊(如被 synchronized 修飾的 getter() 和 setter() 方法),狀態(tài)轉(zhuǎn)換消耗的時(shí)間可能比用戶代碼消耗的時(shí)間還要長(zhǎng)。所以 synchronized 是 Java 中一個(gè)重量級(jí)的操作,因此我們只有在必要的情況下才應(yīng)該使用它。當(dāng)然虛擬機(jī)本身也會(huì)做相應(yīng)的優(yōu)化,比如在操作系統(tǒng)阻塞線程前加入一段自旋等待過(guò)程,避免頻繁的用戶態(tài)到內(nèi)核態(tài)的轉(zhuǎn)換過(guò)程。這一點(diǎn)我們?cè)诮榻B鎖優(yōu)化的時(shí)候再細(xì)聊。

非阻塞同步

互斥同步最大的問(wèn)題就是進(jìn)行線程阻塞和喚醒所帶來(lái)的性能問(wèn)題,因此這種同步也成為阻塞同步。從處理問(wèn)題的方式上來(lái)說(shuō),互斥同步是一種悲觀的并發(fā)策略,認(rèn)為只要不去做正確的同步措施(例如加鎖),就肯定會(huì)出問(wèn)題,無(wú)論共享數(shù)據(jù)是否會(huì)出現(xiàn)競(jìng)爭(zhēng),它都要進(jìn)行加鎖(當(dāng)然虛擬機(jī)也會(huì)優(yōu)化掉一些不必要的鎖)。隨著硬件指令集的發(fā)展,我們有了另外一個(gè)選擇:基于沖突檢查的樂(lè)觀并發(fā)策略。通俗的說(shuō),就是先進(jìn)行操作,如果沒(méi)有其他線程競(jìng)爭(zhēng),那操作就成功了;如果共享數(shù)據(jù)有其它線程競(jìng)爭(zhēng),產(chǎn)生了沖突,就采取其它的補(bǔ)救措施,這種樂(lè)觀的并發(fā)策略的許多實(shí)現(xiàn)都不需要把線程掛起,因此這種同步操作稱為非阻塞同步。

前面之所以說(shuō)需要硬件指令集的發(fā)展,是因?yàn)槲覀冃枰僮骱蜎_突檢測(cè)這兩個(gè)步驟具備原子性。

這個(gè)原子性靠什么來(lái)保證呢?如果這里再使用互斥同步來(lái)保證原子性就失去意義了,所以我們只能靠硬件來(lái)完成這件事,保證一個(gè)從語(yǔ)義上看起來(lái)需要多次操作的行為只通過(guò)一條處理器指令就能完成,這類指令常用的有:

  • 測(cè)試并設(shè)置(Test-and-Set)
  • 獲取并增加(Fetch-and-Increment)
  • 交換(Swap)
  • 比較并交換(Compare-and-Swap,簡(jiǎn)稱 CAS)
  • 加載鏈接/條件存儲(chǔ)(Load-Linked/Store-Conditional,簡(jiǎn)稱 LL/SC)

前三條是之前的處理器指令集里就有的,后兩條是新增的。

CAS 指令需要 3 個(gè)操作數(shù),分別是內(nèi)存位置(在 Java 中可以簡(jiǎn)單理解為變量的內(nèi)存地址,用 V 表示)、舊的預(yù)期值(用 A 表示)和新值(用 B 表示)。CAS 執(zhí)行指令時(shí),當(dāng)且僅當(dāng) V 符合舊預(yù)期值 A 時(shí),處理器用新值 B 更新 V 的值,否則他就不執(zhí)行更新,但是無(wú)論是否更新了 V 的值,都會(huì)返回 V 的舊值,上述的處理過(guò)程是一個(gè)原子操作。

在 JDK 1.5 之后,Java 程序中才可以使用 CAS 操作,該操作由 sun.misc.Unsafe 類里的 compareAndSwapInt() 和 compareAndSwapLong() 等幾個(gè)方法包裝提供,虛擬機(jī)在內(nèi)部對(duì)這些方法做了特殊處理,即時(shí)編譯出來(lái)的結(jié)果就是一條平臺(tái)相關(guān)的處理器 CAS 指令,沒(méi)有方法的調(diào)用過(guò)程,或者可以認(rèn)為是無(wú)條件內(nèi)聯(lián)進(jìn)去了。

由于 Unsafe 類不是提供給用戶程序調(diào)用的類,因此如果不用反射,我們只能通過(guò)其他的 Java API 來(lái)間接使用,比如 J.U.C 包里的整數(shù)原子類,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 類的 CAS 操作。

盡管 CAS 看起來(lái)很美,但是這種操作卻無(wú)法覆蓋互斥同步的所有場(chǎng)景,并且 CAS 從語(yǔ)義上來(lái)說(shuō)并不是完美的。如果一個(gè)變量 V 初次讀取的時(shí)候是 A 值,并且在準(zhǔn)備賦值的時(shí)候檢查它仍然是 A 值,那我們就能說(shuō)它的值沒(méi)有被其他線程修改過(guò)嗎?如果在這段時(shí)間內(nèi)曾經(jīng)被改為了 B,后來(lái)又被改回為 A,那 CAS 操作就會(huì)認(rèn)為它從來(lái)沒(méi)有被改變過(guò)。這個(gè)漏洞稱為 CAS 操作的「ABA」問(wèn)題。

為了解決「ABA」問(wèn)題,J.U.C 包提供了一個(gè)帶有標(biāo)記的原子引用類 AtomicStamoedReference,它可以通過(guò)控制變量值的版本來(lái)保證 CAS 的正確性。不過(guò)這個(gè)類比較「雞肋」,大部分情況下 ABA 問(wèn)題不會(huì)影響程序并發(fā)的正確性,如果需要解決 ABA 問(wèn)題,改用傳統(tǒng)的互斥同步可能會(huì)比原子類更高效。

無(wú)同步方案

要保證線程安全不一定要進(jìn)行同步,如果一個(gè)方法本來(lái)就不涉及共享數(shù)據(jù),那它自然無(wú)需任何同步措施,因此會(huì)有一些代碼天生就是線程安全的,其中就包括下面要說(shuō)的可重入代碼和線程本地存儲(chǔ)。

可重入代碼(Reentrant Code):也叫純代碼,可以在代碼執(zhí)行的任何時(shí)候中斷它,轉(zhuǎn)而去執(zhí)行另一端代碼(包括遞歸調(diào)用自己),而在重新獲得控制權(quán)后,原來(lái)的程序不會(huì)出現(xiàn)任何錯(cuò)誤??芍厝氪a有一些共同特征,例如不依賴存儲(chǔ)在堆上的數(shù)據(jù)和公用的系統(tǒng)資源,用到的狀態(tài)量都由參數(shù)傳入、不調(diào)用非可重入的方法等。如果一個(gè)方法的返回結(jié)果可以預(yù)測(cè),只要輸入相同,就能返回相同的輸出,那它就是可重入代碼,當(dāng)然也就是線程安全的。

線程本地存儲(chǔ)(Thread Local Storage):也就是說(shuō)這個(gè)數(shù)據(jù)是線程獨(dú)有的,ThreadLocal 就是用來(lái)實(shí)現(xiàn)線程本地存儲(chǔ)的。

2.鎖優(yōu)化

HotSpot 虛擬機(jī)開(kāi)發(fā)團(tuán)隊(duì)花費(fèi)了很大的精力實(shí)現(xiàn)了各種鎖優(yōu)化,比如自旋鎖與自適應(yīng)自旋、鎖消除、鎖粗化、輕量級(jí)鎖、偏向鎖等。

自旋鎖與自適應(yīng)自旋

自旋鎖前面我們?cè)诹幕コ馔降臅r(shí)候就提到過(guò),互斥同步對(duì)性能最大的影響就是阻塞的實(shí)現(xiàn),掛起線程和恢復(fù)線程都涉及到了用戶態(tài)到內(nèi)核態(tài)的轉(zhuǎn)換,這種狀態(tài)的轉(zhuǎn)換會(huì)給系統(tǒng)并發(fā)性能帶來(lái)很大的壓力。但是大多數(shù)場(chǎng)景下,共享數(shù)據(jù)的鎖定狀態(tài)只會(huì)持續(xù)很短的一段時(shí)間,為了這短暫的時(shí)間去掛起和恢復(fù)線程顯得不那么劃算。如果物理機(jī)有一個(gè)以上的處理器,能讓兩個(gè)或以上的線程同時(shí)并行處理,我們就可以讓后面請(qǐng)求鎖的那個(gè)線程「稍等一下」,但是不放棄處理器的執(zhí)行時(shí)間,看看持有鎖的線程是否很快就會(huì)釋放鎖。為了讓線程等待,我們只需要執(zhí)行一個(gè)空轉(zhuǎn)的循環(huán)(自旋),這就是所謂的自旋鎖。

自旋等待雖然避免了線程切換的開(kāi)銷,但是它要占用處理器的時(shí)間。如果鎖被占用的時(shí)間很短,那么自旋等待的效果當(dāng)然很好;反之,如果鎖被占用的時(shí)間很長(zhǎng),那么自旋的線程就會(huì)白白消耗處理器資源,反而形成負(fù)優(yōu)化。所以自旋等待必須有個(gè)限度,但是這個(gè)限度如果設(shè)置一個(gè)固定值并不是最有選擇,因此虛擬機(jī)開(kāi)發(fā)團(tuán)隊(duì)設(shè)計(jì)了自適應(yīng)自旋鎖,讓自旋等待的時(shí)間不再固定,而是由前一次在同一個(gè)鎖上自旋的時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定。如果在同一個(gè)鎖對(duì)象上,自旋等待剛剛成功獲得過(guò)鎖,并且持有鎖的線程正在運(yùn)行,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也有可能會(huì)成功,會(huì)將自旋等待的時(shí)間延長(zhǎng)。如果對(duì)于某個(gè)鎖,自旋等待很少成功獲得過(guò),那在以后要獲取這個(gè)鎖的時(shí)候就會(huì)放棄自旋。有了自適應(yīng)自旋,隨著程序運(yùn)行和性能監(jiān)控信息的不斷完善,虛擬機(jī)對(duì)程序鎖的狀況預(yù)測(cè)就會(huì)越來(lái)越準(zhǔn)確。

鎖消除

即時(shí)編譯器在運(yùn)行時(shí),對(duì)一些代碼上要求同步,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖就會(huì)進(jìn)行鎖消除。所消除的主要判定依據(jù)來(lái)源于逃逸分析的數(shù)據(jù)支持,如果判定一段代碼中,堆上的所有數(shù)據(jù)都不會(huì)逃逸出去從而被其它線程訪問(wèn)到,那就可以把它們當(dāng)做棧上數(shù)據(jù)對(duì)待,認(rèn)為它們是線程私有的,同步加鎖自然就沒(méi)必要了。

鎖粗化

我們?cè)诰幋a時(shí),總是推薦將同步塊的作用范圍限制到最小,只在共享數(shù)據(jù)的實(shí)際作用域中才進(jìn)行同步,這樣是為了使得需要的同步操作數(shù)量盡可能變小,如果存在競(jìng)爭(zhēng),那等待鎖的線程也能盡快拿到鎖。通常,這樣做是正確的,但是如果一系列的連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體中,那即使沒(méi)有線程競(jìng)爭(zhēng),頻繁的進(jìn)行互斥同步也會(huì)導(dǎo)致不必要的性能損耗。那加鎖出現(xiàn)在循環(huán)體中來(lái)舉例,虛擬機(jī)遇到這種情況,就會(huì)把加鎖同步的范圍擴(kuò)展(粗化)到循環(huán)體外,這樣只要加鎖一次就可以了,這就是鎖粗化。

關(guān)于輕量級(jí)鎖和偏向鎖這里就不再介紹,如果大家有興趣可以留言反饋,我在單獨(dú)發(fā)文介紹。

責(zé)任編輯:武曉燕 來(lái)源: BaronTalk
相關(guān)推薦

2012-11-14 09:57:46

JavaJava虛擬機(jī)JVM

2024-03-29 11:42:21

Java虛擬機(jī)

2024-04-03 13:49:00

Java虛擬機(jī)方法區(qū)

2016-09-01 12:37:13

OpenStack虛擬機(jī)Metadata

2024-03-26 07:30:07

Java虛擬機(jī)源文件

2023-09-22 23:00:11

Java虛擬機(jī)

2019-12-31 10:45:30

JavaVisualVM高并發(fā)

2024-04-10 07:40:45

Java虛擬機(jī)內(nèi)存

2024-12-31 09:00:12

Java線程狀態(tài)

2020-12-11 07:32:45

編程ThreadLocalJava

2020-11-13 08:42:24

Synchronize

2022-10-12 07:53:46

并發(fā)編程同步工具

2011-12-28 13:24:47

JavaJVM

2011-12-28 13:38:00

JavaJVM

2018-03-22 18:30:22

數(shù)據(jù)庫(kù)MySQL并發(fā)控制

2021-09-18 06:56:01

JavaCAS機(jī)制

2016-12-08 15:36:59

HashMap數(shù)據(jù)結(jié)構(gòu)hash函數(shù)

2010-06-01 15:25:27

JavaCLASSPATH

2020-07-21 08:26:08

SpringSecurity過(guò)濾器

2024-05-24 14:35:49

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)