深入理解Java的堆記憶體和執行緒記憶體

zsq_fengchen發表於2019-01-03

我們都知道Java物件都是在堆中建立的(開啟逃逸分析的情況除外),比如一個執行緒中有一段這樣的程式碼:

    public class A{

       public int xxx;

    }

     通過A a = new A();會在堆中建立一個物件,並引用a 指向了堆中物件的記憶體地址,也就是主記憶體中。

     也就是說執行緒中的引用指向了主記憶體中的物件地址,很多Java程式設計師甚至以為因為持有引用,所以對這個引用的賦值或者讀取都是直接根據地址操作主記憶體的物件,其實並不是這樣的。

     如果按照這個邏輯,執行緒中操作的物件就是主記憶體中物件(為了好理解,我直接認為主記憶體就是堆了);直接操作堆中物件, 那就是隻有一個堆中物件,也不會存在多執行緒下不一致問題了,因為大家都是通過地址操作同一個物件,只有一個版本,就不會有不一致問題。

     可事實並不是如此。執行緒記憶體和主記憶體是不一樣的。當執行緒要讀取a.xxx的時候,其實是通過該引用持有的記憶體地址去堆中讀取這個物件的屬性值,賦值給執行緒中的變數a.xxx;修改也一樣,修改完了後將這個值覆蓋堆中a的xxx屬性的值(怎麼實現稍後講);

       Java操作記憶體相關的指令有8個,lock(鎖定),unlock(解鎖),read(讀取),load(載入),use(使用),assign(賦值),store(儲存)

執行緒物件的操作主要是通過這個幾個指令實現的,而不是我們想象的直接操作。

       所以多執行緒下的時候,每個執行緒都去堆中讀取物件的值,拷貝到自己執行緒變數中使用,修改完了再覆蓋回去。這才會出現不一致和讀到被別人修改的資料。

      volatile關鍵字的作用之一就是每次使用前都和主記憶體(堆)進行上面的讀取或者寫回操作,保證了執行緒的可見性。如果按照執行緒物件就是直接操作堆中物件,那就根本就不需要這個關鍵字了,簡單想象就知道執行緒中的物件也不是堆中的物件這個事實了,使用這個關鍵字就是希望執行緒中的物件和堆中的物件是一致的。

       回答剛才留下的坑,那我們到底是怎麼去堆中讀取物件的內容的呢,比如上面的a.xxx,   物件的屬性的開始地址相對物件開始地址是有一個偏移量的,

每個型別都有其規定的長度,只要從開始地址讀取這個型別長度的地址就可以了。下面演示一種牛B的修改記憶體地址處值的方法。

        這個偏移量的獲取方法如下: 
        Field  field = a.getClass().getDeclaredField(“xxx”);
        long sexOffset = unsafe.objectFieldOffset(field); //sexOffset 就是這個偏移量;

        unsafe 是sun.misc.Unsafe類,是不能通過new出來的。可以通過反射獲取,Unsafe類是無鎖機制。

         public native Object getObjectVolatile(Object arg0, long arg1);

         這個方法的實現類中就可以通過物件引用和偏移地址來獲取其屬性值。

         這個類中還有很多牛B方法,很多都是通過傳說的CAS演算法實現的的,其實這種演算法沒那麼複雜就是比較替換,比較堆中的該地址處的值是否和期望值一樣,一樣就執 行相應的修改操作,並返回真,不一樣就放棄操作,返回假。

相關文章