死磕 java魔法類之Unsafe解析

来源:https://www.cnblogs.com/tong-yuan/archive/2019/05/06/Unsafe.html
-Advertisement-
Play Games

Unsafe是什麼? Unsafe只有CAS的功能嗎? Unsafe為什麼是不安全的? 怎麼使用Unsafe? ...


問題

(1)Unsafe是什麼?

(2)Unsafe只有CAS的功能嗎?

(3)Unsafe為什麼是不安全的?

(4)怎麼使用Unsafe?

簡介

本章是java併發包專題的第一章,但是第一篇寫的卻不是java併發包中類,而是java中的魔法類sun.misc.Unsafe。

Unsafe為我們提供了訪問底層的機制,這種機制僅供java核心類庫使用,而不應該被普通用戶使用。

但是,為了更好地瞭解java的生態體系,我們應該去學習它,去瞭解它,不求深入到底層的C/C++代碼,但求能瞭解它的基本功能。

獲取Unsafe的實例

查看Unsafe的源碼我們會發現它提供了一個getUnsafe()的靜態方法。

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

但是,如果直接調用這個方法會拋出一個SecurityException異常,這是因為Unsafe僅供java內部類使用,外部類不應該使用它。

那麼,我們就沒有方法了嗎?

當然不是,我們有反射啊!查看源碼,我們發現它有一個屬性叫theUnsafe,我們直接通過反射拿到它即可。

public class UnsafeTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Field f = Unsafe.class.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
    }
}

使用Unsafe實例化一個類

假如我們有一個簡單的類如下:

class User {
    int age;

    public User() {
        this.age = 10;
    }
}

如果我們通過構造方法實例化這個類,age屬性將會返回10。

User user1 = new User();
// 列印10
System.out.println(user1.age);

如果我們調用Unsafe來實例化呢?

User user2 = (User) unsafe.allocateInstance(User.class);
// 列印0
System.out.println(user2.age);

age將返回0,因為Unsafe.allocateInstance()只會給對象分配記憶體,並不會調用構造方法,所以這裡只會返回int類型的預設值0。

修改私有欄位的值

使用Unsafe的putXXX()方法,我們可以修改任意私有欄位的值。

public class UnsafeTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
        Field f = Unsafe.class.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);

        User user = new User();
        Field age = user.getClass().getDeclaredField("age");
        unsafe.putInt(user, unsafe.objectFieldOffset(age), 20);

        // 列印20
        System.out.println(user.getAge());
    }
}

class User {
    private int age;

    public User() {
        this.age = 10;
    }

    public int getAge() {
        return age;
    }
}

一旦我們通過反射調用得到欄位age,我們就可以使用Unsafe將其值更改為任何其他int值。(當然,這裡也可以通過反射直接修改)

拋出checked異常

我們知道如果代碼拋出了checked異常,要不就使用try...catch捕獲它,要不就在方法簽名上定義這個異常,但是,通過Unsafe我們可以拋出一個checked異常,同時卻不用捕獲或在方法簽名上定義它。

// 使用正常方式拋出IOException需要定義在方法簽名上往外拋
public static void readFile() throws IOException {
    throw new IOException();
}
// 使用Unsafe拋出異常不需要定義在方法簽名上往外拋
public static void readFileUnsafe() {
    unsafe.throwException(new IOException());
}

使用堆外記憶體

如果進程在運行過程中JVM上的記憶體不足了,會導致頻繁的進行GC。理想情況下,我們可以考慮使用堆外記憶體,這是一塊不受JVM管理的記憶體。

使用Unsafe的allocateMemory()我們可以直接在堆外分配記憶體,這可能非常有用,但我們要記住,這個記憶體不受JVM管理,因此我們要調用freeMemory()方法手動釋放它。

假設我們要在堆外創建一個巨大的int數組,我們可以使用allocateMemory()方法來實現:

class OffHeapArray {
    // 一個int等於4個位元組
    private static final int INT = 4;
    private long size;
    private long address;

