Java不能操作記憶體?Unsafe瞭解一下

来源:https://www.cnblogs.com/star95/archive/2023/08/10/17619438.html
-Advertisement-
Play Games

## 前言 C++可以動態的分類記憶體(但是得主動釋放記憶體,避免記憶體泄漏),而java並不能這樣,java的記憶體分配和垃圾回收統一由JVM管理,是不是java就不能操作記憶體呢?當然有其他辦法可以操作記憶體,接下來有請`Unsafe`出場,我們一起看看`Unsafe`是如何花式操作記憶體的。 ## Unsa ...


前言

C++可以動態的分類記憶體(但是得主動釋放記憶體,避免記憶體泄漏),而java並不能這樣,java的記憶體分配和垃圾回收統一由JVM管理,是不是java就不能操作記憶體呢?當然有其他辦法可以操作記憶體,接下來有請Unsafe出場,我們一起看看Unsafe是如何花式操作記憶體的。

Unsafe介紹

Unsafe見名知意,不安全的意思,因為通過這個類可以繞過JVM的管理直接去操作記憶體,如果操作不當或者使用完成後沒有及時釋放的話,這部分的記憶體不會被回收,久而久之,這種沒有被釋放的記憶體會越來越多造成記憶體泄漏。所以這是一個比較不安全的操作,一般不建議直接使用,畢竟這種問題導致的線上問題很難查出,另外通常的解決辦法就是重啟系統了。
雖然Unsafe是不安全的操作,但可以根據實際情況合理使用可以達到更好的效果,比如像java NIO里就有通過這種方式創建堆外存。Unsafe常用的操作包括:分配記憶體釋放記憶體、實例化及對象操作、數組操作、CAS、線程同步等。

Unsafe實例對象的獲取

Unsafe不能直接獲取到,像下麵這樣使用會直接拋出安全檢查異常。

public static void main(String[] args) throws Exception {
    Unsafe unsafe = Unsafe.getUnsafe();
}

運行結果就這樣:

Exception in thread "main" java.lang.SecurityException: Unsafe
	at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
	at com.star95.study.UnsafeTest.main(UnsafeTest.java:21)

我們來看看sun.misc.Unsafe.getUnsafe這個方法的源碼:

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

這裡會判斷這個類的載入器是否是啟用類載入器Bootstrap ClassLoader,如果不是啟動類載入器載入的則拋異常。
Bootstrap ClassLoader這個類載入器主要載入java核心類庫,比如rt.jar這類包的,java採用的是雙親委托方式載入類,如果父載入器已載入了某個類,則子載入器不再載入,採用這樣的方式進一步加強安全性。關於啟動類載入器Bootstrap ClassLoader、擴展類載入器Extension Classloader、應用程式類載入器Application Classloader這裡不再介紹,感興趣的可自行搜索。
以下介紹3種方法來獲取Unsafe

1、修改啟動類載入器的搜索路徑

public class UnsafeUtil {
    private UnsafeUtil() {}
    public static Unsafe getUnsafe() {
        System.out.println("get getUnsafe...");
        return Unsafe.getUnsafe();
    }
}

就這一個類,打成一個jar包。

把這個jar包的路徑放到jvm啟動參數里-Xbootclasspath/a:D:\work\projects\test\unsafe\target\unsafe-1.0-SNAPSHOT.jar,然後調用即可獲取到Unsafe

關於jvm參數-Xbootclasspath說明:

-Xbootclasspath:新jar路徑(Windows用;分隔,linux用:分隔),這種相當於覆蓋了java預設的核心類搜索路徑,包括核心類庫例如rt.jar,這種基本不用。

-Xbootclasspath/a:新jar路徑(Windows用;分隔,linux用:分隔),這種是把新jar路徑追加到已有的classpath後,相當於擴大了核心類的範圍,這個常用。

-Xbootclasspath/p:新jar路徑(Windows用;分隔,linux用:分隔),這種是把新jar路徑追加到已有的classpath前,也相當於擴大了核心類的範圍,但是放到核心類前可能會引起衝突,這個不常用。

