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


這是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類的功能圖:

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的地方:

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,每天獲取最新文章!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM