Unsafe 類

  1. Unsafe類 不能直接 new, 其構(gòu)造函數(shù)被私有化
  2. public final class Unsafe : Unsafe 是 final 的, 不能被繼承
  3. 方法:
    • native 的 compareAndSwap 方法, 實現(xiàn) CAS 比較交換
    • native 的 put/get volatile 類型變量
    • native 的 park/unpark ,實現(xiàn)加鎖解鎖
    • java 本地方法的 getAndSet 和 getAndAdd, 內(nèi)部調(diào)用了 native 的 compareAndSwap
  1. Unsafe 之所以叫做不安全的, 是因為他可以直接操作內(nèi)存地址, 修改對象屬性

  2. Unsafe 暴露了一個靜態(tài)方法用于獲取對象, 但會校驗加載 Unsafe 的是否為 Bootstrap ClassLoader, 不是則拋異常

    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return this.theUnsafe;
        }
    }
    
    // 靜態(tài)代碼塊
    static {
        registerNatives();
        Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"});
        theUnsafe = new Unsafe();
        ARRAY_BOOLEAN_BASE_OFFSET = theUnsafe.arrayBaseOffset(boolean[].class);
        ARRAY_BYTE_BASE_OFFSET = theUnsafe.arrayBaseOffset(byte[].class);
            ... ... ...
        ADDRESS_SIZE = theUnsafe.addressSize();
    }
    

因此, 如果想獲取 Unsafe 實例, 只能通過反射. 看到 getUnsafe 返回 theUnsafe 屬性, 且改屬性在靜態(tài)代碼塊中已被初始化. 即只要該類加載, 該類的實例就被存在屬性中, 我們直接反射獲取屬性即可

public static Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        //Field unsafeField = Unsafe.class.getDeclaredFields()[0]; //也可以這樣砂蔽,作用相同
        unsafeField.setAccessible(true);
        Unsafe unsafe =(Unsafe) unsafeField.get(null);
        return unsafe;
    }