2、利用反射使用構造方法創建實例

Unsafe有一個私有的無參構造方法,利用反射使用構造方法也可以創建Unsafe實例。

Constructor constructor = Unsafe.class.getDeclaredConstructors()[0];
constructor.setAccessible(true);
Unsafe unsafe = (Unsafe)constructor.newInstance();
System.out.println(unsafe);

3、利用反射獲取Unsafe屬性創建實例

Unsafe類里有一個屬性private static final Unsafe theUnsafe;利用反射獲取到這個屬性然後對null這個對象獲取屬性值就會觸發類靜態塊兒的執行,從而達到實例化的目的。

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

以上3種方法雖然都可以獲取到Unsafe的實例,但第三種更常用一些。

Unsafe常用操作

Unsafe類里大概有100多個方法,按用途主要分為以下幾大類,分別介紹。

Unsafe操作記憶體

記憶體操作主要包括記憶體分配、擴展記憶體、設置記憶體值、釋放記憶體等,常用的方法介紹如下。

//分配指定大小的記憶體
public long allocateMemory(long bytes)
//根據給定的記憶體地址address調整記憶體大小
public long reallocateMemory(long address, long bytes)
//設置記憶體值
public void setMemory(Object o, long offset, long bytes, byte value)
public void setMemory(long address, long bytes, byte value)
//記憶體複製,支持兩種地址模式
public void copyMemory(Object srcBase, long srcOffset,  Object destBase, long destOffset, long bytes)
//釋放allocateMemory和reallocateMemory申請的記憶體
public native void freeMemory(long address)

舉個慄子:

public void test() throws Exception {
        Unsafe unsafe = getUnsafe();
        long address = unsafe.allocateMemory(8);
        System.out.println("allocate memory with 8 bytes, address=" + address);
        long data = 13579L;
        unsafe.putLong(address, data);
        System.out.println("direct put data to address, data=" + data);
        System.out.println("get address data=" + unsafe.getLong(address));
        long address1 = unsafe.allocateMemory(8);
        System.out.println("allocate memory with 8 bytes, address1=" + address);
        unsafe.copyMemory(address, address1, 8);
        System.out.println("copy memory with 8 bytes to address1=" + address1);
        System.out.println("get address1 data=" + unsafe.getLong(address1));
        unsafe.reallocateMemory(address1, 16);
        unsafe.setMemory(address1, 16, (byte)20);
        System.out.println("after setMemory address1=" + unsafe.getByte(address1));
        unsafe.freeMemory(address1);
        unsafe.freeMemory(address);
        System.out.println("free memory over");
        long[] l1 = new long[] {11, 22, 33, 44};
        long[] l2 = new long[4];
        long offset = unsafe.arrayBaseOffset(long[].class);
        unsafe.copyMemory(l1, offset, l2, offset, 32);
        System.out.println("l2=" + Arrays.toString(l2));
    }

輸出結果:

allocate memory with 8 bytes, address=510790256
direct put data to address, data=13579
get address data=13579
allocate memory with 8 bytes, address1=510790256
copy memory with 8 bytes to address1=510788736
get address1 data=13579
after setMemory address1=20
free memory over
l2=[11, 22, 33, 44]

Unsafe操作類、對象及變數

下麵介紹關於類、對象及變數相關的一些操作,還有一些其他的方法沒有一一列出,大家可以自行研究。

//實例化對象,不調構造方法
Object allocateInstance(Class<?> cls)
//欄位在記憶體中的地址相對於實例對象記憶體地址的偏移量
public long objectFieldOffset(Field f)
//欄位在記憶體中的地址相對於class記憶體地址的偏移量
public long objectFieldOffset(Class<?> c, String name)
//靜態欄位在class對象中的偏移
public long staticFieldOffset(Field f)
//獲得靜態欄位所對應類對象
public Object staticFieldBase(Field f)
//獲取對象中指定偏移量的int值,這裡還有基本類型的其他其中,比如char,boolean,long等
public native int getInt(Object o, long offset);
//將int值放入指定對象指定偏移量的位置,這裡還有基本類型的其他其中,比如char,boolean,long等
public native void putInt(Object o, long offset, int x);
//獲取obj對象指定offset的屬性對象
public native Object getObject(Object obj, long offset);
//將newObj對象放入指定obj對象指定offset偏移量的位置
public native void putObject(Object obj, long offset, Object newObj);

