Java并發(fā)的機(jī)制的背后是Java虛擬機(jī)(JVM)的工作機(jī)制,本文從幾個關(guān)于并發(fā)和多線程的疑問開始,引出Java內(nèi)存區(qū)域的介紹被廓,希望能幫助大家更好的理解Java并發(fā)機(jī)制。
1. 線程創(chuàng)建和切換的代價(jià)——JVM的內(nèi)存區(qū)域
在《從任務(wù)到線程:Java結(jié)構(gòu)化并發(fā)應(yīng)用程序》和《嘗試Java加鎖新思路:原子變量和非阻塞同步算法》中椿每,曾經(jīng)分別介紹過伊者,創(chuàng)建線程和線程間切換對于性能和資源的消耗是不容忽視的,無限制地創(chuàng)建線程會消耗過多的內(nèi)存資源并不可取间护,過多的線程間上下文切換也會降低多線程并發(fā)的性能亦渗。但是線程創(chuàng)建和切換的代價(jià)到底是怎么產(chǎn)生的呢?這就不得不提到Java的運(yùn)行時(shí)數(shù)據(jù)區(qū)了汁尺。
1.1 JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)
根據(jù)《Java虛擬機(jī)規(guī)范》 JVM 將所管理的內(nèi)存區(qū)域劃分為 Method Area(方法區(qū))法精,Heap(堆),Program Counter Register(程序計(jì)數(shù)器), VM Stack(虛擬機(jī)棧)痴突,Native Method Stack (本地方法棧)搂蜓,其中Method Area和Heap是線程共享的,VM Stack辽装,Native Method Stack 和Program Counter Register是線程隔離的帮碰。
如果讀者對于JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)不是很了解,由于篇幅有限拾积,請參看JVM初探 -JVM內(nèi)存模型和淺析Java虛擬機(jī)結(jié)構(gòu)與機(jī)制殉挽,這里不再展開說明丰涉,只提供一份思維導(dǎo)圖,幫助大家梳理內(nèi)容:
概括地說來斯碌,JVM初始運(yùn)行的時(shí)候都會分配好Method Area(方法區(qū))和Heap(堆)一死,而JVM 每遇到一個線程,就為其分配一個Program Counter Register(程序計(jì)數(shù)器), VM Stack(虛擬機(jī)棧)和Native Method Stack (本地方法棧)傻唾,當(dāng)線程終止時(shí)投慈,三者(虛擬機(jī)棧,本地方法棧和程序計(jì)數(shù)器)所占用的內(nèi)存空間也會被釋放掉冠骄。
1.2 線程創(chuàng)建的內(nèi)存代價(jià)
每當(dāng)有線程被創(chuàng)建的時(shí)候伪煤,JVM就需要為其在內(nèi)存中分配虛擬機(jī)棧和本地方法棧來記錄調(diào)用方法的內(nèi)容,分配程序計(jì)數(shù)器記錄指令執(zhí)行的位置猴抹,這樣的內(nèi)存消耗就是創(chuàng)建線程的內(nèi)存代價(jià)带族。
內(nèi)存作為有限的資源锁荔,如果JVM創(chuàng)建了過多的線程蟀给,必然會導(dǎo)致資源的耗盡。因此阳堕,使用Executor架構(gòu)復(fù)用線程可以節(jié)省內(nèi)存資源跋理,是十分必要的。
1.3 線程切換的性能代價(jià)
JVM的并發(fā)是通過線程切換并分配時(shí)間片執(zhí)行來實(shí)現(xiàn)的. 在任何一個時(shí)刻, 一個處理器內(nèi)核只會執(zhí)行一條線程中的指令恬总。因此, 為了線程切換后能恢復(fù)到正確的執(zhí)行位置, JVM需要先保存被掛起線程的上下文環(huán)境:將線程執(zhí)行位置保存到程序計(jì)數(shù)器中前普,將調(diào)用方法的信息保存在棧中;同時(shí)將待執(zhí)行線程的程序計(jì)數(shù)器和棧中的信息寫入到處理器中壹堰,完成線程的上下文切換拭卿。維護(hù)線程隔離數(shù)據(jù)區(qū)中的內(nèi)容在處理器中的導(dǎo)入導(dǎo)出,就是線程切換的性能代價(jià)贱纠。
控制線程上下文切換次數(shù)的方法有很多:
- 使用基于CAS的非擁塞算法峻厚,詳見嘗試Java加鎖新思路:原子變量和非阻塞同步算法;
- 無鎖并發(fā)編程谆焊,盡量使用線程封閉(ThreadLocal)或者不變量惠桃,而不是用鎖,詳見對象共享:Java并發(fā)環(huán)境中的煩心事辖试;
- 使用線程池+等待隊(duì)列的方式辜王,控制線程數(shù)目,詳見從任務(wù)到線程:Java結(jié)構(gòu)化并發(fā)應(yīng)用程序罐孝;
2. 對象訪問的定位 VS 內(nèi)存可見性
根據(jù)JVM運(yùn)行時(shí)的內(nèi)存模式呐馆,在一個方法中使用某個變量,JVM要現(xiàn)在棧中找到該變量的引用莲兢,然后通過引用找到該對象在堆中保存到實(shí)例數(shù)據(jù)汹来,如下圖所示:
但是這個過程為什么會出現(xiàn)多線程間的內(nèi)存可見性的問題呢辫继?
這個過程從單個線程的角度來說,是沒有問題的俗慈,線程可以找到自己需要的變量姑宽,但是得到的變量不一定是最新的,這是由于主存和緩存內(nèi)容不一致造成的闺阱。
JVM不光有內(nèi)存區(qū)域的劃分炮车,還有內(nèi)存模式(JMM)來控制Java線程見的通信,其決定了一個線程的共享變量的寫入合適對另一個線程可見酣溃。在Java中通過多線程機(jī)制使得多個任務(wù)同時(shí)執(zhí)行處理瘦穆,所有的線程共享JVM內(nèi)存區(qū)域主存(main memory),而每個線程又單獨(dú)的有自己的工作內(nèi)存赊豌,當(dāng)線程與內(nèi)存區(qū)域進(jìn)行交互時(shí)扛或,數(shù)據(jù)從主存拷貝到工作內(nèi)存,進(jìn)而交由線程處理(操作碼+操作數(shù))碘饼,數(shù)據(jù)寫入時(shí)熙兔,先被寫入到工作緩存中,JMM選擇合適的時(shí)機(jī)將其同步到主存中艾恼,以此提高訪問效率住涉。
工作緩存其實(shí)是一個抽象的概念,Java的內(nèi)存分區(qū)中并沒有專門的一塊區(qū)域叫線程的工作緩存钠绍,其實(shí)際上是緩存舆声、對讀寫緩沖區(qū)、寄存器以及其他硬件和編譯器優(yōu)化的統(tǒng)稱柳爽。
因此虛擬棧到Java堆中尋找實(shí)例數(shù)據(jù)和JMM并不矛盾媳握,二者是JVM不通角度的闡述。
3. 對象的創(chuàng)建——靜態(tài)區(qū)域加載的多線程安全性
為了保證多線程安全性磷脯,一些必要的操作都需要加鎖來保證其原子性和可見性蛾找,但是類中靜態(tài)區(qū)的代碼是不需要加鎖就能保證多線程安全性。這是因?yàn)槭裁茨兀?/p>
答案在于JVM類加載過程的保護(hù)機(jī)制争拐。和普通類的實(shí)例被分配在Java堆上不同腋粥,類的靜態(tài)屬性都保存在方法區(qū),其創(chuàng)建收到類加載過程的影響架曹。
類的加載過程大題分為:加載(Loading),連接(Linking)绑雄,初始化(Initialization)展辞,使用(Using)和卸載(UnLoading)五個步驟。這里和靜態(tài)屬性有關(guān)的主要是連接和初始化:在連接步驟的準(zhǔn)備階段万牺,靜態(tài)屬性會分配內(nèi)存罗珍;在初始化步驟洽腺,JVM會生成一個特別的方法——<clinit>
方法來專門執(zhí)行靜態(tài)代碼塊和靜態(tài)變量初始化。
<clinit>
方法執(zhí)行的過程中覆旱,JVM會對類加鎖蘸朋,保證在多線程環(huán)境下,只有一個線程能成功執(zhí)行<clinit>
方法扣唱,其他線程都將被擁塞藕坯,并且<clinit>
方法只能被執(zhí)行一次,被擁塞的線程被喚醒之后也不會再去執(zhí)行<clinit>
方法噪沙。如果類有繼承關(guān)系炼彪,JVM還會保證父類的<clinit>
方法將先于子類的<clinit>
方法執(zhí)行。
由此可見正歼,靜態(tài)代碼塊的多線程安全性是由JVM為其加鎖實(shí)現(xiàn)的辐马,這也是延遲初始化占位類模式的安全性基礎(chǔ),詳見《從Java內(nèi)存模型角度理解安全初始化》局义。