    private static Unsafe unsafe;
    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    // 構造方法,分配記憶體
    public OffHeapArray(long size) {
        this.size = size;
        // 參數位元組數
        address = unsafe.allocateMemory(size * INT);
    }
    
    // 獲取指定索引處的元素
    public int get(long i) {
        return unsafe.getInt(address + i * INT);
    }
    // 設置指定索引處的元素
    public void set(long i, int value) {
        unsafe.putInt(address + i * INT, value);
    }
    // 元素個數
    public long size() {
        return size;
    }
    // 釋放堆外記憶體
    public void freeMemory() {
        unsafe.freeMemory(address);
    }
}

在構造方法中調用allocateMemory()分配記憶體,在使用完成後調用freeMemory()釋放記憶體。

使用方式如下:

OffHeapArray offHeapArray = new OffHeapArray(4);
offHeapArray.set(0, 1);
offHeapArray.set(1, 2);
offHeapArray.set(2, 3);
offHeapArray.set(3, 4);
offHeapArray.set(2, 5); // 在索引2的位置重覆放入元素

int sum = 0;
for (int i = 0; i < offHeapArray.size(); i++) {
    sum += offHeapArray.get(i);
}
// 列印12
System.out.println(sum);

offHeapArray.freeMemory();

最後,一定要記得調用freeMemory()將記憶體釋放回操作系統。

CompareAndSwap操作

JUC下麵大量使用了CAS操作,它們的底層是調用的Unsafe的CompareAndSwapXXX()方法。這種方式廣泛運用於無鎖演算法,與java中標準的悲觀鎖機制相比,它可以利用CAS處理器指令提供極大的加速。

比如,我們可以基於Unsafe的compareAndSwapInt()方法構建線程安全的計數器。

class Counter {
    private volatile int count = 0;

    private static long offset;
    private static Unsafe unsafe;
    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
            offset = unsafe.objectFieldOffset(Counter.class.getDeclaredField("count"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public void increment() {
        int before = count;
        // 失敗了就重試直到成功為止
        while (!unsafe.compareAndSwapInt(this, offset, before, before + 1)) {
            before = count;
        }
    }

    public int getCount() {
        return count;
    }
}

我們定義了一個volatile的欄位count,以便對它的修改所有線程都可見,併在類載入的時候獲取count在類中的偏移地址。

在increment()方法中,我們通過調用Unsafe的compareAndSwapInt()方法來嘗試更新之前獲取到的count的值,如果它沒有被其它線程更新過,則更新成功,否則不斷重試直到成功為止。

我們可以通過使用多個線程來測試我們的代碼:

Counter counter = new Counter();
ExecutorService threadPool = Executors.newFixedThreadPool(100);

// 起100個線程,每個線程自增10000次
IntStream.range(0, 100)
    .forEach(i->threadPool.submit(()->IntStream.range(0, 10000)
        .forEach(j->counter.increment())));

threadPool.shutdown();

Thread.sleep(2000);

// 列印1000000
System.out.println(counter.getCount());

park/unpark

JVM在上下文切換的時候使用了Unsafe中的兩個非常牛逼的方法park()和unpark()。

當一個線程正在等待某個操作時,JVM調用Unsafe的park()方法來阻塞此線程。

當阻塞中的線程需要再次運行時,JVM調用Unsafe的unpark()方法來喚醒此線程。

我們之前在分析java中的集合時看到了大量的LockSupport.park()/unpark(),它們底層都是調用的Unsafe的這兩個方法。

總結

使用Unsafe幾乎可以操作一切:

(1)實例化一個類;

(2)修改私有欄位的值;

(3)拋出checked異常;

(4)使用堆外記憶體;

(5)CAS操作;

(6)阻塞/喚醒線程;

彩蛋

論實例化一個類的方式?

(1)通過構造方法實例化一個類;

(2)通過Class實例化一個類;

(3)通過反射實例化一個類;

(4)通過克隆實例化一個類;

(5)通過反序列化實例化一個類;

(6)通過Unsafe實例化一個類;

public class InstantialTest {

    private static Unsafe unsafe;
    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) throws Exception {
        // 1. 構造方法
        User user1 = new User();
        // 2. Class,裡面實際也是反射
        User user2 = User.class.newInstance();
        // 3. 反射
        User user3 = User.class.getConstructor().newInstance();
        // 4. 克隆
        User user4 = (User) user1.clone();
        // 5. 反序列化
        User user5 = unserialize(user1);
        // 6. Unsafe
        User user6 = (User) unsafe.allocateInstance(User.class);

        System.out.println(user1.age);
        System.out.println(user2.age);
        System.out.println(user3.age);
        System.out.println(user4.age);
        System.out.println(user5.age);
        System.out.println(user6.age);
    }

    private static User unserialize(User user1) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://object.txt"));
        oos.writeObject(user1);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D://object.txt"));
        // 反序列化
        User user5 = (User) ois.readObject();
        ois.close();
        return user5;
    }