實戰一下吧

class Cat {
    private String name;
    private long speed;

    public Cat(String name, long speed) {
        this.name = name;
        this.speed = speed;
    }

    public Cat() {
        System.out.println("constructor...");
    }

    static {
        System.out.println("static...");
    }

    @Override
    public String toString() {
        return "Cat{" + "name='" + name + '\'' + ", speed=" + speed + '}';
    }
}

class Foo {
    private int age;
    private Cat cat;
    private static String defaultString = "default........";

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Cat getCat() {
        return cat;
    }

    public void setCat(Cat cat) {
        this.cat = cat;
    }
}

//測試方法
public void test1() throws Exception {
    // 使用allocateInstance方法創建一個Cat實例,這裡不會調用構造方法,但是靜態塊會執行,所以會輸出"static..."
    Cat cat = (Cat)unsafe.allocateInstance(Cat.class);
    System.out.println("allocateInstance cat--->" + cat);
    Foo f = new Foo();
    f.setAge(13);
    f.setCat(new Cat("ketty", 120));
    long ageOffset = unsafe.objectFieldOffset(Foo.class.getDeclaredField("age"));
    // 這個offset的屬性是一個Cat對象
    long catOffset = unsafe.objectFieldOffset(Foo.class.getDeclaredField("cat"));
    // 獲取靜態屬性的時候直接用Class對象,用實例對象的話會發生NPE異常
    long defaultStringOffset = unsafe.staticFieldOffset(Foo.class.getDeclaredField("defaultString"));
    System.out.println("get age=" + unsafe.getInt(f, ageOffset));
    System.out.println("get cat=" + unsafe.getObject(f, catOffset));
    System.out.println("get defaultString=" + unsafe.getObject(Foo.class, defaultStringOffset));
    System.out.println("---------------------");
    // 操作記憶體放入新值
    unsafe.putInt(f, ageOffset, 100);
    unsafe.putObject(f, catOffset, new Cat("hello", 333));
    unsafe.putObject(f, defaultStringOffset, "new default string");
    System.out.println("after put then get age=" + unsafe.getInt(f, ageOffset));
    System.out.println("after put then get cat=" + unsafe.getObject(f, catOffset));
    System.out.println("after put then get defaultString=" + unsafe.getObject(f, defaultStringOffset));
    System.out.println("---------------------");
}

程式輸出如下:

static...
allocateInstance cat--->Cat{name='null', speed=0}
get age=13
get cat=Cat{name='ketty', speed=120}
get defaultString=default........
---------------------
after put then get age=100
after put then get cat=Cat{name='hello', speed=333}
after put then get defaultString=new default string
---------------------

Unsafe操作數組

數組除了8種基本類型外,還包括Object數組。在Unsafe類里是通過靜態塊來獲取這些數據。

