java高併發系列 - 第22天:java中底層工具類Unsafe,高手必須要了解

路人甲Java發表於2019-08-06

這是java高併發系列第22篇文章,文章基於jdk1.8環境。

本文主要內容

  1. 基本介紹
  2. 通過反射獲取Unsafe例項
  3. Unsafe中的CAS操作
  4. Unsafe中原子操作相關方法介紹
  5. Unsafe中執行緒排程相關方法
  6. park和unpark示例
  7. Unsafe鎖示例
  8. Unsafe中保證變數的可見性
  9. Unsafe中Class相關方法
  10. 示例:staticFieldOffset、staticFieldBase、staticFieldBase
  11. 示例:shouldBeInitialized、ensureClassInitialized
  12. 物件操作的其他方法
  13. 繞過構造方法建立物件
  14. 陣列相關的一些方法
  15. 記憶體屏障相關操作
  16. java高併發系列目錄

基本介紹

最近我們一直在學習java高併發,java高併發中主要涉及到類位於java.util.concurrent包中,簡稱juc,juc中大部分類都是依賴於Unsafe來實現的,主要用到了Unsafe中的CAS、執行緒掛起、執行緒恢復等相關功能。所以如果打算深入瞭解JUC原理的,必須先了解一下Unsafe類。

先上一幅Unsafe類的功能圖:

java高併發系列 - 第22天:java中底層工具類Unsafe,高手必須要了解

Unsafe是位於sun.misc包下的一個類,主要提供一些用於執行低階別、不安全操作的方法,如直接訪問系統記憶體資源、自主管理記憶體資源等,這些方法在提升Java執行效率、增強Java語言底層資源操作能力方面起到了很大的作用。但由於Unsafe類使Java語言擁有了類似C語言指標一樣操作記憶體空間的能力,這無疑也增加了程式發生相關指標問題的風險。在程式中過度、不正確使用Unsafe類會使得程式出錯的概率變大,使得Java這種安全的語言變得不再“安全”,因此對Unsafe的使用一定要慎重。

從Unsafe功能圖上看出,Unsafe提供的API大致可分為記憶體操作CASClass相關物件操作執行緒排程系統資訊獲取記憶體屏障陣列操作等幾類,本文主要介紹3個常用的操作:CAS、執行緒排程、物件操作。

看一下UnSafe的原碼部分:

public final class Unsafe {
  // 單例物件
  private static final Unsafe theUnsafe;

  private Unsafe() {
  }
  @CallerSensitive
  public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    // 僅在引導類載入器`BootstrapClassLoader`載入時才合法
    if(!VM.isSystemDomainLoader(var0.getClassLoader())) {    
      throw new SecurityException("Unsafe");
    } else {
      return theUnsafe;
    }
  }
}

從程式碼中可以看出,Unsafe類為單例實現,提供靜態方法getUnsafe獲取Unsafe例項,內部會判斷當前呼叫者是否是由系統類載入器載入的,如果不是系統類載入器載入的,會丟擲SecurityException異常。

那我們想使用這個類,如何獲取呢?

可以把我們的類放在jdk的lib目錄下,那麼啟動的時候會自動載入,這種方式不是很好。

我們學過反射,通過反射可以獲取到Unsafe中的theUnsafe欄位的值,這樣可以獲取到Unsafe物件的例項。

通過反射獲取Unsafe例項

程式碼如下:

package com.itsoku.chat21;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * 跟著阿里p7學併發,微信公眾號:javacode2018
 */
public class Demo1 {
    static Unsafe unsafe;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        System.out.println(unsafe);
    }
}

輸出:

sun.misc.Unsafe@76ed5528

Unsafe中的CAS操作

看一下Unsafe中CAS相關方法定義:

/**
 * CAS 操作
 *
 * @param o        包含要修改field的物件
 * @param offset   物件中某field的偏移量
 * @param expected 期望值
 * @param update   更新值
 * @return true | false
 */
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);

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

