我們都知道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算法實現的的,其實這種算法沒那么復雜就是比較和替換,比較堆中的該地址處的值是否和期望值一樣,一樣就執 行相應的修改操作,並返回真,不一樣就放棄操作,返回假。