    static class User implements Cloneable, Serializable {
        private int age;

        public User() {
            this.age = 10;
        }

        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
}

歡迎關註我的公眾號“彤哥讀源碼”,查看更多源碼系列文章, 與彤哥一起暢游源碼的海洋。

qrcode


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

-Advertisement-
Play Games
更多相關文章
  • [toc] 一.題目要求 我們在剛開始上課的時候介紹過一個小學四則運算自動生成程式的例子,請實現它,要求: 能夠自動生成四則運算練習題 可以定製題目數量 用戶可以選擇運算符 用戶設置最大數(如十以內、百以內等) 用戶選擇是否有括弧、是否有小數 用戶選擇輸出方式(如輸出到文件、印表機等) 最好能提供圖 ...
  • 剛開始學習php的時候是在wamp環境下開發的,後來才接觸到 lnmp 環境當時安裝lnmp是按照一大長篇文檔一步步的編譯安裝,當時是真不知道是在做什麼啊!腦袋一片空白~~,只知道按照那麼長的一篇文檔一步步的來做就能實現lnmp的搭建。最近工作閑暇之餘又想起來了這個悲慘的事情,然後我就想能不能不看文 ...
  • 書接上文。上文主要講了下線程的基本概念,三種創建線程的方式與區別,還介紹了線程的狀態,線程通知和等待,join等,本篇繼續介紹併發編程的基礎知識。 sleep 當一個執行的線程調用了Thread的sleep方法,調用線程會暫時讓出指定時間的執行權,在這期間不參與CPU的調度,不占用CPU,但是不會釋 ...
  • mq系列文章 對mq瞭解不是很多的,可以看一下下麵兩篇文章: 1. "聊聊mq的使用場景" 2. "聊聊業務系統中投遞消息到mq的幾種方式" 3. 聊聊消息消費的幾種方式 4. 如何確保消息至少消費一次 5. 如何保證消息消費的冪等性 本章內容 從消費者的角度出發,分析一下消息消費的兩種方式: 1. ...
  • day21 03 異常處理 1.什麼是異常 異常:程式運行時發生錯誤的信號 錯誤:語法錯誤(一般是不能處理的異常) 邏輯錯誤(可處理的異常) 特點:程式一旦發生錯誤,就從錯誤的位置停下來,不再繼續執行後面的內容 2.怎麼處理異常呢? 比如下麵類型代碼的異常: 如果執行後用戶輸入的不是數據就會報錯: ...
  • 簡介 Hystrix Dashboard是一款針對Hystrix進行實時監控的工具,通過Hystrix Dashboard可以直觀地看到各Hystrix Command的請求響應時間,請求成功率等數據。 快速上手 工程說明 | 工程名 | 埠 | 作用 | | : | : | : : | | eu ...
  • 變數的使用: def test(request): num=1 s='hello' li=[1,2,['a','b']] dic={'name':'w','age':1} se={1,2,3} tup=(1,2,3,4) def my_test(): return '這是my_test' class ...
  • 所屬網站分類: python高級 > 面向對象 作者:阿裡媽媽 鏈接:http://www.pythonheidong.com/blog/article/74/ 來源:python黑洞網 有什麼區別? class Child(SomeBaseClass): def __init__(self): s ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...