什麼是CAS? 即比較並替換,實現併發演算法時常用到的一種技術。CAS操作包含三個運算元——記憶體位置、預期原值及新值執行CAS操作的時候,將記憶體位置的值與預期原值比較,如果相匹配,那麼處理器會自動將該位置值更新為新值,否則,處理器不做任何操作,多個執行緒同時執行cas操作,只有一個會成功。我們都知道,CAS是一條CPU的原子指令(cmpxchg指令),不會造成所謂的資料不一致問題,Unsafe提供的CAS方法(如compareAndSwapXXX)底層實現即為CPU指令cmpxchg。執行cmpxchg指令的時候,會判斷當前系統是否為多核系統,如果是就給匯流排加鎖,只有一個執行緒會對匯流排加鎖成功,加鎖成功之後會執行cas操作,也就是說CAS的原子性實際上是CPU實現的, 其實在這一點上還是有排他鎖的,只是比起用synchronized, 這裡的排他時間要短的多, 所以在多執行緒情況下效能會比較好。

說一下offset,offeset為欄位的偏移量,每個物件有個地址,offset是欄位相對於物件地址的偏移量,物件地址記為baseAddress,欄位偏移量記為offeset,那麼欄位對應的實際地址就是baseAddress+offeset,所以cas通過物件、偏移量就可以去操作欄位對應的值了。

CAS在java.util.concurrent.atomic相關類、Java AQS、JUC中併發集合等實現上有非常廣泛的應用,我們看一下java.util.concurrent.atomic.AtomicInteger類,這個類可以在多執行緒環境中對int型別的資料執行高效的原子修改操作,並保證資料的正確性,看一下此類中用到Unsafe cas的地方:

java高併發系列 - 第22天:java中底層工具類Unsafe,高手必須要了解

java高併發系列 - 第22天:java中底層工具類Unsafe,高手必須要了解

JUC中其他地方使用到CAS的地方就不列舉了,有興趣的可以去看一下原始碼。

Unsafe中原子操作相關方法介紹

5個方法,看一下實現:

/**
 * int型別值原子操作,對var2地址對應的值做原子增加操作(增加var4)
 *
 * @param var1 操作的物件
 * @param var2 var2欄位記憶體地址偏移量
 * @param var4 需要加的值
 * @return
 */
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

/**
 * long型別值原子操作,對var2地址對應的值做原子增加操作(增加var4)
 *
 * @param var1 操作的物件
 * @param var2 var2欄位記憶體地址偏移量
 * @param var4 需要加的值
 * @return 返回舊值
 */
public final long getAndAddLong(Object var1, long var2, long var4) {
    long var6;
    do {
        var6 = this.getLongVolatile(var1, var2);
    } while (!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

    return var6;
}

/**
 * int型別值原子操作方法,將var2地址對應的值置為var4
 *
 * @param var1 操作的物件
 * @param var2 var2欄位記憶體地址偏移量
 * @param var4 新值
 * @return 返回舊值
 */
public final int getAndSetInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while (!this.compareAndSwapInt(var1, var2, var5, var4));

    return var5;
}

/**
 * long型別值原子操作方法,將var2地址對應的值置為var4
 *
 * @param var1 操作的物件
 * @param var2 var2欄位記憶體地址偏移量
 * @param var4 新值
 * @return 返回舊值
 */
public final long getAndSetLong(Object var1, long var2, long var4) {
    long var6;
    do {
        var6 = this.getLongVolatile(var1, var2);
    } while (!this.compareAndSwapLong(var1, var2, var6, var4));

    return var6;
}

/**
 * Object型別值原子操作方法,將var2地址對應的值置為var4
 *
 * @param var1 操作的物件
 * @param var2 var2欄位記憶體地址偏移量
 * @param var4 新值
 * @return 返回舊值
 */
public final Object getAndSetObject(Object var1, long var2, Object var4) {
    Object var5;
    do {
        var5 = this.getObjectVolatile(var1, var2);
    } while (!this.compareAndSwapObject(var1, var2, var5, var4));

    return var5;
}

