1. 程式人生 > >java高併發系列 - 第22天:java中底層工具類Unsafe,高手必須要了解

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大致可分為記憶體操作、CAS、Class相關、物件操作、執行緒排程、系統資訊獲取、記憶體屏障、陣列操作等幾類,本文主要介紹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實現原子性操作。

記憶體屏障相關操作

在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併發系列 - 22java底層工具Unsafe高手必須

這是java高併發系列第22篇文章,文章基於jdk1.8環境。 本文主要內容 基本介紹 通過反射獲取Unsafe例項 Unsafe中的CAS操作 Unsafe中原子操作相關方法介紹 Unsafe中執行緒排程相關方法 park和unpark示例 Unsafe鎖示例 Unsafe中保證變數的可見性 Unsafe

java併發系列 - 14JUC的LockSupport工具必備技能

這是java高併發系列第14篇文章。 本文主要內容: 講解3種讓執行緒等待和喚醒的方法,每種方法配合具體的示例 介紹LockSupport主要用法 對比3種方式,瞭解他們之間的區別 LockSupport位於java.util.concurrent(簡稱juc)包中,算是juc中一個基礎類,juc中很多地

java併發系列 - 15JUC的Semaphore最簡單的限流工具必備技能

這是java高併發系列第15篇文章 Semaphore(訊號量)為多執行緒協作提供了更為強大的控制方法,前面的文章中我們學了synchronized和重入鎖ReentrantLock,這2種鎖一次都只能允許一個執行緒訪問一個資源,而訊號量可以控制有多少個執行緒可以同時訪問特定的資源。 Semaphore常用

java併發系列 - 16JUC等待多執行緒完成的工具CountDownLatch必備技能

這是java高併發系列第16篇文章。 本篇內容 介紹CountDownLatch及使用場景 提供幾個示例介紹CountDownLatch的使用 手寫一個並行處理任務的工具類 假如有這樣一個需求,當我們需要解析一個Excel裡多個sheet的資料時,可以考慮使用多執行緒,每個執行緒解析一個sheet裡的資料

java併發系列 - 17JUC的迴圈柵欄CyclicBarrier常見的6種使用場景及程式碼示例

這是java高併發系列第17篇。 本文主要內容: 介紹CyclicBarrier 6個示例介紹CyclicBarrier的使用 對比CyclicBarrier和CountDownLatch CyclicBarrier簡介 CyclicBarrier通常稱為迴圈屏障。它和CountDownLatch很相似,

java併發系列 - 21java的CAS操作java併發的基石

這是java高併發系列第21篇文章。 本文主要內容 從網站計數器實現中一步步引出CAS操作 介紹java中的CAS及CAS可能存在的問題 悲觀鎖和樂觀鎖的一些介紹及資料庫樂觀鎖的一個常見示例 使用java中的原子操作實現網站計數器功能 我們需要解決的問題 需求:我們開發了一個網站,需要對訪問量進行統計,使

java併發系列 - 23JUC原子一篇就夠

這是java高併發系列第23篇文章,環境:jdk1.8。 本文主要內容 JUC中的原子類介紹 介紹基本型別原子類 介紹陣列型別原子類 介紹引用型別原子類 介紹物件屬性修改相關原子類 預備知識 JUC中的原子類都是都是依靠volatile、CAS、Unsafe類配合來實現的,需要了解的請移步: volati

java併發系列 - 24ThreadLocal、InheritableThreadLocal(通俗易懂)

java高併發系列第24篇文章。 環境:jdk1.8。 本文內容 需要解決的問題 介紹ThreadLocal 介紹InheritableThreadLocal 需要解決的問題 我們還是以解決問題的方式來引出ThreadLocal、InheritableThreadLocal,這樣印象會深刻一些。 目前

java併發系列 - 25掌握JUC的阻塞佇列

這是java高併發系列第25篇文章。 環境:jdk1.8。 本文內容 掌握Queue、BlockingQueue介面中常用的方法 介紹6中阻塞佇列,及相關場景示例 重點掌握4種常用的阻塞佇列 Queue介面 佇列是一種先進先出(FIFO)的資料結構,java中用Queue介面來表示佇列。 Queue介面中