public void test2() {
    // 獲取8種基本類型和Object類型數組的基礎偏移量,scale相關的可以理解每個類型對應的值所占的大小
    // 通過輸出信息我們可以看到基礎偏移量都是16,scale除Object的是4外,基礎數據類型的scale就是相應的位元組大小
    System.out.println("Unsafe.ARRAY_BOOLEAN_BASE_OFFSET=" + Unsafe.ARRAY_BOOLEAN_BASE_OFFSET);
    System.out.println("Unsafe.ARRAY_BOOLEAN_INDEX_SCALE=" + Unsafe.ARRAY_BOOLEAN_INDEX_SCALE);
    System.out.println("Unsafe.ARRAY_BYTE_BASE_OFFSET=" + Unsafe.ARRAY_BYTE_BASE_OFFSET);
    System.out.println("Unsafe.ARRAY_BYTE_INDEX_SCALE=" + Unsafe.ARRAY_BYTE_INDEX_SCALE);
    System.out.println("Unsafe.ARRAY_SHORT_BASE_OFFSET=" + Unsafe.ARRAY_SHORT_BASE_OFFSET);
    System.out.println("Unsafe.ARRAY_SHORT_INDEX_SCALE=" + Unsafe.ARRAY_SHORT_INDEX_SCALE);
    System.out.println("Unsafe.ARRAY_CHAR_BASE_OFFSET=" + Unsafe.ARRAY_CHAR_BASE_OFFSET);
    System.out.println("Unsafe.ARRAY_CHAR_INDEX_SCALE=" + Unsafe.ARRAY_CHAR_INDEX_SCALE);
    System.out.println("Unsafe.ARRAY_INT_BASE_OFFSET=" + Unsafe.ARRAY_INT_BASE_OFFSET);
    System.out.println("Unsafe.ARRAY_INT_INDEX_SCALE=" + Unsafe.ARRAY_INT_INDEX_SCALE);
    System.out.println("Unsafe.ARRAY_LONG_BASE_OFFSET=" + Unsafe.ARRAY_LONG_BASE_OFFSET);
    System.out.println("Unsafe.ARRAY_LONG_INDEX_SCALE=" + Unsafe.ARRAY_LONG_INDEX_SCALE);
    System.out.println("Unsafe.ARRAY_FLOAT_BASE_OFFSET=" + Unsafe.ARRAY_FLOAT_BASE_OFFSET);
    System.out.println("Unsafe.ARRAY_FLOAT_INDEX_SCALE=" + Unsafe.ARRAY_FLOAT_INDEX_SCALE);
    System.out.println("Unsafe.ARRAY_DOUBLE_BASE_OFFSET=" + Unsafe.ARRAY_DOUBLE_BASE_OFFSET);
    System.out.println("Unsafe.ARRAY_DOUBLE_INDEX_SCALE=" + Unsafe.ARRAY_DOUBLE_INDEX_SCALE);
    System.out.println("Unsafe.ARRAY_OBJECT_BASE_OFFSET=" + Unsafe.ARRAY_OBJECT_BASE_OFFSET);
    System.out.println("Unsafe.ARRAY_OBJECT_INDEX_SCALE=" + Unsafe.ARRAY_OBJECT_INDEX_SCALE);
    System.out.println("------------------------------");
    // 基本數據數組類型操作
    int[] array = new int[] {11, 22, 33};
    /*
        改變最後一個元素的值,地址的演算法就是:基礎地址+偏移量,這個偏移量就是類型占用的大小*位置,Unsafe.ARRAY_INT_BASE_OFFSET + (array.length - 1) * Unsafe.ARRAY_INT_INDEX_SCALE
     */
    System.out.println("before put array[2]=" + array[2]);
    unsafe.putInt(array, (long)Unsafe.ARRAY_INT_BASE_OFFSET + (array.length - 1) * Unsafe.ARRAY_INT_INDEX_SCALE,
        100);
    // 獲取最後一個元素的值
    System.out.println("after put array[2]=" + array[2]);
    // 也可以這麼獲取,使用基礎地址+偏移量的方式
    System.out.println("after put array[2]=" + unsafe.getInt(array,
        (long)Unsafe.ARRAY_INT_BASE_OFFSET + (array.length - 1) * Unsafe.ARRAY_INT_INDEX_SCALE));
    System.out.println("-------------------");
    // Object類型數組操作
    Cat[] cats = {new Cat("cat1", 1), new Cat("cat2", 2), new Cat("cat3", 3)};
    System.out.println("before put cats[2]=" + cats[2]);
    unsafe.putObject(cats,
        (long)Unsafe.ARRAY_OBJECT_BASE_OFFSET + (cats.length - 1) * Unsafe.ARRAY_OBJECT_INDEX_SCALE,
        new Cat("newcat", 10000));
    // 獲取最後一個元素的值
    System.out.println("after put cats[2]=" + cats[2]);
    // 也可以這麼獲取,使用基礎地址+偏移量的方式
    System.out.println("after put cats[2]=" + unsafe.getObject(cats,
        (long)Unsafe.ARRAY_OBJECT_BASE_OFFSET + (cats.length - 1) * Unsafe.ARRAY_OBJECT_INDEX_SCALE));
    System.out.println("-------------------");
}