看一下上面的方法,內部通過自旋的CAS操作實現的,這些方法都可以保證操作的資料在多執行緒環境中的原子性,正確性。

來個示例,我們還是來實現一個網站計數功能,同時有100個人發起對網站的請求,每個人發起10次請求,每次請求算一次,最終結果是1000次,程式碼如下:

package com.itsoku.chat21;

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * 跟著阿里p7學併發,微信公眾號:javacode2018
 */
public class Demo2 {
    static Unsafe unsafe;
    //用來記錄網站訪問量,每次訪問+1
    static int count;
    //count在Demo.class物件中的地址偏移量
    static long countOffset;

    static {
        try {
            //獲取Unsafe物件
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);

            Field countField = Demo2.class.getDeclaredField("count");
            //獲取count欄位在Demo2中的記憶體地址的偏移量
            countOffset = unsafe.staticFieldOffset(countField);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //模擬訪問一次
    public static void request() throws InterruptedException {
        //模擬耗時5毫秒
        TimeUnit.MILLISECONDS.sleep(5);
        //對count原子加1
        unsafe.getAndAddInt(Demo2.class, countOffset, 1);
    }

    public static void main(String[] args) throws InterruptedException {
        long starTime = System.currentTimeMillis();
        int threadSize = 100;
        CountDownLatch countDownLatch = new CountDownLatch(threadSize);
        for (int i = 0; i < threadSize; i++) {
            Thread thread = new Thread(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        request();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            });
            thread.start();
        }

        countDownLatch.await();
        long endTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + ",耗時:" + (endTime - starTime) + ",count=" + count);
    }
}

輸出:

main,耗時:114,count=1000

程式碼中我們在靜態塊中通過反射獲取到了Unsafe類的例項,然後獲取Demo2中count欄位記憶體地址偏移量countOffset,main方法中模擬了100個人,每人發起10次請求,等到所有請求完畢之後,輸出count的結果。

程式碼中用到了CountDownLatch,通過countDownLatch.await()讓主執行緒等待,等待100個子執行緒都執行完畢之後,主執行緒在進行執行。CountDownLatch的使用可以參考:java高併發系列 - 第16天:JUC中等待多執行緒完成的工具類CountDownLatch,必備技能

Unsafe中執行緒排程相關方法

這部分,包括執行緒掛起、恢復、鎖機制等方法。

//取消阻塞執行緒
public native void unpark(Object thread);
//阻塞執行緒,isAbsolute:是否是絕對時間,如果為true,time是一個絕對時間,如果為false,time是一個相對時間,time表示納秒
public native void park(boolean isAbsolute, long time);
//獲得物件鎖(可重入鎖)
@Deprecated
public native void monitorEnter(Object o);
//釋放物件鎖
@Deprecated
public native void monitorExit(Object o);
//嘗試獲取物件鎖
@Deprecated
public native boolean tryMonitorEnter(Object o);

呼叫park後,執行緒將被阻塞,直到unpark呼叫或者超時,如果之前呼叫過unpark,不會進行阻塞,即parkunpark不區分先後順序。monitorEnter、monitorExit、tryMonitorEnter 3個方法已過期,不建議使用了。

park和unpark示例

程式碼如下:

package com.itsoku.chat21;

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;

/**
 * 跟著阿里p7學併發,微信公眾號:javacode2018
 */
public class Demo3 {
    static Unsafe unsafe;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 呼叫park和unpark,模擬執行緒的掛起和喚醒
     *
     * @throws InterruptedException
     */
    public static void m1() throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",start");
            unsafe.park(false, 0);
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",end");
        });
        thread.setName("thread1");
        thread.start();

        TimeUnit.SECONDS.sleep(5);
        unsafe.unpark(thread);
    }

    /**
     * 阻塞指定的時間
     */
    public static void m2() {
        Thread thread = new Thread(() -> {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",start");
            //執行緒掛起3秒
            unsafe.park(false, TimeUnit.SECONDS.toNanos(3));
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",end");
        });
        thread.setName("thread2");
        thread.start();
    }

    public static void main(String[] args) throws InterruptedException {
        m1();
        m2();
    }
}