java併發系列 - 27實戰篇介面效能成倍提升讓同事刮目相看現學現用

這是java高併發系列第27篇文章。 開發環境:jdk1.8。 案例講解 電商app都有用過吧,商品詳情頁,需要給他們提供一個介面獲取商品相關資訊: 商品基本資訊(名稱、價格、庫存、會員價格等) 商品圖片列表 商品描述資訊(描述資訊一般是由富文字編輯的大文字資訊) 資料庫中我們用了3張表儲存上面的資訊:

java併發系列 - 31獲取執行緒執行結果這6種方法你都知道?

這是java高併發系列第31篇。 環境:jdk1.8。 java高併發系列已經學了不少東西了,本篇文章,我們用前面學的知識來實現一個需求: 在一個執行緒中需要獲取其他執行緒的執行結果,能想到幾種方式?各有什麼優缺點? 結合這個需求,我們使用6種方式,來對之前學過的知識點做一個回顧,加深記憶。 方式1:Thre

java併發系列 - 32併發計數器的實現方式有哪些?

這是java高併發系列第32篇文章。 java環境:jdk1.8。 本文主要內容 4種方式實現計數器功能,對比其效能 介紹LongAdder 介紹LongAccumulator 需求:一個jvm中實現一個計數器功能,需保證多執行緒情況下資料正確性。 我們來模擬50個執行緒,每個執行緒對計數器遞增100萬次

java併發系列-1:必須知道的幾個概念

java高併發系列-第1天:必須知道的幾個概念 同步(Synchronous)和非同步(Asynchronous) 同步和非同步通常來形容一次方法呼叫,同步方法呼叫一旦開始,呼叫者必須等到方法呼叫返回後,才能繼續後續的行為。非同步方法呼叫更像一個訊息傳遞,一旦開始,方法呼叫就會立即返回,呼叫者就可以繼續後續的

java併發系列 - 6:執行緒的基本操作

新建執行緒 新建執行緒很簡單。只需要使用new關鍵字建立一個執行緒物件,然後呼叫它的start()啟動執行緒即可。 Thread thread1 = new Thread1(); t1.start(); 那麼執行緒start()之後,會幹什麼呢?執行緒有個run()方法,start()會建立一個新的執行緒並讓

java併發系列 - 12JUC:ReentrantLock重入鎖

java高併發系列 - 第12天JUC:ReentrantLock重入鎖 本篇文章開始將juc中常用的一些類,估計會有十來篇。 synchronized的侷限性 synchronized是java內建的關鍵字,它提供了一種獨佔的加鎖方式。synchronized的獲取和釋放鎖由jvm實現,使用者不需要顯示的釋

java併發系列 - 29併發常見的限流方式

這是java高併發系列第29篇。 環境:jdk1.8。 本文內容 介紹常見的限流演算法 通過控制最大併發數來進行限流 通過漏桶演算法來進行限流 通過令牌桶演算法來進行限流 限流工具類RateLimiter 常見的限流的場景 秒殺活動,數量有限,訪問量巨大,為了防止系統宕機,需要做限流處理 國慶期間,一般

Java併發程式設計(十)Java併發工具

1. 等待多執行緒完成的CountDownLatch CountDownLatch允許一個或多個執行緒等待其他執行緒完成操作。 1.1 應用場景 假如有這樣一個需求:我們需要解析一個Excel裡多個sheet的資料,此時可以考慮使用多 執行緒,每個執行緒解析一個sheet裡的資料

Java併發程式設計(八)Java併發容器和框架

1. ConcurrentHashMap 1.1 ConcurrentHashMap的優勢 在併發程式設計中使用HashMap可能導致程式死迴圈。而使用執行緒安全的HashTable效率又非 常低下,基於以上兩個原因,便有了ConcurrentHashMap的登場機會。

十三ArrayList的底層add方法原理按自己理解重寫一下

private int size; private Object[] obj;; //1.5,1.6,ArrayList的構造器預設初始化的時候自動賦值為10 //1.7,1.8都是預設為0

22Java執行緒基礎、併發集合

1 概述 2 生命週期(五狀態圖) 3 如何定義一個執行緒 4 控制執行緒執行的方法 1 概述        程式當中一條獨立的執行線索。Java中的主執行緒是一個J