輸出:

Unsafe.ARRAY_BOOLEAN_BASE_OFFSET=16
Unsafe.ARRAY_BOOLEAN_INDEX_SCALE=1
Unsafe.ARRAY_BYTE_BASE_OFFSET=16
Unsafe.ARRAY_BYTE_INDEX_SCALE=1
Unsafe.ARRAY_SHORT_BASE_OFFSET=16
Unsafe.ARRAY_SHORT_INDEX_SCALE=2
Unsafe.ARRAY_CHAR_BASE_OFFSET=16
Unsafe.ARRAY_CHAR_INDEX_SCALE=2
Unsafe.ARRAY_INT_BASE_OFFSET=16
Unsafe.ARRAY_INT_INDEX_SCALE=4
Unsafe.ARRAY_LONG_BASE_OFFSET=16
Unsafe.ARRAY_LONG_INDEX_SCALE=8
Unsafe.ARRAY_FLOAT_BASE_OFFSET=16
Unsafe.ARRAY_FLOAT_INDEX_SCALE=4
Unsafe.ARRAY_DOUBLE_BASE_OFFSET=16
Unsafe.ARRAY_DOUBLE_INDEX_SCALE=8
Unsafe.ARRAY_OBJECT_BASE_OFFSET=16
Unsafe.ARRAY_OBJECT_INDEX_SCALE=4
------------------------------
before put array[2]=33
after put array[2]=100
after put array[2]=100
-------------------
static...
before put cats[2]=Cat{name='cat3', speed=3}
after put cats[2]=Cat{name='newcat', speed=10000}
after put cats[2]=Cat{name='newcat', speed=10000}
-------------------

Tips:

如果操作的元素位置沒有在數組範圍內的話,put和get操作不會異常,都會成功,因為這是記憶體操作,使用的是基礎地址+偏移量,但是並沒有改變原始數組的大小,put後可以獲取相應位置的記憶體數據,在沒有put前調用get則獲取的是數據類型的預設值。

CAS

比較並交換(Compare And Swap),在jvm里是一個原子操作,先獲取記憶體的值,然後判斷記憶體值和預期值是否相同,相同則更新為新值表示操作成功,不同則直接返回false,表明操作失敗。java里的JUC包下很多隊列或者鎖都採用了這種實現方式。

//每次都從主記憶體獲取var1對象var2偏移量的long值
public native long getLongVolatile(Object var1, long var2);
//將var4值放入指定var1對象的var2偏移量位置,直接刷新到主記憶體
public native void putLongVolatile(Object var1, long var2, long var4);
// 比較並替換原值為新值,操作成功返回true否則false,var1是指定對象,var2是偏移量地址,var4是預期的原值,var5是要更新的新值
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
// 自旋獲取原值並增加數值,var1是指定對象,var2是偏移量地址,var4是要增加的值
public final int getAndAddLong(Object var1, long var2, long var4) {
    int var5;
    do {
        var5 = this.getLongVolatile(var1, var2);
    } while(!this.compareAndSwapLong(var1, var2, var5, var5 + var4));

    return var5;
}
// 自旋獲取原值並設置新值,var1是指定對象,var2是偏移量地址,var4是要設置的新值
public final int getAndSetLong(Object var1, long var2, long var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapLong(var1, var2, var5, var4));

    return var5;
}
// 還有Object相關的cas操作這裡沒有列出

舉幾個慄子