輸出:

1565000238474,thread1,start
1565000243475,thread1,end
1565000243475,thread2,start
1565000246476,thread2,end

m1()中thread1呼叫park方法,park方法會將當前執行緒阻塞,被阻塞了5秒之後,被主執行緒呼叫unpark方法給喚醒了,unpark方法參數列示需要喚醒的執行緒。

執行緒中相當於有個許可,許可預設是0,呼叫park的時候,發現是0會阻塞當前執行緒,呼叫unpark之後,許可會被置為1,並會喚醒當前執行緒。如果在park之前先呼叫了unpark方法,執行park方法的時候,不會阻塞。park方法被喚醒之後,許可又會被置為0。多次呼叫unpark的效果是一樣的,許可還是1。

juc中的LockSupport類是通過unpark和park方法實現的,需要了解LockSupport可以移步:JUC中的LockSupport工具類

Unsafe鎖示例

程式碼如下:

package com.itsoku.chat21;

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * 跟著阿里p7學併發,微信公眾號:javacode2018
 */
public class Demo4 {

    static Unsafe unsafe;
    //用來記錄網站訪問量,每次訪問+1
    static int count;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //模擬訪問一次
    public static void request() {
        unsafe.monitorEnter(Demo4.class);
        try {
            count++;
        } finally {
            unsafe.monitorExit(Demo4.class);
        }
    }


    public static void main(String[] args) throws InterruptedException {
        long starTime = System.currentTimeMillis();
        int threadSize = 100;
        CountDownLatch countDownLatch = new CountDownLatch(threadSize);
        for (int i = 0; i < threadSize; i++) {
            Thread thread = new Thread(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        request();
                    }
                } finally {
                    countDownLatch.countDown();
                }
            });
            thread.start();
        }

        countDownLatch.await();
        long endTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + ",耗時:" + (endTime - starTime) + ",count=" + count);
    }
}

輸出:

main,耗時:64,count=1000

monitorEnter、monitorExit都有1個引數,表示上鎖的物件。用法和synchronized關鍵字語義類似。

注意:

  1. monitorEnter、monitorExit、tryMonitorEnter 3個方法已過期,不建議使用了
  2. monitorEnter、monitorExit必須成對出現,出現的次數必須一致,也就是說鎖了n次,也必須釋放n次,否則會造成死鎖

Unsafe中保證變數的可見性

關於變數可見性需要先了解java記憶體模型JMM,可以移步到:

JMM相關的一些概念

volatile與Java記憶體模型

java中操作記憶體分為主記憶體和工作記憶體,共享資料在主記憶體中,執行緒如果需要操作主記憶體的資料,需要先將主記憶體的資料複製到執行緒獨有的工作記憶體中,操作完成之後再將其重新整理到主記憶體中。如執行緒A要想看到執行緒B修改後的資料,需要滿足:執行緒B修改資料之後,需要將資料從自己的工作記憶體中重新整理到主記憶體中,並且A需要去主記憶體中讀取資料。

被關鍵字volatile修飾的資料,有2點語義:

  1. 如果一個變數被volatile修飾,讀取這個變數時候,會強制從主記憶體中讀取,然後將其複製到當前執行緒的工作記憶體中使用
  2. 給volatile修飾的變數賦值的時候,會強制將賦值的結果從工作記憶體重新整理到主記憶體

上面2點語義保證了被volatile修飾的資料在多執行緒中的可見性。

Unsafe中提供了和volatile語義一樣的功能的方法,如下:

//設定給定物件的int值,使用volatile語義,即設定後立馬更新到記憶體對其他執行緒可見
public native void  putIntVolatile(Object o, long offset, int x);
//獲得給定物件的指定偏移量offset的int值,使用volatile語義,總能獲取到最新的int值。
public native int getIntVolatile(Object o, long offset);

putIntVolatile方法,2個引數:

o:表示需要操作的物件

offset:表示操作物件中的某個欄位地址偏移量

x:將offset對應的欄位的值修改為x,並且立即重新整理到主存中

呼叫這個方法,會強制將工作記憶體中修改的資料重新整理到主記憶體中。

getIntVolatile方法,2個引數

o:表示需要操作的物件

offset:表示操作物件中的某個欄位地址偏移量

每次呼叫這個方法都會強制從主記憶體讀取值,將其複製到工作記憶體中使用。

其他的還有幾個putXXXVolatile、getXXXVolatile方法和上面2個類似。

本文主要講解這些內容,希望您能有所收穫,謝謝。

Unsafe中Class相關方法

此部分主要提供Class和它的靜態欄位的操作相關方法,包含靜態欄位記憶體定位、定義類、定義匿名類、檢驗&確保初始化等。

//獲取給定靜態欄位的記憶體地址偏移量,這個值對於給定的欄位是唯一且固定不變的
public native long staticFieldOffset(Field f);
//獲取一個靜態類中給定欄位的物件指標
public native Object staticFieldBase(Field f);
//判斷是否需要初始化一個類,通常在獲取一個類的靜態屬性的時候(因為一個類如果沒初始化,它的靜態屬性也不會初始化)使用。 當且僅當ensureClassInitialized方法不生效時返回false。
public native boolean shouldBeInitialized(Class<?> c);
//檢測給定的類是否已經初始化。通常在獲取一個類的靜態屬性的時候(因為一個類如果沒初始化,它的靜態屬性也不會初始化)使用。
public native void ensureClassInitialized(Class<?> c);
//定義一個類,此方法會跳過JVM的所有安全檢查,預設情況下,ClassLoader(類載入器)和ProtectionDomain(保護域)例項來源於呼叫者
public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
//定義一個匿名類
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

示例:staticFieldOffset、staticFieldBase、staticFieldBase

package com.itsoku.chat21;

import lombok.extern.slf4j.Slf4j;
import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 跟著阿里p7學併發,微信公眾號:javacode2018
 */
@Slf4j
public class Demo7 {

    static Unsafe unsafe;
    //靜態屬性
    private static Object v1;
    //例項屬性
    private Object v2;

    static {
        //獲取Unsafe物件
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws NoSuchFieldException {
        Field v1Field = Demo7.class.getDeclaredField("v1");
        Field v2Field = Demo7.class.getDeclaredField("v2");

        System.out.println(unsafe.staticFieldOffset(v1Field));
        System.out.println(unsafe.objectFieldOffset(v2Field));

        System.out.println(unsafe.staticFieldBase(v1Field)==Demo7.class);
    }
}

輸出:

112
12
true

可以看出staticFieldBase返回的就是Demo2的class物件。

示例:shouldBeInitialized、ensureClassInitialized

package com.itsoku.chat21;

import lombok.extern.slf4j.Slf4j;
import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;

/**
 * 跟著阿里p7學併發,微信公眾號:javacode2018
 */
public class Demo8 {

    static Unsafe unsafe;

    static {
        //獲取Unsafe物件
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class C1 {
        private static int count;

        static {
            count = 10;
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",C1 static init.");
        }
    }

    static class C2 {
        private static int count;

        static {
            count = 11;
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",C2 static init.");
        }
    }

    public static void main(String[] args) throws NoSuchFieldException {
        //判斷C1類是需要需要初始化,如果已經初始化了,會返回false,如果此類沒有被初始化過,返回true
        if (unsafe.shouldBeInitialized(C1.class)) {
            System.out.println("C1需要進行初始化");
            //對C1進行初始化
            unsafe.ensureClassInitialized(C1.class);
        }

        System.out.println(C2.count);
        System.out.println(unsafe.shouldBeInitialized(C1.class));
    }
}

