Java內存模型-JMM簡介

Java內存模型-JMM簡介

BangQ IT哈哈
本來準備把內存模型單獨放到某一篇文章的某個章節裏面講解,後來查閱了國外不少文檔才發現其實JVM內存模型的內容還蠻多的,因此直接做爲一個章節的基礎知識來說解,可能該章節概念的東西比較多。一個開發Java的開發者,一旦瞭解了JVM內存模型就可以更加深刻地瞭解該語言的語言特性,可能這個章節更多的是概念,沒有太多代碼實例,因此但願讀者諒解,本文儘可能涵蓋全部Java語言能夠碰到的和內存相關的內容,一樣也會提到一些和內存相關的計算機語言的一些知識,爲草案。由於平時開發的時候沒有特殊狀況不會進行內存管理,因此有可能有筆誤的地方比較多,我用的是Windows平臺,因此本文涉及到的與操做系統相關的只是僅僅侷限於Windows平臺。不只僅如此,這一個章節牽涉到的多線程和另一些內容並無講到,這裏主要是結合JVM內部特性把本章節做爲核心的概念性章節來說解,這樣方便初學者深刻以及完全理解Java語言)java

1.JMM簡介

  i.內存模型概述

  Java平臺自動集成了線程以及多處理器技術,這種集成程度比Java之前誕生的計算機語言要厲害不少,該語言針對多種異構平臺的平臺獨立性而使用的多線程技術支持也是具備開拓性的一面,有時候在開發Java同步和線程安全要求很嚴格的程序時,每每容易混淆的一個概念就是內存模型。究竟什麼是內存模型?內存模型描述了程序中各個變量(實例域、靜態域和數組元素)之間的關係,以及在實際計算機系統中將變量存儲到內存和從內存中取出變量這樣的底層細節,對象最終是存儲在內存裏面的,這點沒有錯,可是編譯器、運行庫、處理器或者系統緩存能夠有特權在變量指定內存位置存儲或者取出變量的值。【JMM】(Java Memory Model的縮寫)容許編譯器和緩存以數據在處理器特定的緩存(或寄存器)和主存之間移動的次序擁有重要的特權,除非程序員使用了final或synchronized明確請求了某些可見性的保證。程序員

  1)JSR133:

  在Java語言規範裏面指出了JMM是一個比較開拓性的嘗試,這種嘗試視圖定義一個一致的、跨平臺的內存模型,可是它有一些比較細微並且很重要的缺點。其實Java語言裏面比較容易混淆的關鍵字主要是synchronized和volatile,也由於這樣在開發過程當中每每開發者會忽略掉這些規則,這也使得編寫同步代碼比較困難。
  JSR133自己的目的是爲了修復本來JMM的一些缺陷而提出的,其自己的制定目標有如下幾個:編程

  • 保留目前JVM的安全保證,以進行類型的安全檢查:
  • 提供(out-of-thin-air safety)無中生有安全性,這樣「正確同步的」應該被正式並且直觀地定義
  • 程序員要有信心開發多線程程序,固然沒有其餘辦法使得併發程序變得很容易開發,可是該規範的發佈主要目標是爲了減輕程序員理解內存模型中的一些細節負擔
  • 提供大範圍的流行硬件體系結構上的高性能JVM實現,如今的處理器在它們的內存模型上有着很大的不一樣,JMM應該可以適合於實際的儘量多的體系結構而不以性能爲代價,這也是Java跨平臺型設計的基礎
  • 提供一個同步的習慣用法,以容許發佈一個對象使他不用同步就可見,這種狀況又稱爲初始化安全(initialization safety)的新的安全保證
  • 對現有代碼應該只有最小限度的影響
      2)同步、異步【這裏僅僅指概念上的理解,不牽涉到計算機底層基礎的一些操做】:
      在系統開發過程,常常會遇到這幾個基本概念,不管是網絡通信、對象之間的消息通信仍是Web開發人員經常使用的Http請求都會遇到這樣幾個概念,常常有人提到Ajax是異步通信方式,那麼究竟怎樣的方式是這樣的概念描述呢?
      同步:同步就是在發出一個功能調用的時候,在沒有獲得響應以前,該調用就不返回,按照這樣的定義,其實大部分程序的執行都是同步調用的,通常狀況下,在描述同步和異步操做的時候,主要是指代須要其餘部件協做處理或者須要協做響應的一些任務處理。好比有一個線程A,在A執行的過程當中,可能須要B提供一些相關的執行數據,固然觸發B響應的就是A向B發送一個請求或者說對B進行一個調用操做,若是A在執行該操做的時候是同步的方式,那麼A就會停留在這個位置等待B給一個響應消息,在B沒有任何響應消息回來的時候,A不能作其餘事情,只能等待,那麼這樣的狀況,A的操做就是一個同步的簡單說明。
      異步:異步就是在發出一個功能調用的時候,不須要等待響應,繼續進行它該作的事情,一旦獲得響應了事後給予必定的處理,可是不影響正常的處理過程的一種方式。好比有一個線程A,在A執行的過程當中,一樣須要B提供一些相關數據或者操做,當A向B發送一個請求或者對B進行調用操做事後,A不須要繼續等待,而是執行A本身應該作的事情,一旦B有了響應事後會通知A,A接受到該異步請求的響應的時候會進行相關的處理,這種狀況下A的操做就是一個簡單的異步操做。

      3)可見性、可排序性

      Java內存模型的兩個關鍵概念:可見性(Visibility)和可排序性(Ordering)
      開發過多線程程序的程序員都明白,synchronized關鍵字強制實施一個線程之間的互斥鎖(相互排斥),該互斥鎖防止每次有多個線程進入一個給定監控器所保護的同步語句塊,也就是說在該狀況下,執行程序代碼所獨有的某些內存是獨佔模式,其餘的線程是不能針對它執行過程所獨佔的內存進行訪問的,這種狀況稱爲該內存不可見。可是在該模型的同步模式中,還有另一個方面:JMM中指出了,JVM在處理該強制實施的時候能夠提供一些內存的可見規則,在該規則裏面,它確保當存在一個同步塊時,緩存被更新,當輸入一個同步塊時,緩存失效。所以在JVM內部提供給定監控器保護的同步塊之中,一個線程所寫入的值對於其他全部的執行由同一個監控器保護的同步塊線程來講是可見的,這就是一個簡單的可見性的描述。這種機器保證編譯器不會把指令從一個同步塊的內部移到外部,雖然有時候它會把指令由外部移動到內部。JMM在缺省狀況下不作這樣的保證——只要有多個線程訪問相同變量時必須使用同步。簡單總結:
      可見性就是在多核或者多線程運行過程當中內存的一種共享模式,在JMM模型裏面,經過併發線程修改變量值的時候,必須將線程變量同步回主存事後,其餘線程纔可能訪問到。
      【*:簡單講,內存的可見性使內存資源能夠共享,當一個線程執行的時候它所佔有的內存,若是它佔有的內存資源是可見的,那麼這時候其餘線程在必定規則內是能夠訪問該內存資源的,這種規則是由JMM內部定義的,這種狀況下內存的該特性稱爲其可見性。】
      可排序性提供了內存內部的訪問順序,在不一樣的程序針對不一樣的內存塊進行訪問的時候,其訪問不是無序的,好比有一個內存塊,A和B須要訪問的時候,JMM會提供必定的內存分配策略有序地分配它們使用的內存,而在內存的調用過程也會變得有序地進行,內存的折中性質能夠簡單理解爲有序性。而在Java多線程程序裏面,JMM經過Java關鍵字volatile來保證內存的有序訪問。數組

      ii.JMM結構:

      1)簡單分析:

      Java語言規範中提到過,JVM中存在一個主存區(Main Memory或Java Heap Memory),Java中全部變量都是存在主存中的,對於全部線程進行共享,而每一個線程又存在本身的工做內存(Working Memory),工做內存中保存的是主存中某些變量的拷貝,線程對全部變量的操做並不是發生在主存區,而是發生在工做內存中,而線程之間是不能直接相互訪問,變量在程序中的傳遞,是依賴主存來完成的。而在多核處理器下,大部分數據存儲在高速緩存中,若是高速緩存不通過內存的時候,也是不可見的一種表現。在Java程序中,內存自己是比較昂貴的資源,其實不只僅針對Java應用程序,對操做系統自己而言內存也屬於昂貴資源,Java程序在性能開銷過程當中有幾個比較典型的可控制的來源。synchronized和volatile關鍵字提供的內存中模型的可見性保證程序使用一個特殊的、存儲關卡(memory barrier)的指令,來刷新緩存,使緩存無效,刷新硬件的寫緩存而且延遲執行的傳遞過程,無疑該機制會對Java程序的性能產生必定的影響。
    Java內存模型-JMM簡介
      JMM的最初目的,就是爲了可以支持多線程程序設計的,每一個線程能夠認爲是和其餘線程不一樣的CPU上運行,或者對於多處理器的機器而言,該模型須要實現的就是使得每個線程就像運行在不一樣的機器、不一樣的CPU或者自己就不一樣的線程上同樣,這種狀況實際上在項目開發中是常見的。對於CPU自己而言,不能直接訪問其餘CPU的寄存器,模型必須經過某種定義規則來使得線程和線程在工做內存中進行相互調用而實現CPU自己對其餘CPU、或者說線程對其餘線程的內存中資源的訪問,而表現這種規則的運行環境通常爲運行該程序的運行宿主環境(操做系統、服務器、分佈式系統等),而程序自己表現就依賴於編寫該程序的語言特性,這裏也就是說用Java編寫的應用程序在內存管理中的實現就是遵循其部分原則,也就是前邊說起到的JMM定義了Java語言針對內存的一些的相關規則。然而,雖然設計之初是爲了可以更好支持多線程,可是該模型的應用和實現固然不侷限於多處理器,而在JVM編譯器編譯Java編寫的程序的時候以及運行期執行該程序的時候,對於單CPU的系統而言,這種規則也是有效的,這就是是上邊提到的線程和線程之間的內存策略。JMM自己在描述過程沒有提過具體的內存地址以及在實現該策略中的實現方法是由JVM的哪個環節(編譯器、處理器、緩存控制器、其餘)提供的機制來實現的,甚至針對一個開發很是熟悉的程序員,也不必定可以瞭解它內部對於類、對象、方法以及相關內容的一些具體可見的物理結構。相反,JMM定義了一個線程與主存之間的抽象關係,其實從上邊的圖能夠知道,每個線程能夠抽象成爲一個工做內存(抽象的高速緩存和寄存器),其中存儲了Java的一些值,該模型保證了Java裏面的屬性、方法、字段存在必定的數學特性,按照該特性,該模型存儲了對應的一些內容,而且針對這些內容進行了必定的序列化以及存儲排序操做,這樣使得Java對象在工做內存裏面被JVM順利調用,(固然這是比較抽象的一種解釋)既然如此,大多數JMM的規則在實現的時候,必須使得主存和工做內存之間的通訊可以得以保證,並且不能違反內存模型自己的結構,這是語言在設計之處必須考慮到的針對內存的一種設計方法。這裏須要知道的一點是,這一切的操做在Java語言裏面都是依靠Java語言自身來操做的,由於Java針對開發人員而言,內存的管理在不須要手動操做的狀況下自己存在內存的管理策略,這也是Java本身進行內存管理的一種優點。緩存

      [1]原子性(Atomicity):

      這一點說明了該模型定義的規則針對原子級別的內容存在獨立的影響,對於模型設計最初,這些規則須要說明的僅僅是最簡單的讀取和存儲單元寫入的的一些操做,這種原子級別的包括——實例、靜態變量、數組元素,只是在該規則中不包括方法中的局部變量。安全

      [2]可見性(Visibility):

      在該規則的約束下,定義了一個線程在哪一種狀況下能夠訪問另一個線程或者影響另一個線程,從JVM的操做上講包括了從另一個線程的可見區域讀取相關數據以及將數據寫入到另一個線程內。服務器

      [3]可排序性(Ordering):

      該規則將會約束任何一個違背了規則調用的線程在操做過程當中的一些順序,排序問題主要圍繞了讀取、寫入和賦值語句有關的序列。
      若是在該模型內部使用了一致的同步性的時候,這些屬性中的每個屬性都遵循比較簡單的原則:和全部同步的內存塊同樣,每一個同步塊以內的任何變化都具有了原子性以及可見性,和其餘同步方法以及同步塊遵循一樣一致的原則,並且在這樣的一個模型內,每一個同步塊不能使用同一個鎖,在整個程序的調用過程是按照編寫的程序指定指令運行的。即便某一個同步塊內的處理可能會失效,可是該問題不會影響到其餘線程的同步問題,也不會引發連環失效。簡單講:當程序運行的時候使用了一致的同步性的時候,每一個同步塊有一個獨立的空間以及獨立的同步控制器和鎖機制,而後對外按照JVM的執行指令進行數據的讀寫操做。這種狀況使得使用內存的過程變得很是嚴謹!
      若是不使用同步或者說使用同步不一致(這裏能夠理解爲異步,但不必定是異步操做),該程序執行的答案就會變得極其複雜。並且在這樣的狀況下,該內存模型處理的結果比起大多數程序員所指望的結果而言就變得十分脆弱,甚至比起JVM提供的實現都脆弱不少。由於這樣因此出現了Java針對該內存操做的最簡單的語言規範來進行必定的習慣限制,排除該狀況發生的作法在於:
      JVM線程必須依靠自身來維持對象的可見性以及對象自身應該提供相對應的操做而實現整個內存操做的三個特性,而不是僅僅依靠特定的修改對象狀態的線程來完成如此複雜的一個流程。
      *:綜上所屬,JMM在JVM內部實現的結構就變得相對複雜,固然通常的Java初學者能夠不用瞭解得這麼深刻。】**網絡

      [4]三個特性的解析(針對JMM內部):

      原子性(Atomicity):

      訪問存儲單元內的任何類型的字段的值以及對其更新操做的時候,除開long類型和double類型,其餘類型的字段是必需要保證其原子性的,這些字段也包括爲對象服務的引用。此外,該原子性規則擴展能夠延伸到基於long和double的另外兩種類型:volatile long和volatile double(volatile爲java關鍵字),沒有被volatile聲明的long類型以及double類型的字段值雖然不保證其JMM中的原子性,可是是被容許的。針對non-long/non-double的字段在表達式中使用的時候,JMM的原子性有這樣一種規則:若是你得到或者初始化該值或某一些值的時候,這些值是由其餘線程寫入,並且不是從兩個或者多個線程產生的數據在同一時間戳混合寫入的時候,該字段的原子性在JVM內部是必須獲得保證的。也就是說JMM在定義JVM原子性的時候,只要在該規則不違反的條件下,JVM自己不去理睬該數據的值是來自於什麼線程,由於這樣使得Java語言在並行運算的設計的過程當中針對多線程的原子性設計變得極其簡單,並且即便開發人員沒有考慮到最終的程序也沒有太大的影響。再次解釋一下:這裏的原子性指的是原子級別的操做,好比最小的一塊內存的讀寫操做,能夠理解爲Java語言最終編譯事後最接近內存的最底層的操做單元,這種讀寫操做的數據單元不是變量的值,而是本機碼,也就是前邊在講《Java基礎知識》中提到的由運行器解釋的時候生成的Native Code。多線程

      可見性(Visibility):

      當一個線程須要修改另外線程的可見單元的時候必須遵循如下原則:併發

  • 一個寫入線程釋放的同步鎖和緊隨其後進行讀取的讀線程的同步鎖是同一個
    從本質上講,釋放鎖操做強迫它的隸屬線程【釋放鎖的線程】從工做內存中的寫入緩存裏面刷新(專業上講這裏不該該是刷新,能夠理解爲提供)數據(flush操做),而後獲取鎖操做使得另一個線程【得到鎖的線程】直接讀取前一個線程可訪問域(也就是可見區域)的字段的值。由於該鎖內部提供了一個同步方法或者同步塊,該同步內容具備線程排他性,這樣就使得上邊兩個操做只能針對單一線程在同步內容內部進行操做,這樣就使得全部操做該內容的單一線程具備該同步內容(加鎖的同步方法或者同步塊)內的線程排他性,這種狀況的交替也能夠理解爲具備「短暫記憶效應」。
    這裏須要理解的是同步的雙重含義:使用鎖機制容許基於高層同步協議進行處理操做,這是最基本的同步;同時系統內存(不少時候這裏是指基於機器指令的底層存儲關卡memory barrier,前邊提到過)在處理同步的時候可以跨線程操做,使得線程和線程之間的數據是同步的。這樣的機制也折射出一點,並行編程相對於順序編程而言,更加相似於分佈式編程。後一種同步能夠做爲JMM機制中的方法在一個線程中運行的效果展現,注意這裏不是多個線程運行的效果展現,由於它反應了該線程願意發送或者接受的雙重操做,而且使得它本身的可見區域能夠提供給其餘線程運行或者更新,從這個角度來看,使用鎖和消息傳遞能夠視爲相互之間的變量同步,由於相對其餘線程而言,它的操做針對其餘線程也是對等的。
  • 一旦某個字段被申明爲volatile,在任何一個寫入線程在工做內存中刷新緩存的以前須要進行進一步的內存操做,也就是說針對這樣的字段進行當即刷新,能夠理解爲這種volatile不會出現通常變量的緩存操做,而讀取線程每次必須根據前一個線程的可見域裏面從新讀取該變量的值,而不是直接讀取。
  • 當某個線程第一次去訪問某個對象的域的時候,它要麼初始化該對象的值,要麼從其餘寫入線程可見域裏面去讀取該對象的值;這裏結合上邊理解,在知足某種條件下,該線程對某對象域的值的讀取是直接讀取,有些時候卻須要從新讀取。
    這裏須要當心一點的是,在併發編程裏面,很差的一個實踐就是使用一個合法引用去引用不徹底構造的對象,這種狀況在從其餘寫入線程可見域裏面進行數據讀取的時候發生頻率比較高。從編程角度上講,在構造函數裏面開啓一個新的線程是有必定的風險的,特別是該類是屬於一個可子類化的類的時候。Thread.start由調用線程啓動,而後由得到該啓動的線程釋放鎖具備相同的「短暫記憶效應」,若是一個實現了Runnable接口的超類在子類構造子執行以前調用了Thread(this).start()方法,那麼就可能使得該對象在線程方法run執行以前並無被徹底初始化,這樣就使得一個指向該對象的合法引用去引用了不徹底構造的一個對象。一樣的,若是建立一個新的線程T而且啓動該線程,而後再使用線程T來建立對象X,這種狀況就不能保證X對象裏面全部的屬性針對線程T都是可見的除非是在全部針對X對象的引用中進行同步處理,或者最好的方法是在T線程啓動以前建立對象X。
  • 若一個線程終止,全部的變量值都必須從工做內存中刷到主存,好比,若是一個同步線程由於另外一個使用Thread.join方法的線程而終止,那麼該線程的可見域針對那個線程而言其發生的改變以及產生的一些影響是須要保證可知道的。
      注意:若是在同一個線程裏面經過方法調用去傳一個對象的引用是絕對不會出現上邊說起到的可見性問題的。JMM保證全部上邊的規定以及關於內存可見性特性的描述——一個特殊的更新、一個特定字段的修改都是某個線程針對其餘線程的一個「可見性」的概念,最終它發生的場所在內存模型中Java線程和線程之間,至於這個發生時間能夠是一個任意長的時間,可是最終會發生,也就是說,Java內存模型中的可見性的特性主要是針對線程和線程之間使用內存的一種規則和約定,該約定由JMM定義。
      不只僅如此,該模型還容許不一樣步的狀況下可見性特性。好比針對一個線程提供一個對象或者字段訪問域的原始值進行操做,而針對另一個線程提供一個對象或者字段刷新事後的值進行操做。一樣也有可能針對一個線程讀取一個原始的值以及引用對象的對象內容,針對另一個線程讀取一個刷新事後的值或者刷新事後的引用。
      儘管如此,上邊的可見性特性分析的一些特徵在跨線程操做的時候是有可能失敗的,並且不可以避免這些故障發生。這是一個不爭的事實,使用同步多線程的代碼並不能絕對保證線程安全的行爲,只是容許某種規則對其操做進行必定的限制,可是在最新的JVM實現以及最新的Java平臺中,即便是多個處理器,經過一些工具進行可見性的測試發現實際上是不多發生故障的。跨線程共享CPU的共享緩存的使用,其缺陷就在於影響了編譯器的優化操做,這也體現了強有力的緩存一致性使得硬件的價值有所提高,由於它們之間的關係在線程與線程之間的複雜度變得更高。這種方式使得可見度的自由測試顯得更加不切實際,由於這些錯誤的發生極爲罕見,或者說在平臺上咱們開發過程當中根本碰不到。在並行程開發中,不使用同步致使失敗的緣由也不只僅是對可見度的不良把握致使的,致使其程序失敗的緣由是多方面的,包括緩存一致性、內存一致性問題等。
  •   可排序(Ordering):
      可排序規則在線程與線程之間主要有下邊兩點:
  • 從操做線程的角度看來,若是全部的指令執行都是按照普通順序進行,那麼對於一個順序運行的程序而言,可排序性也是順序的
  • 從其餘操做線程的角度看來,排序性如同在這個線程中運行在非同步方法中的一個「間諜」,因此任何事情都有可能發生。惟一有用的限制是同步方法和同步塊的相對排序,就像操做volatile字段同樣,老是保留下來使用
      【:如何理解這裏「間諜」的意思,能夠這樣理解,排序規則在本線程裏面遵循了第一條法則,可是對其餘線程而言,某個線程自身的排序特性可能使得它不定地訪問執行線程的可見域,而使得該線程對自己在執行的線程產生必定的影響。舉個例子,A線程須要作三件事情分別是A一、A二、A3,而B是另一個線程具備操做B一、B2,若是把參考定位到B線程,那麼對A線程而言,B的操做B一、B2有可能隨時會訪問到A的可見區域,好比A有一個可見區域a,A1就是把a修改稱爲1,可是B線程在A線程調用了A1事後,卻訪問了a而且使用B1或者B2操做使得a發生了改變,變成了2,那麼當A按照排序性進行A2操做讀取到a的值的時候,讀取到的是2而不是1,這樣就使得程序最初設計的時候A線程的初衷發生了改變,就是排序被打亂了,那麼B線程對A線程而言,其身份就是「間諜」,並且須要注意到一點,B線程的這些操做不會和A之間存在等待關係,那麼B線程的這些操做就是異步操做,因此針對執行線程A而言,B的身份就是「非同步方法中的‘間諜’。】
      一樣的,這僅僅是一個最低限度的保障性質,在任何給定的程序或者平臺,開發中有可能發現更加嚴格的排序,可是開發人員在設計程序的時候不能依賴這種排序,若是依賴它們會發現測試難度會成指數級遞增,並且在複合規定的時候會由於不一樣的特性使得JVM的實現由於不符合設計初衷而失敗。
      注意:第一點在JLS(Java Language Specification)的全部討論中也是被採用的,例如算數表達式通常狀況都是從上到下、從左到右的順序,可是這一點須要理解的是,從其餘操做線程的角度看來這一點又具備不肯定性,對線程內部而言,其內存模型自己是存在排序性的。【
    :這裏討論的排序是最底層的內存裏面執行的時候的NativeCode的排序,不是說按照順序執行的Java代碼具備的有序性質,本文主要分析的是JVM的內存模型,因此但願讀者明白這裏指代的討論單元是內存區。】

      iii.原始JMM缺陷:

      JMM最初設計的時候存在必定的缺陷,這種缺陷雖然現有的JVM平臺已經修復,可是這裏不得不說起,也是爲了讀者更加了解JMM的設計思路,這一個小節的概念可能會牽涉到不少更加深刻的知識,若是讀者不能讀懂沒有關係先看了文章後邊的章節再返回來看也能夠。

      1)問題1:不可變對象不是不可變的

      學過Java的朋友都應該知道Java中的不可變對象,這一點在本文最後講解String類的時候也會說起,而JMM最初設計的時候,這個問題一直都存在,就是:不可變對象彷佛能夠改變它們的值(這種對象的不可變指經過使用final關鍵字來獲得保證),(Publis Service Reminder:讓一個對象的全部字段都爲final並不必定使得這個對象不可變——全部類型還必須是原始類型而不能是對象的引用。而不可變對象被認爲不要求同步的。可是,由於在將內存寫方面的更改從一個線程傳播到另一個線程的時候存在潛在的延遲,這樣就使得有可能存在一種競態條件,即容許一個線程首先看到不可變對象的一個值,一段時間以後看到的是一個不一樣的值。這種狀況之前怎麼發生的呢?在JDK 1.4中的String實現裏,這兒基本有三個重要的決定性字段:對字符數組的引用、長度和描述字符串的開始數組的偏移量。String就是以這樣的方式在JDK 1.4中實現的,而不是隻有字符數組,所以字符數組能夠在多個String和StringBuffer對象之間共享,而不須要在每次建立一個String的時候都拷貝到一個新的字符數組裏。假設有下邊的代碼:
    String s1 = "/usr/tmp";
    String s2 = s1.substring(4); // "/tmp"
      這種狀況下,字符串s2將具備大小爲4的長度和偏移量,可是它將和s1共享「/usr/tmp」裏面的同一字符數組,在String構造函數運行以前,Object的構造函數將用它們默認的值初始化全部的字段,包括決定性的長度和偏移字段。當String構造函數運行的時候,字符串長度和偏移量被設置成所須要的值。可是在舊的內存模型中,由於缺少同步,有可能另外一個線程會臨時地看到偏移量字段具備初始默認值0,然後又看到正確的值4,結果是s2的值從「/usr」變成了「/tmp」,這並非咱們真正的初衷,這個問題就是原始JMM的第一個缺陷所在,由於在原始JMM模型裏面這是合理並且合法的,JDK 1.4如下的版本都容許這樣作。

      2)問題2:從新排序的易失性和非易失性存儲

      另外一個主要領域是與volatile字段的內存操做從新排序有關,這個領域中現有的JMM引發了一些比較混亂的結果。現有的JMM代表易失性的讀和寫是直接和主存打交道的,這樣避免了把值存儲到寄存器或者繞過處理器特定的緩存,這使得多個線程通常能看見一個給定變量最新的值。但是,結果是這種volatile定義並無最初想象中那樣如願以償,而且致使了volatile的重大混亂。爲了在缺少同步的狀況下提供較好的性能,編譯器、運行時和緩存一般是容許進行內存的從新排序操做的,只要當前執行的線程分辨不出它們的區別。(這就是within-thread as-if-serial semantics[線程內彷佛是串行]的解釋)可是,易失性的讀和寫是徹底跨線程安排的,編譯器或緩存不能在彼此之間從新排序易失性的讀和寫。遺憾的是,經過參考普通變量的讀寫,JMM容許易失性的讀和寫被重排序,這樣覺得着開發人員不能使用易失性標誌做爲操做已經完成的標誌。好比:

     

Map configOptions;
char[] configText;
volatile boolean initialized = false;

// 線程1
configOptions = new HashMap();
configText = readConfigFile(filename);
processConfigOptions(configText,configOptions);
initialized = true;

// 線程2
while(!initialized)
sleep();

  這裏的思想是使用易失性變量initialized擔任守衛來代表一套別的操做已經完成了,這是一個很好的思想,可是不能在JMM下工做,由於舊的JMM容許非易失性的寫(好比寫到configOptions字段,以及寫到由configOptions引用Map的字段中)與易失性的寫一塊兒從新排序,所以另一個線程可能會看到initialized爲true,可是對於configOptions字段或它所引用的對象尚未一個一致的或者說當前的針對內存的視圖變量,volatile的舊語義只承諾在讀和寫的變量的可見性,而不承諾其餘變量,雖然這種方法更加有效的實現,可是結果會和咱們設計之初截然不同。
相關文章
相關標籤/搜索