public void test3() throws Exception {
    Cat cat = new Cat("Kitty", 1000);
    long speedOffset = unsafe.objectFieldOffset(Cat.class.getDeclaredField("speed"));
    System.out.println("before putLongVolatile,getLongVolatile=" + unsafe.getLongVolatile(cat, speedOffset));
    // 設置speed的值為2000
    unsafe.putLongVolatile(cat, speedOffset, 2000);
    System.out.println("after putLongVolatile,getLongVolatile=" + unsafe.getLongVolatile(cat, speedOffset));
    // 到這裡speed的值是2000,但是compareAndSwapLong里預期的值是3000,所以cas失敗,返回false
    System.out.println("compareAndSwapLong result:" + unsafe.compareAndSwapLong(cat, speedOffset, 3000, 4000));
    // 到這裡speed的值是2000,但是compareAndSwapLong里預期的值是2000,cas更新成功,返回true
    System.out.println("compareAndSwapLong result:" + unsafe.compareAndSwapLong(cat, speedOffset, 2000, 4000));
    // cas後speed的值就是4000了
    System.out.println("after compareAndSwapLong,getLongVolatile=" + unsafe.getLongVolatile(cat, speedOffset));
    // getAndAddLong會返回原值4000,新值=原值+10
    System.out.println("getAndAddLong:" + unsafe.getAndAddLong(cat, speedOffset, 10));
    // getAndAddLong後speed新值是4010
    System.out.println("after getAndAddLong,getLongVolatile=" + unsafe.getLongVolatile(cat, speedOffset));
    // getAndSetLong會返回原值4010,新值=要設置的新值1000
    System.out.println("getAndSetLong:" + unsafe.getAndSetLong(cat, speedOffset, 1000));
    // getAndSetLong後speed新值是1000
    System.out.println("after getAndSetLong,getLongVolatile=" + unsafe.getLongVolatile(cat, speedOffset));
}

輸出結果:

static...
before putLongVolatile,getLongVolatile=1000
after putLongVolatile,getLongVolatile=2000
compareAndSwapLong result:false
compareAndSwapLong result:true
after compareAndSwapLong,getLongVolatile=4000
getAndAddLong:4000
after getAndAddLong,getLongVolatile=4010
getAndSetLong:4010
after getAndSetLong,getLongVolatile=1000

線程調度及同步

// 釋放線程讓其繼續執行,多次調用只會生效一次,可以在park前調用
public native void unpark(Object thread);
// 阻塞線程,isAbsolute為true:表示絕對時間,time的單位是毫秒ms,false:表示相對時間,time的單位是納秒級的時間
public native void park(boolean isAbsolute, long time);
//以下3個方法均標註過期了,建議使用其他同步方法
// 獲取var1的對象鎖,沒獲取到則阻塞等待
public native void monitorEnter(Object var1);
// 嘗試獲取var1的對象鎖,不阻塞,獲取到則返回true,沒獲取到返回false
public native boolean tryMonitorEnter(Object var1);
// 釋放var1對象鎖
public native void monitorExit(Object var1);

我們先看看monitor同步相關的測試:

public void test4() {
    Object obj = new Object();
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + " isrunning...");
                unsafe.monitorEnter(obj);
                System.out.println(Thread.currentThread().getName() + " got monitorEnter...");
                Thread.sleep(3000);
                System.out.println(Thread.currentThread().getName() + " business over...");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                unsafe.monitorExit(obj);
                System.out.println(Thread.currentThread().getName() + " monitorExit...");
            }
        }
    });
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + " isrunning...");
                unsafe.monitorEnter(obj);
                System.out.println(Thread.currentThread().getName() + " got monitorEnter...");
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + " business over...");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                unsafe.monitorExit(obj);
                System.out.println(Thread.currentThread().getName() + " monitorExit...");
            }
        }
    });
    Thread t3 = new Thread(new Runnable() {
        @Override
        public void run() {
            boolean flag = false;
            try {
                System.out.println(Thread.currentThread().getName() + " isrunning...");
                flag = unsafe.tryMonitorEnter(obj);
                System.out.println(Thread.currentThread().getName() + " tryMonitorEnter:" + flag);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (flag) {
                    unsafe.monitorExit(obj);
                    System.out.println(Thread.currentThread().getName() + " monitorExit...");
                }
            }
            System.out.println(Thread.currentThread().getName() + " over...");
        }
    });
    t1.start();
    t2.start();
    t3.start();
}