輸出:

C1需要進行初始化
1565069660679,main,C1 static init.
1565069660680,main,C2 static init.
11
false

程式碼中C1未被初始化過,所以unsafe.shouldBeInitialized(C1.class)返回true,然後呼叫unsafe.ensureClassInitialized(C1.class)進行初始化。

程式碼中執行C2.count會觸發C2進行初始化,所以shouldBeInitialized(C1.class)返回false

物件操作的其他方法

//返回物件成員屬性在記憶體地址相對於此物件的記憶體地址的偏移量
public native long objectFieldOffset(Field f);
//獲得給定物件的指定地址偏移量的值,與此類似操作還有:getInt,getDouble,getLong,getChar等
public native Object getObject(Object o, long offset);
//給定物件的指定地址偏移量設值,與此類似操作還有:putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);
//從物件的指定偏移量處獲取變數的引用,使用volatile的載入語義
public native Object getObjectVolatile(Object o, long offset);
//儲存變數的引用到物件的指定的偏移量處,使用volatile的儲存語義
public native void putObjectVolatile(Object o, long offset, Object x);
//有序、延遲版本的putObjectVolatile方法,不保證值的改變被其他執行緒立即看到,只有在field被volatile修飾符修飾時有效
public native void putOrderedObject(Object o, long offset, Object x);
//繞過構造方法、初始化程式碼來建立物件
public native Object allocateInstance(Class<?> cls) throws InstantiationException;

getObject相當於獲取物件中欄位的值,putObject相當於給欄位賦值,有興趣的可以自己寫個例子看看效果。

繞過構造方法建立物件

介紹一下allocateInstance,這個方法可以繞過構造方法來建立物件,示例程式碼如下:

package com.itsoku.chat21;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * 跟著阿里p7學併發,微信公眾號:javacode2018
 */
public class Demo9 {

    static Unsafe unsafe;

    static {
        //獲取Unsafe物件
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class C1 {
        private String name;

        private C1() {
            System.out.println("C1 default constructor!");
        }

        private C1(String name) {
            this.name = name;
            System.out.println("C1 有參 constructor!");
        }
    }

    public static void main(String[] args) throws InstantiationException {
        System.out.println(unsafe.allocateInstance(C1.class));
    }
}

輸出:

com.itsoku.chat21.Demo9$C1@782830e

看一下類C1中有兩個構造方法,都是private的,通過new、反射的方式都無法建立物件。但是可以通過Unsafe的allocateInstance方法繞過建構函式來建立C1的例項,輸出的結果中可以看出建立成功了,並且沒有呼叫構造方法。

典型應用