二. 功能實現(xiàn)

  1. 內(nèi)存操作
    實現(xiàn)像 C++ 一樣的申請, 釋放, 調(diào)整, 拷貝內(nèi)存. 通過 Unsafe 申請的內(nèi)存是對外內(nèi)存, 需要手動釋放. 方法為在 finally 中釋放申請的內(nèi)存

    • 方法列表
    // 分配新的本地空間
    public native long allocateMemory(long bytes);
    // 重新調(diào)整內(nèi)存空間的大小
    public native long reallocateMemory(long address, long bytes);
    // 將內(nèi)存設(shè)置為指定值
    public native void setMemory(Object o, long offset, long bytes, byte value);
    // 內(nèi)存拷貝
    public native void copyMemory(Object srcBase, long srcOffset,Object destBase, long destOffset,long bytes);
    // 清除內(nèi)存
    public native void freeMemory(long address);
    
    • 示例
    private static void memoryTest(Unsafe unsafe) {
        int size = 4;
        // 分配4字節(jié)長度的內(nèi)存
        long addr = unsafe.allocateMemory(size);
        // 分配一個8字節(jié)長度的內(nèi)存
        long addr3 = unsafe.reallocateMemory(addr, size * 2);
        System.out.println("addr地址: "+addr);
        System.out.println("addr3地址: "+addr3);
        try {
            // 初始化內(nèi)存, 向4個字節(jié)中的每個字節(jié), 一次寫入 byte(1). 即內(nèi)存中為  0001 0001 0001 0001 => 組成的4字節(jié) int 型值為 16843009
            unsafe.setMemory(null,addr ,size,(byte)1);
            // 從 addr 想 addr3 拷貝值, 因為 addr3 比 addr 長1倍, 所以需要拷貝2次
            // 拷貝后, addr3 中的內(nèi)容為  0001 0001 0001 0001 0001 0001 0001 0001 => long 型值為 72340172838076673
            for (int i = 0; i < 2; i++) {
                unsafe.copyMemory(null,addr,null,addr3+size*i,4);
            }
            System.out.println(unsafe.getInt(addr));
            System.out.println(unsafe.getLong(addr3));
        }finally {
            // 手動釋放內(nèi)存, Unsafe的內(nèi)存不在 jvm 中
            unsafe.freeMemory(addr);
            unsafe.freeMemory(addr3);
        }
    }
    
  2. 內(nèi)存屏障

    • Unsafe 提供了防止指令重排的方法. 分為讀重排, 寫重排, 讀寫重排. 內(nèi)存屏障相當于一個操作的同步點, 確保同步點之前的指令不會被重排到同步點之后
     public native void loadFence();
     public native void storeFence();
     public native void fullFence();
    
    • loadFence 為例, 他會禁止讀操作重排序, 確保該點之前的所有讀操作都已完成, 且會將線程從主內(nèi)存拷貝過來的緩存值設(shè)為無效, 重新從主內(nèi)存中加載數(shù)據(jù)
    • 示例, 我們用 Unsafe 的讀屏障, 模擬 volatile
    // 定義一個線程方法,在線程中去修改flag標志位,注意這里的flag是沒有被volatile修飾的:
    class ChangeThread implements Runnable{
     /**volatile**/ boolean flag=false;
     @Override
     public void run() {
         try {
             Thread.sleep(3000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }        
         System.out.println("subThread change flag to:" + flag);
         flag = true;
     }
    }
    
    // 主線程用讀屏障, 確保變量的可見性  
    public static void main(String[] args){
     ChangeThread changeThread = new ChangeThread();
     new Thread(changeThread).start();
     while (true) {
         boolean flag = changeThread.isFlag();
         unsafe.loadFence(); //加入讀內(nèi)存屏障
         if (flag){
             System.out.println("detected flag changed");
             break;
         }
     }
     System.out.println("main thread end");
    }
    
  3. 對象操作

    1. 修改屬性值

      • Unsafe 可以通過計算某個屬性在對象內(nèi)中的偏移量, 直接修改內(nèi)存地址上的值, 達到修改對象屬性的目的
          /**
       * 獲取屬性的內(nèi)存地址修改對象屬性值
       */
      public static void fieldTest(Unsafe unsafe) throws NoSuchFieldException {
          User user=new User();
          long fieldOffset = unsafe.objectFieldOffset(User.class.getDeclaredField("age"));
          System.out.println("內(nèi)存地址offset:"+fieldOffset);
          unsafe.putInt(user,fieldOffset,20);
          System.out.println("age:"+unsafe.getInt(user,fieldOffset));
          System.out.println("age:"+user.getAge());
      }
      
      • Unsafe 的操作屬性修改有三類: volatile讀寫, 有序?qū)懭? 普通讀寫
      // 在對象的指定偏移地址獲取一個對象引用
      // (1) 普通讀寫
      public native Object getObject(Object o, long offset);
      // 在對象指定偏移地址寫入一個對象引用
      public native void putObject(Object o, long offset, Object x);
      
      // (2) volatile 讀寫
      //在對象的指定偏移地址處讀取一個int值,支持volatile load語義
      public native int getIntVolatile(Object o, long offset);
      //在對象指定偏移地址處寫入一個int菱蔬,支持volatile store語義
      public native void putIntVolatile(Object o, long offset, int x);
      
      // (3) 順序讀寫
      public native void putOrderedObject(Object o, long offset, Object x);
      public native void putOrderedInt(Object o, long offset, int x);
      public native void putOrderedLong(Object o, long offset, long x);
      
      • 從實現(xiàn)效果上看
        • 有序?qū)懭? 保證寫入時的有序性夹攒,而不保證可見性,也就是一個線程寫入的值不能保證其他線程立即可見碟绑。
        • volatile寫入: 保證可見性和有序性单默。get 和 set 會同步操作主存. 在執(zhí)行g(shù)et操作時碘举,強制從主存中獲取屬性值; 在使用put方法設(shè)置屬性值時,會強制將值更新到主存中搁廓,從而保證這些變更對其他線程是可見的引颈。
    2. 初始化對象

      • Unsafe 的 allocateInstance 方法,允許我們使用非常規(guī)的方式進行對象的實例化. 不通過構(gòu)造函數(shù)初始化對象, 通過對象及其屬性的字段長度初始化
      • Unsafe 的 allocateInstance 初始化對象, 只能將屬性設(shè)置為每種數(shù)據(jù)類型的默認值
      public class A {
          private int b;
          public A(){
              this.b =1;
          }
      }
      
      public void objTest() throws Exception{
          A a1=new A();
          System.out.println(a1.getB());
          A a2 = A.class.newInstance();
          System.out.println(a2.getB());
          A a3= (A) unsafe.allocateInstance(A.class);
          System.out.println(a3.getB());
      }
      
  4. 數(shù)組操作 (略)

  5. CAS
    在 Unsafe 類中境蜕,提供了 compareAndSwapObject蝙场、compareAndSwapIntcompareAndSwapLong 方法來實現(xiàn)的對 Object粱年、int售滤、long 類型的CAS操作

    public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
    
    • compareAndSwapInt 方法名: 表示 CAS 更新對象中的 int 屬性
    • Object o : 待更新的對象
    • long offset : 待更新字段的地址偏移量
    • int expected,int x : 表示只有字段值為 expected 時, 才把字段值改為 x

    如下示例 , 輸出 1 2 3 4 5 6 7 8 9

    public class Test {
        private volatile int a;
        public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
            Test Test=new Test();
            Unsafe unsafe = Test.getUnsafe();
            // 負責(zé) [1,5) 的累加
            new Thread(()->{
                for (int i = 1; i < 5; i++) {
                    Test.increment(i,unsafe);
                    System.out.print(Test.a+" ");
                }
            }).start();
            // 負責(zé) [5,10] 的累加
            new Thread(()->{
                for (int i = 5 ; i <10 ; i++) {
                    Test.increment(i,unsafe);
                    System.out.print(Test.a+" ");
                }
            }).start();
        }
    
        private void increment(int x, Unsafe unsafe){
            while (true){
                try {
                    // 只有在a的值等于傳入的參數(shù)x減一時,才會將a的值變?yōu)閤
                    long fieldOffset = unsafe.objectFieldOffset(Test.class.getDeclaredField("a"));
                    if (unsafe.compareAndSwapInt(this,fieldOffset,x-1,x))
                        break;
                    else
                        Thread.yield();
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
  6. 線程調(diào)度
    AQS 經(jīng)常使用 LockSupport 掛起或喚醒指定線程. LockSupport 的 park 方法調(diào)用了Unsafe 的 park 方法來阻塞當前線程台诗,此方法將線程阻塞后就不會繼續(xù)往后執(zhí)行完箩,直到有其他線程調(diào)用 unpark 方法喚醒當前線程。

    • unpark 方法是其他線程調(diào)用, 喚醒本線程的
    • park 方法是本線程調(diào)用, 自己阻塞自己

    下面一個例子對Unsafe的這兩個方法進行測試:

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Thread mainThread = Thread.currentThread();
        Unsafe unsafe = Test.getUnsafe();
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(5);
                System.out.println("subThread try to unpark mainThread");
                // 新線程喚醒主線程
                unsafe.unpark(mainThread);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    
        System.out.println("park main mainThread");
        // 主線程自己阻塞自己
        unsafe.park(false,0L);
        System.out.println("unpark mainThread success");
    }
    
  7. 對 Class 操作
    Unsafe 對 Class 的相關(guān)操作主要包括類加載和靜態(tài)變量的操作方法拉队。

    1. 加載靜態(tài)變量
      • 前面提到 unsafe 可以操作對象的屬性, 但靜態(tài)屬性和對象沒關(guān)系, 和類有關(guān)系; 靜態(tài)屬性單獨存在方法區(qū), jdk1.8 后靜態(tài)屬性存在堆中, 但仍與普通對象在不同區(qū)域. 因此操作靜態(tài)屬性的是新的 api
      //獲取靜態(tài)屬性的偏移量
      public native long staticFieldOffset(Field f);
      //獲取靜態(tài)屬性的對象指針
      public native Object staticFieldBase(Field f);
      //判斷類是否需要實例化(用于獲取類的靜態(tài)屬性前進行檢測)
      // 如果 jvm 已加載 Class c , 則返回 false, 否則返回 true
      public native boolean shouldBeInitialized(Class<?> c);
      
    • 測試示例
       private static void staticTest(Unsafe unsafe) throws Exception {
         User user = new User(); // 為了讓 jvm 加載類, 從而初始化靜態(tài)字段. 下面那句和這句目的一樣
         //        Class.forName(User.class.getName());
         System.out.println(unsafe.shouldBeInitialized(User.class));
         // 反射字段
         Field sexField = User.class.getDeclaredField("name");
         // 獲取靜態(tài)字段 "name" 對象
         Object fieldBase = unsafe.staticFieldBase(sexField);
         // 獲取靜態(tài)字段 "name" 所在方法區(qū)/堆 中的地址
         long fieldOffset = unsafe.staticFieldOffset(sexField);
         Object object = unsafe.getObject(fieldBase, fieldOffset);
         System.out.println(object);
      }
      
      class User {
       public static String name = "Hydra";
       int age;
      }
      
      上面示輸出 false, Hydra
      如果注釋掉User user = new User(); , 輸出 true , null
    1. 加載類
    public native Class<?> defineClass(String name, byte[] b, int off, int len,
                                   ClassLoader loader,ProtectionDomain protectionDomain);
    
  8. 系統(tǒng)信息
    Unsafe 中提供的 addressSizepageSize 方法用于獲取系統(tǒng)信息

    • addressSize方法會返回系統(tǒng)指針的大小弊知,如果在64位系統(tǒng)下默認會返回8,而32位系統(tǒng)則會返回4粱快。
    • pageSize 方法會返回內(nèi)存頁的大小秩彤,值為2的整數(shù)冪夺鲜。
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Unsafe unsafe = Test.getUnsafe();
        System.out.println(unsafe.addressSize());  // 8 (64位系統(tǒng), 地址長度為8字節(jié); 32位系統(tǒng), 地址長度為4字節(jié))
        System.out.println(unsafe.pageSize());     // 4 (內(nèi)存頁大小: 4kb) 
    }
    
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市呐舔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌慷蠕,老刑警劉巖珊拼,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異流炕,居然都是意外死亡澎现,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門每辟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來剑辫,“玉大人,你說我怎么就攤上這事渠欺∶帽危” “怎么了?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵挠将,是天一觀的道長胳岂。 經(jīng)常有香客問我,道長舔稀,這世上最難降的妖魔是什么乳丰? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮内贮,結(jié)果婚禮上产园,老公的妹妹穿的比我還像新娘。我一直安慰自己夜郁,他們只是感情好什燕,可當我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著拂酣,像睡著了一般秋冰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上婶熬,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天剑勾,我揣著相機與錄音,去河邊找鬼赵颅。 笑死虽另,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的饺谬。 我是一名探鬼主播捂刺,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼谣拣,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了族展?” 一聲冷哼從身側(cè)響起森缠,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎仪缸,沒想到半個月后贵涵,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡恰画,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年宾茂,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拴还。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡跨晴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出片林,到底是詐尸還是另有隱情端盆,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布拇厢,位于F島的核電站爱谁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏孝偎。R本人自食惡果不足惜访敌,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望衣盾。 院中可真熱鬧寺旺,春花似錦、人聲如沸势决。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽果复。三九已至陈莽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間虽抄,已是汗流浹背走搁。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留迈窟,地道東北人私植。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像车酣,于是被迫代替她去往敵國和親曲稼。 傳聞我的和親對象是個殘疾皇子索绪,可洞房花燭夜當晚...
    茶點故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內(nèi)容