可能的一種輸出如下(線程是根據系統分配資源調度的,輸出先後順序會有多種),下麵的這個輸出我們可以看到先輸出3個線程都啟動了,Thread-2嘗試獲取鎖失敗就結束了,然後Thread-0競爭到了對象鎖,等Thread-0線程運行完畢釋放了鎖,Thread-1才會獲取到鎖繼續執行直到結束釋放鎖。
Tips:

monitor相關的方法已經加了@deprecated註解,官方已經不再建議使用,可以換成其他鎖或者同步方式

Thread-0 isrunning...
Thread-2 isrunning...
Thread-2 tryMonitorEnter:false
Thread-2 over...
Thread-1 isrunning...
Thread-0 got monitorEnter...
Thread-0 business over...
Thread-0 monitorExit...
Thread-1 got monitorEnter...
Thread-1 business over...
Thread-1 monitorExit...

我們在來看看park、unpark相關的使用

public void test5() throws Exception {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    System.out
        .println(LocalDateTime.now().format(formatter) + " " + Thread.currentThread().getName() + " is running...");
    // 這裡讓當前線程阻塞6s,註意:如果park第一個參數是true的話,表示絕對時間,這個時間是毫秒級的,也就是系統時間,系統到這個絕對時間後才喚醒執行
    unsafe.park(true, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(6));
    System.out
        .println(LocalDateTime.now().format(formatter) + " " + Thread.currentThread().getName() + " continue...");
    // 這裡讓當前線程阻塞3s,註意:如果park第一個參數是false的話,這個是納秒級別的時間,表示相對當前時間3s後繼續喚醒執行
    unsafe.park(false, TimeUnit.SECONDS.toNanos(3));
    System.out
        .println(LocalDateTime.now().format(formatter) + " " + Thread.currentThread().getName() + " continue...");
    // 如果park的第一個參數是false,第二個值是0,則會一直等待,直到其他線程調用了unpark這個線程才會結束阻塞
    // 一般像這種無限期等待的調了多少次park(false, 0)就要對於調同樣次數的unpark才會完全解除阻塞
    unsafe.park(false, 0);
}

輸出:

2020-05-11 08:01:04 main is running...
2020-05-11 08:01:10 main continue...
2020-05-11 08:01:13 main continue...

再看一個案例:

public void test6() throws Exception {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(LocalDateTime.now().format(formatter) + " " + Thread.currentThread().getName()
                    + " is running...");
                // 這裡讓當前線程阻塞600s也就是10分鐘,註意:如果park第一個參數是true的話,表示絕對時間,這個時間是毫秒級的,也就是系統時間,系統到這個絕對時間後才喚醒執行
                unsafe.park(true, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(600));
                System.out.println(LocalDateTime.now().format(formatter) + " " + Thread.currentThread().getName()
                    + " continue...");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(LocalDateTime.now().format(formatter) + " " + Thread.currentThread().getName()
                    + " is running...");
                // 這裡讓當前線程阻塞600s也就是10分鐘,註意:如果park第一個參數是true的話,表示絕對時間,這個時間是毫秒級的,也就是系統時間,系統到這個絕對時間後才喚醒執行
                unsafe.park(true, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(600));
                // unsafe.park(true, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(600));
                System.out.println(LocalDateTime.now().format(formatter) + " " + Thread.currentThread().getName()
                    + " continue...");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
    t1.start();
    t2.start();
    // 主線程休眠2秒
    Thread.sleep(2000);
    System.out
        .println(LocalDateTime.now().format(formatter) + " " + Thread.currentThread().getName() + " is running...");
    // 這裡調了unpark方法,參數就是t1線程,unsafe.unpark喚醒了t1線程,使得t1線程不用等到10分鐘立馬就可以執行
    unsafe.unpark(t1);
    // 下麵連續調用了兩次unpark t2線程,但是結果只釋放了一次令牌,如果把t2線程的unsafe.park註釋去掉,那麼t2線程會一直等到park的時間到後被喚醒執行,
    unsafe.unpark(t2);
    unsafe.unpark(t2);
    System.out.println(LocalDateTime.now().format(formatter) + " " + Thread.currentThread().getName() + " over...");
}