  • 常規物件例項化方式:我們通常所用到的建立物件的方式,從本質上來講,都是通過new機制來實現物件的建立。但是,new機制有個特點就是當類只提供有參的建構函式且無顯示宣告無參建構函式時,則必須使用有參建構函式進行物件構造,而使用有參建構函式時,必須傳遞相應個數的引數才能完成物件例項化。
  • 非常規的例項化方式:而Unsafe中提供allocateInstance方法,僅通過Class物件就可以建立此類的例項物件,而且不需要呼叫其建構函式、初始化程式碼、JVM安全檢查等。它抑制修飾符檢測,也就是即使構造器是private修飾的也能通過此方法例項化,只需提類物件即可建立相應的物件。由於這種特性,allocateInstance在java.lang.invoke、Objenesis(提供繞過類構造器的物件生成方式)、Gson(反序列化時用到)中都有相應的應用。

陣列相關的一些方法

這部分主要介紹與資料操作相關的arrayBaseOffset與arrayIndexScale這兩個方法,兩者配合起來使用,即可定位陣列中每個元素在記憶體中的位置。

//返回陣列中第一個元素的偏移地址
public native int arrayBaseOffset(Class<?> arrayClass);
//返回陣列中一個元素佔用的大小
public native int arrayIndexScale(Class<?> arrayClass);

這兩個與資料操作相關的方法,在java.util.concurrent.atomic 包下的AtomicIntegerArray(可以實現對Integer陣列中每個元素的原子性操作)中有典型的應用,如下圖AtomicIntegerArray原始碼所示,通過Unsafe的arrayBaseOffset、arrayIndexScale分別獲取陣列首元素的偏移地址base及單個元素大小因子scale。後續相關原子性操作,均依賴於這兩個值進行陣列中元素的定位,如下圖二所示的getAndAdd方法即通過checkedByteOffset方法獲取某陣列元素的偏移地址,而後通過CAS實現原子性操作。

陣列元素定位:

Unsafe類中有很多以BASE_OFFSET結尾的常量,比如ARRAY_INT_BASE_OFFSET,ARRAY_BYTE_BASE_OFFSET等,這些常量值是通過arrayBaseOffset方法得到的。arrayBaseOffset方法是一個本地方法,可以獲取陣列第一個元素的偏移地址。Unsafe類中還有很多以INDEX_SCALE結尾的常量,比如 ARRAY_INT_INDEX_SCALE , ARRAY_BYTE_INDEX_SCALE等,這些常量值是通過arrayIndexScale方法得到的。arrayIndexScale方法也是一個本地方法,可以獲取陣列的轉換因子,也就是陣列中元素的增量地址。將arrayBaseOffset與arrayIndexScale配合使用,可以定位陣列中每個元素在記憶體中的位置。

記憶體屏障相關操作

在Java 8中引入,用於定義記憶體屏障(也稱記憶體柵欄,記憶體柵障,屏障指令等,是一類同步屏障指令,是CPU或編譯器在對記憶體隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執行後才可以開始執行此點之後的操作),避免程式碼重排序。

//記憶體屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障後,屏障後的load操作不能被重排序到屏障前
public native void loadFence();
//記憶體屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障後,屏障後的store操作不能被重排序到屏障前
public native void storeFence();
//記憶體屏障,禁止load、store操作重排序
public native void fullFence();

Unsafe相關的就介紹這麼多!

java高併發系列目錄

  1. java高併發系列 - 第1天:必須知道的幾個概念
  2. java高併發系列 - 第2天:併發級別
  3. java高併發系列 - 第3天:有關並行的兩個重要定律
  4. java高併發系列 - 第4天:JMM相關的一些概念
  5. java高併發系列 - 第5天:深入理解程式和執行緒
  6. java高併發系列 - 第6天:執行緒的基本操作
  7. java高併發系列 - 第7天:volatile與Java記憶體模型
  8. java高併發系列 - 第8天:執行緒組
  9. java高併發系列 - 第9天:使用者執行緒和守護執行緒
  10. java高併發系列 - 第10天:執行緒安全和synchronized關鍵字
  11. java高併發系列 - 第11天:執行緒中斷的幾種方式
  12. java高併發系列 - 第12天JUC:ReentrantLock重入鎖
  13. java高併發系列 - 第13天:JUC中的Condition物件
  14. java高併發系列 - 第14天:JUC中的LockSupport工具類,必備技能
  15. java高併發系列 - 第15天:JUC中的Semaphore(訊號量)
  16. java高併發系列 - 第16天:JUC中等待多執行緒完成的工具類CountDownLatch,必備技能
  17. java高併發系列 - 第17天:JUC中的迴圈柵欄CyclicBarrier的6種使用場景
  18. java高併發系列 - 第18天:JAVA執行緒池,這一篇就夠了
  19. java高併發系列 - 第19天:JUC中的Executor框架詳解1
  20. java高併發系列 - 第20天:JUC中的Executor框架詳解2
  21. java高併發系列 - 第21天:java中的CAS,你需要知道的東西

java高併發系列連載中,總計估計會有四五十篇文章。

阿里p7一起學併發,公眾號:路人甲java,每天獲取最新文章!

java高併發系列 - 第22天:java中底層工具類Unsafe,高手必須要了解

相關文章