線程安全總結(二)

站內很多人都問我,所謂線程的“工作內存”到底是個什麼東西?有的人認爲是線程的棧,其實這種理解是不正確的。看看JLS(java語言規範)對線程工作 內存的描述,線程的working memory只是cpu的寄存器和高速緩存的抽象描述。

 

      可能 很多人都覺得莫名其妙,說JVM的內存模型,怎麼會扯到cpu上去呢?在此,我認爲很有必要闡述下,免 得很多人看得不明不白的。先拋開java虛擬機不談,我們都知道,現在的計算機,cpu在計算的時候,並不總是從內存讀取數據,它的數據讀取順序優先級 是:寄存器-高速緩存-內存。線程耗費的是CPU,線程計算的時候,原始的數據來自內存,在計算過程中,有些數據可能被頻繁讀取,這些數據被存儲在寄存器 和高速緩存中,當線程計算完後,這些緩存的數據在適當的時候應該寫回內存。當個多個線程同時讀寫某個內存數據時,就會產生多線程併發問題,涉及到三個特 性:原子性,有序性,可見性。在《線程安全總結》這篇文章中,爲了理解方便,我把原子性和有序性統一叫做“多線程執行有序性”。支持多線程的平臺都會面臨 這種問題,運行在多線程平臺上支持多線程的語言應該提供解決該問題的方案。

 

       那麼,我們看看JVM,JVM是一個虛擬 的計算機,它也會面臨多線程併發問題,java程序運行在java虛擬機平臺上,java程序員不可能直接去控制底層線程對寄存器高速緩存內存之間的同 步,那麼java從語法層面,應該給開發人員提供一種解決方案,這個方案就是諸如synchronized, volatile,鎖機制(如同步塊,就緒隊 列,阻塞隊列)等等。這些方案只是語法層面的,但我們要從本質上去理解它,不能僅僅知道一個 synchronized 可以保證同步就完了。   在這裏我說的是jvm的內存模型,是動態的,面向多線程併發的,沿襲JSL的“working memory”的說法,只是不想牽扯到太多底層細節,因爲 《線程安全總結》這篇文章意在說明怎樣從語法層面去理解java的線程同步,知道各個關鍵字的使用場 景。

 

      今天有人問我,那java的線程不是有棧嗎?難道棧不是工作內存嗎?工作內存這四個字得放到具體的場景 中描述,方能體現它具體的意義,在描述JVM的線程同步時,工作內存指的是寄存器和告訴緩存的抽象描述,具體請自行參閱JLS。上面講的都是動態的內存模 型,甚至已經超越了JVM的範圍,那麼JVM的內存靜態存儲是怎麼劃分的?今天還有人問我,jvm的內存模型不是有eden區嗎?也不見你提起。我跟他 說,這是兩個角度去看的,甚至是兩個不同的範圍,動態的線程同步的內存模型,涵蓋了cpu,寄存器,高速緩存,內存;JVM的靜態內存儲模型只是一種對內 存的物理劃分而已,它只侷限在內存,而且只侷限在JVM的內存。那些什麼線程棧,eden區都僅僅在JVM內存。

 

      說說JVM的線程棧和有個朋友反覆跟我糾結的eden區吧。JVM的內存,被劃分了很多的區域:

1.程序計數器


      每一個Java線程都有一個程序計數器來用於保存程序執行到當前方法的哪一個指令。


2.線程棧


      線程的每個方法被執行的時候,都會同時創建一個幀(Frame)用於存儲本地變量表、操作棧、動態鏈接、方法出入口等信息。每一個方法的調用至完成,就意味着一個幀在VM棧中的入棧至出棧的過程。如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;如果VM棧可以動態擴展(VM Spec中允許固定長度的VM棧),當擴展時無法申請到足夠內存則拋出OutOfMemoryError異常。
3.本地方法棧


4.堆


       每個線程的棧都是該線程私有的,堆則是所有線程共享的。當我們new一個對象時,該對象就被分配到了堆中。但是堆,並不是一個簡單的概念,堆區又劃分了很多區域,爲什麼堆劃分成這麼多區域,這是爲了JVM的內存垃圾收集,似乎越扯越遠了,扯到垃圾收集了,現在的jvm的gc都是按代收集,堆區大致被分爲三大塊:新生代,舊生代,持久代(虛擬的);新生代又分爲eden區,s0區,s1區。新建一個對象時,基本小的對象,生命週期短的對象都會放在新生代的eden區中,eden區滿時,有一個小範圍的gc(minor gc),整個新生代滿時,會有一個大範圍的gc(major gc),將新生代裏的部分對象轉到舊生代裏。


5.方法區 


       其實就是永久代(Permanent Generation),方法區中存放了每個Class的結構信息,包括常量池、字段描述、方法描述等等。VM Space描述中對這個區域的限制非常寬鬆,除了和Java堆一樣不需要連續的內存,也可以選擇固定大小或者可擴展外,甚至可以選擇不實現垃圾收集。相對來說,垃圾收集行爲在這個區域是相對比較少發生的,但並不是某些描述那樣永久代不會發生GC(至 少對當前主流的商業JVM實現來說是如此),這裏的GC主要是對常量池的回收和對類的卸載,雖然回收的“成績”一般也比較差強人意,尤其是類卸載,條件相當苛刻。


6.常量池


      Class文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量表(constant_pool table),用於存放編譯期已可知的常量,這部分內容將在類加載後進入方法區(永久代)存放。但是Java語言並不要求常量一定只有編譯期預置入Class的常量表的內容才能進入方法區常量池,運行期間也可將新內容放入常量池(最典型的String.intern()方法)。

 

      關於垃圾收集,在此不多說,流到垃圾收集那一章再詳細說吧。關於java的同步,其實還有基於CPU原語的比較並交換的非阻塞算法(CAS),不過這個在java的併發包裏已經實現了很多,因此關於這點,就留到java併發包那一章介紹吧。後面我會專門寫一篇文章,JVM內存與垃圾收集。

發佈了56 篇原創文章 · 獲贊 7 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章