輸出如下:

2020-05-11 08:05:26 Thread-1 is running...
2020-05-11 08:05:26 Thread-0 is running...
2020-05-11 08:05:28 main is running...
2020-05-11 08:05:28 main over...
2020-05-11 08:05:28 Thread-1 continue...
2020-05-11 08:05:28 Thread-0 continue...

unsafe的park和unpark在JUC併發包下使用的特別多,後續再介紹吧

記憶體屏障

這個我沒有深入的瞭解,網上找了些資料看了看,沒有具體的實踐過,大家可以瞭解下。

loadFence:保證在這個屏障之前的所有讀操作都已經完成。
storeFence:保證在這個屏障之前的所有寫操作都已經完成。
fullFence:保證在這個屏障之前的所有讀寫操作都已經完成。

其他

類載入,類實例化相關的一些方法,還有其他的方法,這裡不再一一說明,雖然不常用,但是瞭解其運行原理,或者去研究一下jvm的源碼對自己都是一種提升。

再見

好了,就胡扯到這裡吧,文章里的都是個人理解和實踐,難免會有理解錯誤和實踐錯誤的,請各位看官多多指正。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在軟體版本快速迭代的過程中,經常會遇到一些介面變化問題。而如果需要相容舊版本的話,就需要使用到版本判斷的方法。判斷清楚版本號屬於哪一個區間,再對不同的版本號區間採取不同的演算法或者執行策略。Python中預先內置的LooseVersion就是一個很好的版本號比對工具,不僅僅可以對相同位數或者相同類型的... ...
  • 原文在[這裡](https://go.dev/blog/go1.21)。 > 由Eli Bendersky, on behalf of the Go team 發佈於 8 August 2023 Go團隊今天非常高興地發佈了Go 1.21版本,你可以通過訪問[下載頁面](https://go.dev ...
  • ## JSR-303 規範 在程式進行數據處理之前,對數據進行準確性校驗是我們必須要考慮的事情。儘早發現數據錯誤,不僅可以防止錯誤向核心業務邏輯蔓延,而且這種錯誤非常明顯,容易發現解決。 JSR303 規範(Bean Validation 規範)為 JavaBean 驗證定義了相應的元數據模型和 A ...
  • Go最新面經分享:演算法、併發模型、緩存落盤、etcd、actor模型、epoll等等... 本文先分享2段面經,文末總結了關鍵問題的復盤筆記。一定要看到最後! ...
  • ## 1 BUG現象 系統併發請求,系統停滯無法使用,所有介面都是無法與後端進行交互的狀態,系統並沒有宕機 ## 2 BUG的業務流程 1. 插入分數方法 涉及插入表ABCD 加了聲明式事務 2. 查詢分數方法 涉及表ABCD ``` controller() { @Transactional in ...
  • 映射列值是指將一個列中的某些特定值映射為另外一些值,常用於數據清洗和轉換。 使用映射列值的場景有很多,以下是幾種常見的場景: 1. 將字元串類型的列中的某些值映射為數字。例如,將“男”和“女”分別映射為 0 和 1,以便進行機器學習演算法的訓練和預測。 2. 將縮寫替換為全稱。例如,將“USA”和“U ...
  • PeFile模塊是`Python`中一個強大的攜帶型第三方`PE`格式分析工具,用於解析和處理`Windows`可執行文件。該模塊提供了一系列的API介面,使得用戶可以通過`Python`腳本來讀取和分析PE文件的結構,包括文件頭、節表、導入表、導出表、資源表、重定位表等等。此外,PEfile模塊還... ...
  • 源碼請到:自然語言處理練習: 學習自然語言處理時候寫的一些代碼 (gitee.com) 數據來源: 搜狗新聞語料庫 由於鏈接失效,現在使用百度網盤分享 鏈接:https://pan.baidu.com/s/1RTx2k7V3Ujgg9-Rv8I8IRA?pwd=ujn3 提取碼:ujn3 停用詞 來 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...