<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Java雙刃劍之Unsafe類(lèi)詳解

          共 23999字,需瀏覽 48分鐘

           ·

          2021-05-10 01:00


          前一段時(shí)間在研究juc源碼的時(shí)候,發(fā)現(xiàn)在很多工具類(lèi)中都調(diào)用了一個(gè)Unsafe類(lèi)中的方法,出于好奇就想要研究一下這個(gè)類(lèi)到底有什么作用,于是先查閱了一些資料,一查不要緊,很多資料中對(duì)Unsafe的態(tài)度都是這樣的畫(huà)風(fēng):

          其實(shí)看到這些說(shuō)法也沒(méi)什么意外,畢竟Unsafe這個(gè)詞直譯過(guò)來(lái)就是“不安全的”,從名字里我們也大概能看來(lái)Java的開(kāi)發(fā)者們對(duì)它有些不放心。但是作為一名極客,不能你說(shuō)不安全我就不去研究了,畢竟只有了解一項(xiàng)技術(shù)的風(fēng)險(xiǎn)點(diǎn),才能更好的避免出現(xiàn)這些問(wèn)題嘛。

          下面我們言歸正傳,先通過(guò)簡(jiǎn)單的介紹來(lái)對(duì)Unsafe類(lèi)有一個(gè)大致的了解。Unsafe類(lèi)是一個(gè)位于sun.misc包下的類(lèi),它提供了一些相對(duì)底層方法,能夠讓我們接觸到一些更接近操作系統(tǒng)底層的資源,如系統(tǒng)的內(nèi)存資源、cpu指令等。而通過(guò)這些方法,我們能夠完成一些普通方法無(wú)法實(shí)現(xiàn)的功能,例如直接使用偏移地址操作對(duì)象、數(shù)組等等。但是在使用這些方法提供的便利的同時(shí),也存在一些潛在的安全因素,例如對(duì)內(nèi)存的錯(cuò)誤操作可能會(huì)引起內(nèi)存泄漏,嚴(yán)重時(shí)甚至可能引起jvm崩潰。因此在使用Unsafe前,我們必須要了解它的工作原理與各方法的應(yīng)用場(chǎng)景,并且在此基礎(chǔ)上仍需要非常謹(jǐn)慎的操作,下面我們正式開(kāi)始對(duì)Unsafe的學(xué)習(xí)。

          Unsafe 基礎(chǔ)

          首先我們來(lái)嘗試獲取一個(gè)Unsafe實(shí)例,如果按照new的方式去創(chuàng)建對(duì)象,不好意思,編譯器會(huì)報(bào)錯(cuò)提示你:

          Unsafe() has private access in 'sun.misc.Unsafe'

          查看Unsafe類(lèi)的源碼,可以看到它被final修飾不允許被繼承,并且構(gòu)造函數(shù)為private類(lèi)型,即不允許我們手動(dòng)調(diào)用構(gòu)造方法進(jìn)行實(shí)例化,只有在static靜態(tài)代碼塊中,以單例的方式初始化了一個(gè)Unsafe對(duì)象:

          public final class Unsafe {
              private static final Unsafe theUnsafe;
              ...
              private Unsafe() {
              }
              ...
              static {
                  theUnsafe = new Unsafe();
              }   
          }

          在Unsafe類(lèi)中,提供了一個(gè)靜態(tài)方法getUnsafe,看上去貌似可以用它來(lái)獲取Unsafe實(shí)例:

          @CallerSensitive
          public static Unsafe getUnsafe() {
              Class var0 = Reflection.getCallerClass();
              if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
                  throw new SecurityException("Unsafe");
              } else {
                  return theUnsafe;
              }
          }

          但是如果我們直接調(diào)用這個(gè)靜態(tài)方法,會(huì)拋出異常:

          Exception in thread "main" java.lang.SecurityException: Unsafe
           at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
           at com.cn.test.GetUnsafeTest.main(GetUnsafeTest.java:12)

          這是因?yàn)樵?code style="">getUnsafe方法中,會(huì)對(duì)調(diào)用者的classLoader進(jìn)行檢查,判斷當(dāng)前類(lèi)是否由Bootstrap classLoader加載,如果不是的話那么就會(huì)拋出一個(gè)SecurityException異常。也就是說(shuō),只有啟動(dòng)類(lèi)加載器加載的類(lèi)才能夠調(diào)用Unsafe類(lèi)中的方法,來(lái)防止這些方法在不可信的代碼中被調(diào)用。

          那么,為什么要對(duì)Unsafe類(lèi)進(jìn)行這么謹(jǐn)慎的使用限制呢,說(shuō)到底,還是因?yàn)樗鼘?shí)現(xiàn)的功能過(guò)于底層,例如直接進(jìn)行內(nèi)存操作、繞過(guò)jvm的安全檢查創(chuàng)建對(duì)象等等,概括的來(lái)說(shuō),Unsafe類(lèi)實(shí)現(xiàn)功能可以被分為下面8類(lèi):

          創(chuàng)建實(shí)例

          看到上面的這些功能,你是不是已經(jīng)有些迫不及待想要試一試了。那么如果我們執(zhí)意想要在自己的代碼中調(diào)用Unsafe類(lèi)的方法,應(yīng)該怎么獲取一個(gè)它的實(shí)例對(duì)象呢,答案是利用反射獲得Unsafe類(lèi)中已經(jīng)實(shí)例化完成的單例對(duì)象:

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

          在獲取到Unsafe的實(shí)例對(duì)象后,我們就可以使用它為所欲為了,先來(lái)嘗試使用它對(duì)一個(gè)對(duì)象的屬性進(jìn)行讀寫(xiě):

          public void fieldTest(Unsafe unsafe) throws NoSuchFieldException {
              User user=new User();
              long fieldOffset = unsafe.objectFieldOffset(User.class.getDeclaredField("age"));
              System.out.println("offset:"+fieldOffset);
              unsafe.putInt(user,fieldOffset,20);
              System.out.println("age:"+unsafe.getInt(user,fieldOffset));
              System.out.println("age:"+user.getAge());
          }

          運(yùn)行代碼輸出如下,可以看到通過(guò)Unsafe類(lèi)的objectFieldOffset方法獲取了對(duì)象中字段的偏移地址,這個(gè)偏移地址不是內(nèi)存中的絕對(duì)地址而是一個(gè)相對(duì)地址,之后再通過(guò)這個(gè)偏移地址對(duì)int類(lèi)型字段的屬性值進(jìn)行了讀寫(xiě)操作,通過(guò)結(jié)果也可以看到Unsafe的方法和類(lèi)中的get方法獲取到的值是相同的。

          offset:12
          age:20
          age:20

          在上面的例子中調(diào)用了Unsafe類(lèi)的putIntgetInt方法,看一下源碼中的方法:

          public native int getInt(Object o, long offset);
          public native void putInt(Object o, long offset, int x);

          先說(shuō)作用,getInt用于從對(duì)象的指定偏移地址處讀取一個(gè)intputInt用于在對(duì)象指定偏移地址處寫(xiě)入一個(gè)int,并且即使類(lèi)中的這個(gè)屬性是private私有類(lèi)型的,也可以對(duì)它進(jìn)行讀寫(xiě)。但是有細(xì)心的小伙伴可能發(fā)現(xiàn)了,這兩個(gè)方法相對(duì)于我們平常寫(xiě)的普通方法,多了一個(gè)native關(guān)鍵字修飾,并且沒(méi)有具體的方法邏輯,那么它是怎么實(shí)現(xiàn)的呢?

          native方法

          在java中,這類(lèi)方法被稱為native方法(Native Method),簡(jiǎn)單的說(shuō)就是由java調(diào)用非java代碼的接口,被調(diào)用的方法是由非java 語(yǔ)言實(shí)現(xiàn)的,例如它可以由C或C++語(yǔ)言來(lái)實(shí)現(xiàn),并編譯成DLL,然后直接供java進(jìn)行調(diào)用。native方法是通過(guò)JNI(Java Native Interface)實(shí)現(xiàn)調(diào)用的,從 java1.1開(kāi)始 JNI 標(biāo)準(zhǔn)就是java平臺(tái)的一部分,它允許java代碼和其他語(yǔ)言的代碼進(jìn)行交互。

          Unsafe類(lèi)中的很多基礎(chǔ)方法都屬于native方法,那么為什么要使用native方法呢?原因可以概括為以下幾點(diǎn):

          • 需要用到 java 中不具備的依賴于操作系統(tǒng)的特性,java在實(shí)現(xiàn)跨平臺(tái)的同時(shí)要實(shí)現(xiàn)對(duì)底層的控制,需要借助其他語(yǔ)言發(fā)揮作用
          • 對(duì)于其他語(yǔ)言已經(jīng)完成的一些現(xiàn)成功能,可以使用java直接調(diào)用
          • 程序?qū)r(shí)間敏感或?qū)π阅芤蠓浅8邥r(shí),有必要使用更加底層的語(yǔ)言,例如C/C++甚至是匯編

          juc包的很多并發(fā)工具類(lèi)在實(shí)現(xiàn)并發(fā)機(jī)制時(shí),都調(diào)用了native方法,通過(guò)它們打破了java運(yùn)行時(shí)的界限,能夠接觸到操作系統(tǒng)底層的某些功能。對(duì)于同一個(gè)native方法,不同的操作系統(tǒng)可能會(huì)通過(guò)不同的方式來(lái)實(shí)現(xiàn),但是對(duì)于使用者來(lái)說(shuō)是透明的,最終都會(huì)得到相同的結(jié)果,至于java如何實(shí)現(xiàn)的通過(guò)JNI調(diào)用其他語(yǔ)言的代碼,不是本文的重點(diǎn),會(huì)在后續(xù)的文章中具體學(xué)習(xí)。

          Unsafe 應(yīng)用

          在對(duì)Unsafe的基礎(chǔ)有了一定了解后,我們來(lái)看一下它的基本應(yīng)用。由于篇幅有限,不能對(duì)所有方法進(jìn)行介紹,如果大家有學(xué)習(xí)的需要,可以下載openJDK的源碼進(jìn)行學(xué)習(xí)。

          1、內(nèi)存操作

          如果你是一個(gè)寫(xiě)過(guò)c或者c++的程序員,一定對(duì)內(nèi)存操作不會(huì)陌生,而在java中是不允許直接對(duì)內(nèi)存進(jìn)行操作的,對(duì)象內(nèi)存的分配和回收都是由jvm自己實(shí)現(xiàn)的。但是在Unsafe中,提供的下列接口可以直接進(jìn)行內(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);

          使用下面的代碼進(jìn)行測(cè)試:

          private void memoryTest() {
              int size = 4;
              long addr = unsafe.allocateMemory(size);
              long addr3 = unsafe.reallocateMemory(addr, size * 2);
              System.out.println("addr: "+addr);
              System.out.println("addr3: "+addr3);
              try {
                  unsafe.setMemory(null,addr ,size,(byte)1);
                  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 {
                  unsafe.freeMemory(addr);
                  unsafe.freeMemory(addr3);
              }
          }

          先看結(jié)果輸出:

          addr: 2433733895744
          addr3: 2433733894944
          16843009
          72340172838076673

          分析一下運(yùn)行結(jié)果,首先使用allocateMemory方法申請(qǐng)4字節(jié)長(zhǎng)度的內(nèi)存空間,在循環(huán)中調(diào)用setMemory方法向每個(gè)字節(jié)寫(xiě)入內(nèi)容為byte類(lèi)型的1,當(dāng)使用Unsafe調(diào)用getInt方法時(shí),因?yàn)橐粋€(gè)int型變量占4個(gè)字節(jié),會(huì)一次性讀取4個(gè)字節(jié),組成一個(gè)int的值,對(duì)應(yīng)的十進(jìn)制結(jié)果為16843009,可以通過(guò)圖示理解這個(gè)過(guò)程:

          在代碼中調(diào)用reallocateMemory方法重新分配了一塊8字節(jié)長(zhǎng)度的內(nèi)存空間,通過(guò)比較addraddr3可以看到和之前申請(qǐng)的內(nèi)存地址是不同的。在代碼中的第二個(gè)for循環(huán)里,調(diào)用copyMemory方法進(jìn)行了兩次內(nèi)存的拷貝,每次拷貝內(nèi)存地址addr開(kāi)始的4個(gè)字節(jié),分別拷貝到以addr3addr3+4開(kāi)始的內(nèi)存空間上:

          拷貝完成后,使用getLong方法一次性讀取8個(gè)字節(jié),得到long類(lèi)型的值為72340172838076673。

          需要注意,通過(guò)這種方式分配的內(nèi)存屬于堆外內(nèi)存,是無(wú)法進(jìn)行垃圾回收的,需要我們把這些內(nèi)存當(dāng)做一種資源去手動(dòng)調(diào)用freeMemory方法進(jìn)行釋放,否則會(huì)產(chǎn)生內(nèi)存泄漏。通用的操作內(nèi)存方式是在try中執(zhí)行對(duì)內(nèi)存的操作,最終在finally塊中進(jìn)行內(nèi)存的釋放。

          2、內(nèi)存屏障

          在介紹內(nèi)存屏障前,需要知道編譯器和CPU會(huì)在保證程序輸出結(jié)果一致的情況下,會(huì)對(duì)代碼進(jìn)行重排序,從指令優(yōu)化角度提升性能。而指令重排序可能會(huì)帶來(lái)一個(gè)不好的結(jié)果,導(dǎo)致CPU的高速緩存和內(nèi)存中數(shù)據(jù)的不一致,而內(nèi)存屏障(Memory Barrier)就是通過(guò)組織屏障兩邊的指令重排序從而避免編譯器和硬件的不正確優(yōu)化情況。

          在硬件層面上,內(nèi)存屏障是CPU為了防止代碼進(jìn)行重排序而提供的指令,不同的硬件平臺(tái)上實(shí)現(xiàn)內(nèi)存屏障的方法可能并不相同。在java8中,引入了3個(gè)內(nèi)存屏障的函數(shù),它屏蔽了操作系統(tǒng)底層的差異,允許在代碼中定義、并統(tǒng)一由jvm來(lái)生成內(nèi)存屏障指令,來(lái)實(shí)現(xiàn)內(nèi)存屏障的功能。Unsafe中提供了下面三個(gè)內(nèi)存屏障相關(guān)方法:

          //禁止讀操作重排序
          public native void loadFence();
          //禁止寫(xiě)操作重排序
          public native void storeFence();
          //禁止讀、寫(xiě)操作重排序
          public native void fullFence();

          內(nèi)存屏障可以看做對(duì)內(nèi)存隨機(jī)訪問(wèn)的操作中的一個(gè)同步點(diǎn),使得此點(diǎn)之前的所有讀寫(xiě)操作都執(zhí)行后才可以開(kāi)始執(zhí)行此點(diǎn)之后的操作。以loadFence方法為例,它會(huì)禁止讀操作重排序,保證在這個(gè)屏障之前的所有讀操作都已經(jīng)完成,并且將緩存數(shù)據(jù)設(shè)為無(wú)效,重新從主存中進(jìn)行加載。

          看到這估計(jì)很多小伙伴們會(huì)想到volatile關(guān)鍵字了,如果在字段上添加了volatile關(guān)鍵字,就能夠?qū)崿F(xiàn)字段在多線程下的可見(jiàn)性。基于讀內(nèi)存屏障,我們也能實(shí)現(xiàn)相同的功能。下面定義一個(gè)線程方法,在線程中去修改flag標(biāo)志位,注意這里的flag是沒(méi)有被volatile修飾的:

          @Getter
          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;
              }
          }

          在主線程的while循環(huán)中,加入內(nèi)存屏障,測(cè)試是否能夠感知到flag的修改變化:

          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");
          }

          運(yùn)行結(jié)果:

          subThread change flag to:false
          detected flag changed
          main thread end

          而如果刪掉上面代碼中的loadFence方法,那么主線程將無(wú)法感知到flag發(fā)生的變化,會(huì)一直在while中循環(huán)。可以用圖來(lái)表示上面的過(guò)程:

          了解java內(nèi)存模型(JMM)的小伙伴們應(yīng)該清楚,運(yùn)行中的線程不是直接讀取主內(nèi)存中的變量的,只能操作自己工作內(nèi)存中的變量,然后同步到主內(nèi)存中,并且線程的工作內(nèi)存是不能共享的。上面的圖中的流程就是子線程借助于主內(nèi)存,將修改后的結(jié)果同步給了主線程,進(jìn)而修改主線程中的工作空間,跳出循環(huán)。

          3、對(duì)象操作

          a、對(duì)象成員屬性的內(nèi)存偏移量獲取,以及字段屬性值的修改,在上面的例子中我們已經(jīng)測(cè)試過(guò)了。除了前面的putIntgetInt方法外,Unsafe提供了全部8種基礎(chǔ)數(shù)據(jù)類(lèi)型以及Objectputget方法,并且所有的put方法都可以越過(guò)訪問(wèn)權(quán)限,直接修改內(nèi)存中的數(shù)據(jù)。閱讀openJDK源碼中的注釋發(fā)現(xiàn),基礎(chǔ)數(shù)據(jù)類(lèi)型和Object的讀寫(xiě)稍有不同,基礎(chǔ)數(shù)據(jù)類(lèi)型是直接操作的屬性值(value),而Object的操作則是基于引用值(reference value)。下面是Object的讀寫(xiě)方法:

          //在對(duì)象的指定偏移地址獲取一個(gè)對(duì)象引用
          public native Object getObject(Object o, long offset);
          //在對(duì)象指定偏移地址寫(xiě)入一個(gè)對(duì)象引用
          public native void putObject(Object o, long offset, Object x);

          除了對(duì)象屬性的普通讀寫(xiě)外,Unsafe還提供了volatile讀寫(xiě)有序?qū)懭?/strong>方法。volatile讀寫(xiě)方法的覆蓋范圍與普通讀寫(xiě)相同,包含了全部基礎(chǔ)數(shù)據(jù)類(lèi)型和Object類(lèi)型,以int類(lèi)型為例:

          //在對(duì)象的指定偏移地址處讀取一個(gè)int值,支持volatile load語(yǔ)義
          public native int getIntVolatile(Object o, long offset);
          //在對(duì)象指定偏移地址處寫(xiě)入一個(gè)int,支持volatile store語(yǔ)義
          public native void putIntVolatile(Object o, long offset, int x);

          相對(duì)于普通讀寫(xiě)來(lái)說(shuō),volatile讀寫(xiě)具有更高的成本,因?yàn)樗枰WC可見(jiàn)性和有序性。在執(zhí)行get操作時(shí),會(huì)強(qiáng)制從主存中獲取屬性值,在使用put方法設(shè)置屬性值時(shí),會(huì)強(qiáng)制將值更新到主存中,從而保證這些變更對(duì)其他線程是可見(jiàn)的。

          有序?qū)懭氲姆椒ㄓ幸韵氯齻€(gè):

          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);

          有序?qū)懭氲某杀鞠鄬?duì)volatile較低,因?yàn)樗槐WC寫(xiě)入時(shí)的有序性,而不保證可見(jiàn)性,也就是一個(gè)線程寫(xiě)入的值不能保證其他線程立即可見(jiàn)。為了解決這里的差異性,需要對(duì)內(nèi)存屏障的知識(shí)點(diǎn)再進(jìn)一步進(jìn)行補(bǔ)充,首先需要了解兩個(gè)指令的概念:

          • Load:將主內(nèi)存中的數(shù)據(jù)拷貝到處理器的緩存中
          • Store:將處理器緩存的數(shù)據(jù)刷新到主內(nèi)存中

          順序?qū)懭肱cvolatile寫(xiě)入的差別在于,在順序?qū)憰r(shí)加入的內(nèi)存屏障類(lèi)型為StoreStore類(lèi)型,而在volatile寫(xiě)入時(shí)加入的內(nèi)存屏障是StoreLoad類(lèi)型,如下圖所示:

          在有序?qū)懭敕椒ㄖ校褂玫氖?code style="">StoreStore屏障,該屏障確保Store1立刻刷新數(shù)據(jù)到內(nèi)存,這一操作先于Store2以及后續(xù)的存儲(chǔ)指令操作。而在volatile寫(xiě)入中,使用的是StoreLoad屏障,該屏障確保Store1立刻刷新數(shù)據(jù)到內(nèi)存,這一操作先于Load2及后續(xù)的裝載指令,并且,StoreLoad屏障會(huì)使該屏障之前的所有內(nèi)存訪問(wèn)指令,包括存儲(chǔ)指令和訪問(wèn)指令全部完成之后,才執(zhí)行該屏障之后的內(nèi)存訪問(wèn)指令。

          綜上所述,在上面的三類(lèi)寫(xiě)入方法中,在寫(xiě)入效率方面,按照putputOrderputVolatile的順序效率逐漸降低,

          b、使用Unsafe的allocateInstance方法,允許我們使用非常規(guī)的方式進(jìn)行對(duì)象的實(shí)例化,首先定義一個(gè)實(shí)體類(lèi),并且在構(gòu)造函數(shù)中對(duì)其成員變量進(jìn)行賦值操作:

          @Data
          public class A {
              private int b;
              public A(){
                  this.b =1;
              }
          }

          分別基于構(gòu)造函數(shù)、反射以及Unsafe方法的不同方式創(chuàng)建對(duì)象進(jìn)行比較:

          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());
          }

          打印結(jié)果分別為1、1、0,說(shuō)明通過(guò)allocateInstance方法創(chuàng)建對(duì)象過(guò)程中,不會(huì)調(diào)用類(lèi)的構(gòu)造方法。使用這種方式創(chuàng)建對(duì)象時(shí),只用到了Class對(duì)象,所以說(shuō)如果想要跳過(guò)對(duì)象的初始化階段或者跳過(guò)構(gòu)造器的安全檢查,就可以使用這種方法。在上面的例子中,如果將A類(lèi)的構(gòu)造函數(shù)改為private類(lèi)型,將無(wú)法通過(guò)構(gòu)造函數(shù)和反射創(chuàng)建對(duì)象,但allocateInstance方法仍然有效。

          4、數(shù)組操作

          在Unsafe中,可以使用arrayBaseOffset方法可以獲取數(shù)組中第一個(gè)元素的偏移地址,使用arrayIndexScale方法可以獲取數(shù)組中元素間的偏移地址增量。使用下面的代碼進(jìn)行測(cè)試:

          private void arrayTest() {
              String[] array=new String[]{"str1str1str","str2","str3"};
              int baseOffset = unsafe.arrayBaseOffset(String[].class);
              System.out.println(baseOffset);
              int scale = unsafe.arrayIndexScale(String[].class);
              System.out.println(scale);

              for (int i = 0; i < array.length; i++) {
                  int offset=baseOffset+scale*i;
                  System.out.println(offset+" : "+unsafe.getObject(array,offset));
              }
          }

          上面代碼的輸出結(jié)果為:

          16
          4
          16 : str1str1str
          20 : str2
          24 : str3

          通過(guò)配合使用數(shù)組偏移首地址和各元素間偏移地址的增量,可以方便的定位到數(shù)組中的元素在內(nèi)存中的位置,進(jìn)而通過(guò)getObject方法直接獲取任意位置的數(shù)組元素。需要說(shuō)明的是,arrayIndexScale獲取的并不是數(shù)組中元素占用的大小,而是地址的增量,按照openJDK中的注釋,可以將它翻譯為元素尋址的轉(zhuǎn)換因子scale factor for addressing elements)。在上面的例子中,第一個(gè)字符串長(zhǎng)度為11字節(jié),但其地址增量仍然為4字節(jié)。

          那么,基于這兩個(gè)值是如何實(shí)現(xiàn)的尋址和數(shù)組元素的訪問(wèn)呢,這里需要借助一點(diǎn)在前面的文章中講過(guò)的Java對(duì)象內(nèi)存布局的知識(shí),先把上面例子中的String數(shù)組對(duì)象的內(nèi)存布局畫(huà)出來(lái),就很方便大家理解了:

          在String數(shù)組對(duì)象中,對(duì)象頭包含3部分,mark word標(biāo)記字占用8字節(jié),klass point類(lèi)型指針占用4字節(jié),數(shù)組對(duì)象特有的數(shù)組長(zhǎng)度部分占用4字節(jié),總共占用了16字節(jié)。第一個(gè)String的引用類(lèi)型相對(duì)于對(duì)象的首地址的偏移量是就16,之后每個(gè)元素在這個(gè)基礎(chǔ)上加4,正好對(duì)應(yīng)了我們上面代碼中的尋址過(guò)程,之后再使用前面說(shuō)過(guò)的getObject方法,通過(guò)數(shù)組對(duì)象可以獲得對(duì)象在堆中的首地址,再配合對(duì)象中變量的偏移量,就能獲得每一個(gè)變量的引用。

          5、CAS操作

          juc包的并發(fā)工具類(lèi)中大量地使用了CAS操作,像在前面介紹synchronizedAQS的文章中也多次提到了CAS,其作為樂(lè)觀鎖在并發(fā)工具類(lèi)中廣泛發(fā)揮了作用。在Unsafe類(lèi)中,提供了compareAndSwapObjectcompareAndSwapIntcompareAndSwapLong方法來(lái)實(shí)現(xiàn)的對(duì)Objectintlong類(lèi)型的CAS操作。以compareAndSwapInt方法為例:

          public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);

          參數(shù)中o為需要更新的對(duì)象,offset是對(duì)象o中整形字段的偏移量,如果這個(gè)字段的值與expected相同,則將字段的值設(shè)為x這個(gè)新值,并且此更新是不可被中斷的,也就是一個(gè)原子操作。下面是一個(gè)使用compareAndSwapInt的例子:

          private volatile int a;
          public static void main(String[] args){
              CasTest casTest=new CasTest();
              new Thread(()->{
                  for (int i = 1; i < 5; i++) {
                      casTest.increment(i);
                      System.out.print(casTest.a+" ");
                  }
              }).start();
              new Thread(()->{
                  for (int i = 5 ; i <10 ; i++) {
                      casTest.increment(i);
                      System.out.print(casTest.a+" ");
                  }
              }).start();
          }

          private void increment(int x){
              while (true){
                  try {
                      long fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a"));
                      if (unsafe.compareAndSwapInt(this,fieldOffset,x-1,x))
                          break;
                  } catch (NoSuchFieldException e) {
                      e.printStackTrace();
                  }
              }
          }

          運(yùn)行代碼會(huì)依次輸出:

          1 2 3 4 5 6 7 8 9 

          在上面的例子中,使用兩個(gè)線程去修改int型屬性a的值,并且只有在a的值等于傳入的參數(shù)x減一時(shí),才會(huì)將a的值變?yōu)?code style="">x,也就是實(shí)現(xiàn)對(duì)a的加一的操作。流程如下所示:

          需要注意的是,在調(diào)用compareAndSwapInt方法后,會(huì)直接返回truefalse的修改結(jié)果,因此需要我們?cè)诖a中手動(dòng)添加自旋的邏輯。在AtomicInteger類(lèi)的設(shè)計(jì)中,也是采用了將compareAndSwapInt的結(jié)果作為循環(huán)條件,直至修改成功才退出死循環(huán)的方式來(lái)實(shí)現(xiàn)的原子性的自增操作。

          6、線程調(diào)度

          Unsafe類(lèi)中提供了parkunparkmonitorEntermonitorExittryMonitorEnter方法進(jìn)行線程調(diào)度,在前面介紹AQS的文章中我們提到過(guò)使用LockSupport掛起或喚醒指定線程,看一下LockSupport的源碼,可以看到它也是調(diào)用的Unsafe類(lèi)中的方法:

          public static void park(Object blocker) {
              Thread t = Thread.currentThread();
              setBlocker(t, blocker);
              UNSAFE.park(false0L);
              setBlocker(t, null);
          }
          public static void unpark(Thread thread) {
              if (thread != null)
                  UNSAFE.unpark(thread);
          }

          LockSupport的park方法調(diào)用了Unsafe的park方法來(lái)阻塞當(dāng)前線程,此方法將線程阻塞后就不會(huì)繼續(xù)往后執(zhí)行,直到有其他線程調(diào)用unpark方法喚醒當(dāng)前線程。下面的例子對(duì)Unsafe的這兩個(gè)方法進(jìn)行測(cè)試:

          public static void main(String[] args) {
              Thread mainThread = Thread.currentThread();
              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");
          }

          程序輸出為:

          park main mainThread
          subThread try to unpark mainThread
          unpark mainThread success

          程序運(yùn)行的流程也比較容易看懂,子線程開(kāi)始運(yùn)行后先進(jìn)行睡眠,確保主線程能夠調(diào)用park方法阻塞自己,子線程在睡眠5秒后,調(diào)用unpark方法喚醒主線程,使主線程能繼續(xù)向下執(zhí)行。整個(gè)流程如下圖所示:

          此外,Unsafe源碼中monitor相關(guān)的三個(gè)方法已經(jīng)被標(biāo)記為deprecated,不建議被使用:

          //獲得對(duì)象鎖
          @Deprecated
          public native void monitorEnter(Object var1);
          //釋放對(duì)象鎖
          @Deprecated
          public native void monitorExit(Object var1);
          //嘗試獲得對(duì)象鎖
          @Deprecated
          public native boolean tryMonitorEnter(Object var1);

          monitorEnter方法用于獲得對(duì)象鎖,monitorExit用于釋放對(duì)象鎖,如果對(duì)一個(gè)沒(méi)有被monitorEnter加鎖的對(duì)象執(zhí)行此方法,會(huì)拋出IllegalMonitorStateException異常。tryMonitorEnter方法嘗試獲取對(duì)象鎖,如果成功則返回true,反之返回false

          7、Class操作

          Unsafe對(duì)Class的相關(guān)操作主要包括類(lèi)加載和靜態(tài)變量的操作方法。

          a、靜態(tài)屬性讀取相關(guān)的方法:

          //獲取靜態(tài)屬性的偏移量
          public native long staticFieldOffset(Field f);
          //獲取靜態(tài)屬性的對(duì)象指針
          public native Object staticFieldBase(Field f);
          //判斷類(lèi)是否需要實(shí)例化(用于獲取類(lèi)的靜態(tài)屬性前進(jìn)行檢測(cè))
          public native boolean shouldBeInitialized(Class<?> c);

          創(chuàng)建一個(gè)包含靜態(tài)屬性的類(lèi),進(jìn)行測(cè)試:

          @Data
          public class User {
              public static String name="Hydra";
              int age;
          }
          private void staticTest() throws Exception {
              User user=new User();
              System.out.println(unsafe.shouldBeInitialized(User.class));
              Field sexField = User.class.getDeclaredField("name");
              long fieldOffset = unsafe.staticFieldOffset(sexField);
              Object fieldBase = unsafe.staticFieldBase(sexField);
              Object object = unsafe.getObject(fieldBase, fieldOffset);
              System.out.println(object);
          }

          運(yùn)行結(jié)果:

          false
          Hydra

          在Unsafe的對(duì)象操作中,我們學(xué)習(xí)了通過(guò)objectFieldOffset方法獲取對(duì)象屬性偏移量并基于它對(duì)變量的值進(jìn)行存取,但是它不適用于類(lèi)中的靜態(tài)屬性,這時(shí)候就需要使用staticFieldOffset方法。在上面的代碼中,只有在獲取Field對(duì)象的過(guò)程中依賴到了Class,而獲取靜態(tài)變量的屬性時(shí)不再依賴于Class

          在上面的代碼中首先創(chuàng)建一個(gè)User對(duì)象,這是因?yàn)槿绻粋€(gè)類(lèi)沒(méi)有被實(shí)例化,那么它的靜態(tài)屬性也不會(huì)被初始化,最后獲取的字段屬性將是null。所以在獲取靜態(tài)屬性前,需要調(diào)用shouldBeInitialized方法,判斷在獲取前是否需要初始化這個(gè)類(lèi)。如果刪除創(chuàng)建User對(duì)象的語(yǔ)句,運(yùn)行結(jié)果會(huì)變?yōu)椋?/p>

          true
          null

          b、使用defineClass方法允許程序在運(yùn)行時(shí)動(dòng)態(tài)地創(chuàng)建一個(gè)類(lèi),方法定義如下:

          public native Class<?> defineClass(String name, byte[] b, int off, int len,
                                             ClassLoader loader,ProtectionDomain protectionDomain);

          在實(shí)際使用過(guò)程中,可以只傳入字節(jié)數(shù)組、起始字節(jié)的下標(biāo)以及讀取的字節(jié)長(zhǎng)度,默認(rèn)情況下,類(lèi)加載器(ClassLoader)和保護(hù)域(ProtectionDomain)來(lái)源于調(diào)用此方法的實(shí)例。下面的例子中實(shí)現(xiàn)了反編譯生成后的class文件的功能:

          private static void defineTest() {
              String fileName="F:\\workspace\\unsafe-test\\target\\classes\\com\\cn\\model\\User.class";
              File file = new File(fileName);
              try(FileInputStream fis = new FileInputStream(file)) {
                  byte[] content=new byte[(int)file.length()];
                  fis.read(content);
                  Class clazz = unsafe.defineClass(null, content, 0, content.length, nullnull);
                  Object o = clazz.newInstance();
                  Object age = clazz.getMethod("getAge").invoke(o, null);
                  System.out.println(age);
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }

          在上面的代碼中,首先讀取了一個(gè)class文件并通過(guò)文件流將它轉(zhuǎn)化為字節(jié)數(shù)組,之后使用defineClass方法動(dòng)態(tài)的創(chuàng)建了一個(gè)類(lèi),并在后續(xù)完成了它的實(shí)例化工作,流程如下圖所示,并且通過(guò)這種方式創(chuàng)建的類(lèi),會(huì)跳過(guò)JVM的所有安全檢查。

          除了defineClass方法外,Unsafe還提供了一個(gè)defineAnonymousClass方法:

          public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

          使用該方法可以用來(lái)動(dòng)態(tài)的創(chuàng)建一個(gè)匿名類(lèi),在Lambda表達(dá)式中就是使用ASM動(dòng)態(tài)生成字節(jié)碼,然后利用該方法定義實(shí)現(xiàn)相應(yīng)的函數(shù)式接口的匿名類(lèi)。在jdk15發(fā)布的新特性中,在隱藏類(lèi)(Hidden classes)一條中,指出將在未來(lái)的版本中棄用Unsafe的defineAnonymousClass方法。

          8、系統(tǒng)信息

          Unsafe中提供的addressSizepageSize方法用于獲取系統(tǒng)信息,調(diào)用addressSize方法會(huì)返回系統(tǒng)指針的大小,如果在64位系統(tǒng)下默認(rèn)會(huì)返回8,而32位系統(tǒng)則會(huì)返回4。調(diào)用pageSize方法會(huì)返回內(nèi)存頁(yè)的大小,值為2的整數(shù)冪。使用下面的代碼可以直接進(jìn)行打印:

          private void systemTest() {
              System.out.println(unsafe.addressSize());
              System.out.println(unsafe.pageSize());
          }

          執(zhí)行結(jié)果:

          8
          4096

          這兩個(gè)方法的應(yīng)用場(chǎng)景比較少,在java.nio.Bits類(lèi)中,在使用pageCount計(jì)算所需的內(nèi)存頁(yè)的數(shù)量時(shí),調(diào)用了pageSize方法獲取內(nèi)存頁(yè)的大小。另外,在使用copySwapMemory方法拷貝內(nèi)存時(shí),調(diào)用了addressSize方法,檢測(cè)32位系統(tǒng)的情況。

          總結(jié)

          在本文中,我們首先介紹了Unsafe的基本概念、工作原理,并在此基礎(chǔ)上,對(duì)它的API進(jìn)行了說(shuō)明與實(shí)踐。相信大家通過(guò)這一過(guò)程,能夠發(fā)現(xiàn)Unsafe在某些場(chǎng)景下,確實(shí)能夠?yàn)槲覀兲峁┚幊讨械谋憷5腔氐介_(kāi)頭的話題,在使用這些便利時(shí),確實(shí)存在著一些安全上的隱患,在我看來(lái),一項(xiàng)技術(shù)具有不安全因素并不可怕,可怕的是它在使用過(guò)程中被濫用。盡管之前有傳言說(shuō)會(huì)在java9中移除Unsafe類(lèi),不過(guò)它還是照樣已經(jīng)存活到了jdk16,按照存在即合理的邏輯,只要使用得當(dāng),它還是能給我們帶來(lái)不少的幫助,因此最后還是建議大家,在使用Unsafe的過(guò)程中一定要做到使用謹(jǐn)慎使用、避免濫用。


          1、最牛逼的 Java 日志框架,性能無(wú)敵,橫掃所有對(duì)手!
          2、把Redis當(dāng)作隊(duì)列來(lái)用,真的合適嗎?
          3、驚呆了,Spring Boot居然這么耗內(nèi)存!你知道嗎?
          4、牛逼哄哄的 BitMap,到底牛逼在哪?
          5、全網(wǎng)最全 Java 日志框架適配方案!還有誰(shuí)不會(huì)?
          6、30個(gè)IDEA插件總有一款適合你
          7、Spring中毒太深,離開(kāi)Spring我居然連最基本的接口都不會(huì)寫(xiě)了

          點(diǎn)分享

          點(diǎn)收藏

          點(diǎn)點(diǎn)贊

          點(diǎn)在看

          瀏覽 41
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩人妻在线视频 | 大屌视频在线观看 | 日本三级日产三级国产三级 | 日本不卡中文 | 阴阴婷婷小视频 |