阿里不允許使用 Executors 建立執行緒池!那怎麼使用,怎麼監控?

小傅哥發表於2020-12-17


作者:小傅哥
部落格:https://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收穫!?

一、前言

五常大米好吃!

哈哈哈,是不你總買五常大米,其實五常和榆樹是挨著的,榆樹大米也好吃,榆樹還是天下第一糧倉呢!但是五常出名,所以只認識五常。

為什麼提這個呢,因為阿里不允許使用 Executors 建立執行緒池!其他很多大廠也不允許,這麼建立的話,控制不好會出現OOM。

,本篇就帶你學習四種執行緒池的不同使用方式、業務場景應用以及如何監控執行緒。

二、面試題

謝飛機,小記!,上次從面試官那逃跑後,惡補了多執行緒,自己好像也內捲了,所以出門逛逛!

面試官:嗨,飛機,飛機,這邊!

謝飛機:嗯?!哎呀,面試官你咋來南海子公園了?

面試官:我家就附近,跑步來了。最近你咋樣,上次問你的多執行緒學了嗎?

謝飛機:哎,看了是看了,記不住鴨!

面試官:嗯,不常用確實記不住。不過你可以選擇跳槽,來大廠,大廠的業務體量較大!

謝飛機:我就糾結呢,想回家考教師資格證了,我們村小學要教java了!

面試官:哈哈哈哈哈,一起!

三、四種執行緒池使用介紹

Executors 是建立執行緒池的工具類,比較典型常見的四種執行緒池包括:newFixedThreadPoolnewSingleThreadExecutornewCachedThreadPoolnewScheduledThreadPool。每一種都有自己特定的典型例子,可以按照每種的特性用在不同的業務場景,也可以做為參照精細化建立執行緒池。

1. newFixedThreadPool

public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(3);
    for (int i = 1; i < 5; i++) {
        int groupId = i;
        executorService.execute(() -> {
            for (int j = 1; j < 5; j++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ignored) {
                }
                logger.info("第 {} 組任務,第 {} 次執行完成", groupId, j);
            }
        });
    }
    executorService.shutdown();
}

// 測試結果
23:48:24.628 [pool-2-thread-1] INFO  o.i.i.test.Test_newFixedThreadPool - 第 1 組任務,第 1 次執行完成
23:48:24.628 [pool-2-thread-2] INFO  o.i.i.test.Test_newFixedThreadPool - 第 2 組任務,第 1 次執行完成
23:48:24.628 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 3 組任務,第 1 次執行完成
23:48:25.633 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 3 組任務,第 2 次執行完成
23:48:25.633 [pool-2-thread-1] INFO  o.i.i.test.Test_newFixedThreadPool - 第 1 組任務,第 2 次執行完成
23:48:25.633 [pool-2-thread-2] INFO  o.i.i.test.Test_newFixedThreadPool - 第 2 組任務,第 2 次執行完成
23:48:26.633 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 3 組任務,第 3 次執行完成
23:48:26.633 [pool-2-thread-2] INFO  o.i.i.test.Test_newFixedThreadPool - 第 2 組任務,第 3 次執行完成
23:48:26.633 [pool-2-thread-1] INFO  o.i.i.test.Test_newFixedThreadPool - 第 1 組任務,第 3 次執行完成
23:48:27.634 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 3 組任務,第 4 次執行完成
23:48:27.634 [pool-2-thread-2] INFO  o.i.i.test.Test_newFixedThreadPool - 第 2 組任務,第 4 次執行完成
23:48:27.634 [pool-2-thread-1] INFO  o.i.i.test.Test_newFixedThreadPool - 第 1 組任務,第 4 次執行完成
23:48:28.635 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 4 組任務,第 1 次執行完成
23:48:29.635 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 4 組任務,第 2 次執行完成
23:48:30.635 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 4 組任務,第 3 次執行完成
23:48:31.636 [pool-2-thread-3] INFO  o.i.i.test.Test_newFixedThreadPool - 第 4 組任務,第 4 次執行完成

Process finished with exit code 0

圖解

圖 22-1 newFixedThreadPool 執行過程

  • 程式碼new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
  • 介紹:建立一個固定大小可重複使用的執行緒池,以 LinkedBlockingQueue 無界阻塞佇列存放等待執行緒。
  • 風險:隨著執行緒任務不能被執行的的無限堆積,可能會導致OOM。

2. newSingleThreadExecutor

public static void main(String[] args) {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    for (int i = 1; i < 5; i++) {
        int groupId = i;
        executorService.execute(() -> {
            for (int j = 1; j < 5; j++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ignored) {
                }
                logger.info("第 {} 組任務,第 {} 次執行完成", groupId, j);
            }
        });
    }
    executorService.shutdown();
}

// 測試結果
23:20:15.066 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 1 組任務,第 1 次執行完成
23:20:16.069 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 1 組任務,第 2 次執行完成
23:20:17.070 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 1 組任務,第 3 次執行完成
23:20:18.070 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 1 組任務,第 4 次執行完成
23:20:19.071 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 2 組任務,第 1 次執行完成
23:23:280.071 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 2 組任務,第 2 次執行完成
23:23:281.072 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 2 組任務,第 3 次執行完成
23:23:282.072 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 2 組任務,第 4 次執行完成
23:23:283.073 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 3 組任務,第 1 次執行完成
23:23:284.074 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 3 組任務,第 2 次執行完成
23:23:285.074 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 3 組任務,第 3 次執行完成
23:23:286.075 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 3 組任務,第 4 次執行完成
23:23:287.075 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 4 組任務,第 1 次執行完成
23:23:288.075 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 4 組任務,第 2 次執行完成
23:23:289.076 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 4 組任務,第 3 次執行完成
23:20:30.076 [pool-2-thread-1] INFO  o.i.i.t.Test_newSingleThreadExecutor - 第 4 組任務,第 4 次執行完成

圖解

圖 22-2 newSingleThreadExecutor 執行過程

  • 程式碼new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
  • 介紹:只建立一個執行執行緒任務的執行緒池,如果出現意外終止則再建立一個。
  • 風險:同樣這也是一個無界佇列存放待執行執行緒,無限堆積下會出現OOM。

3. newCachedThreadPool

public static void main(String[] args) throws InterruptedException {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 1; i < 5; i++) {
        int groupId = i;
        executorService.execute(() -> {
            for (int j = 1; j < 5; j++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ignored) {
                }
                logger.info("第 {} 組任務,第 {} 次執行完成", groupId, j);
            }
        });
    }
    executorService.shutdown();
    
    // 測試結果
    23:25:59.818 [pool-2-thread-2] INFO  o.i.i.test.Test_newCachedThreadPool - 第 2 組任務,第 1 次執行完成
    23:25:59.818 [pool-2-thread-3] INFO  o.i.i.test.Test_newCachedThreadPool - 第 3 組任務,第 1 次執行完成
    23:25:59.818 [pool-2-thread-1] INFO  o.i.i.test.Test_newCachedThreadPool - 第 1 組任務,第 1 次執行完成
    23:25:59.818 [pool-2-thread-4] INFO  o.i.i.test.Test_newCachedThreadPool - 第 4 組任務,第 1 次執行完成
    23:25:00.823 [pool-2-thread-4] INFO  o.i.i.test.Test_newCachedThreadPool - 第 4 組任務,第 2 次執行完成
    23:25:00.823 [pool-2-thread-1] INFO  o.i.i.test.Test_newCachedThreadPool - 第 1 組任務,第 2 次執行完成
    23:25:00.823 [pool-2-thread-2] INFO  o.i.i.test.Test_newCachedThreadPool - 第 2 組任務,第 2 次執行完成
    23:25:00.823 [pool-2-thread-3] INFO  o.i.i.test.Test_newCachedThreadPool - 第 3 組任務,第 2 次執行完成
    23:25:01.823 [pool-2-thread-4] INFO  o.i.i.test.Test_newCachedThreadPool - 第 4 組任務,第 3 次執行完成
    23:25:01.823 [pool-2-thread-1] INFO  o.i.i.test.Test_newCachedThreadPool - 第 1 組任務,第 3 次執行完成
    23:25:01.824 [pool-2-thread-2] INFO  o.i.i.test.Test_newCachedThreadPool - 第 2 組任務,第 3 次執行完成
    23:25:01.824 [pool-2-thread-3] INFO  o.i.i.test.Test_newCachedThreadPool - 第 3 組任務,第 3 次執行完成
    23:25:02.824 [pool-2-thread-1] INFO  o.i.i.test.Test_newCachedThreadPool - 第 1 組任務,第 4 次執行完成
    23:25:02.824 [pool-2-thread-4] INFO  o.i.i.test.Test_newCachedThreadPool - 第 4 組任務,第 4 次執行完成
    23:25:02.825 [pool-2-thread-3] INFO  o.i.i.test.Test_newCachedThreadPool - 第 3 組任務,第 4 次執行完成
    23:25:02.825 [pool-2-thread-2] INFO  o.i.i.test.Test_newCachedThreadPool - 第 2 組任務,第 4 次執行完成
}

圖解

圖 22-3 newCachedThreadPool 執行過程

  • 程式碼new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>())
  • 介紹:首先 SynchronousQueue 是一個生產消費模式的阻塞任務佇列,只要有任務就需要有執行緒執行,執行緒池中的執行緒可以重複使用。
  • 風險:如果執行緒任務比較耗時,又大量建立,會導致OOM

4. newScheduledThreadPool

public static void main(String[] args) {
    ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
    executorService.schedule(() -> {
        logger.info("3秒後開始執行");
    }, 3, TimeUnit.SECONDS);
    executorService.scheduleAtFixedRate(() -> {
        logger.info("3秒後開始執行,以後每2秒執行一次");
    }, 3, 2, TimeUnit.SECONDS);
    executorService.scheduleWithFixedDelay(() -> {
        logger.info("3秒後開始執行,後續延遲2秒");
    }, 3, 2, TimeUnit.SECONDS);
}

// 測試結果
23:28:32.442 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒後開始執行
23:28:32.444 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒後開始執行,以後每2秒執行一次
23:28:32.444 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒後開始執行,後續延遲2秒
23:28:34.441 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒後開始執行,以後每2秒執行一次
23:28:34.445 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒後開始執行,後續延遲2秒
23:28:36.440 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒後開始執行,以後每2秒執行一次
23:28:36.445 [pool-2-thread-1] INFO  o.i.i.t.Test_newScheduledThreadPool - 3秒後開始執行,後續延遲2秒

圖解

圖 22-4 newScheduledThreadPool 執行過程

  • 程式碼public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue()); }
  • 介紹:這就是一個比較有意思的執行緒池了,它可以延遲定時執行,有點像我們的定時任務。同樣它也是一個無限大小的執行緒池 Integer.MAX_VALUE。它提供的呼叫方法比較多,包括:scheduleAtFixedRatescheduleWithFixedDelay,可以按需選擇延遲執行方式。
  • 風險:同樣由於這是一組無限容量的執行緒池,所以依舊又OOM風險。

四、執行緒池使用場景說明

什麼時候使用執行緒池?

說簡單是當為了給老闆省錢的時候,因為使用執行緒池可以降低伺服器資源的投入,讓每臺機器儘可能更大限度的使用CPU。

?那你這麼說肯定沒辦法升職加薪了!

所以如果說的高大上一點,那麼是在符合科特爾法則阿姆達爾定律的情況下,引入執行緒池的使用最為合理。啥意思呢,還得簡單說!

假如:我們有一套電商服務,使用者瀏覽商品的併發訪問速率是:1000客戶/每分鐘,平均每個客戶在伺服器上的耗時0.5分鐘。根據利特爾法則,在任何時刻,服務端都承擔著1000*0.5=500個客戶的業務處理量。過段時間大促了,併發訪問的使用者擴了一倍2000客戶了,那怎麼保障服務效能呢?

  1. 提高伺服器併發處理的業務量,即提高到2000×0.5=1000
  2. 減少伺服器平均處理客戶請求的時間,即減少到:2000×0.25=500

所以:在有些場景下會把序列的請求介面,壓縮成並行執行,如圖 22-5

圖22-5 多執行緒介面查詢使用

但是,執行緒池的使用會隨著業務場景變化而不同,如果你的業務需要大量的使用執行緒池,並非常依賴執行緒池,那麼就不可能用 Executors 工具類中提供的方法。因為這些執行緒池的建立都不夠精細化,也非常容易造成OOM風險,而且隨著業務場景邏輯不同,會有IO密集型和CPU密集型。

最終,大家使用的執行緒池都是使用 new ThreadPoolExecutor() 建立的,當然也有基於Spring的執行緒池配置 org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor

可你想過嗎,同樣一個介面在有活動時候怎麼辦、有大促時候怎麼辦,可能你當時設定的執行緒池是合理的,但是一到流量非常大的時候就很不適合了,所以如果能動態調整執行緒池就非常有必要了。而且使用 new ThreadPoolExecutor() 方式建立的執行緒池是可以通過提供的 set 方法進行動態調整的。有了這個動態調整的方法後,就可以把執行緒池包裝起來,在配合動態調整的頁面,動態更新執行緒池引數,就可以非常方便的調整執行緒池了。

五、獲取執行緒池監控資訊

你收過報警簡訊嗎?

收過,半夜還有報警機器人打電話呢!崴,你的系統有個機器睡著了,快起來看看!!!

所以,如果你高頻、高依賴執行緒池,那麼有一個完整的監控系統,就非重要了。總不能線上掛了,你還不知道!

可監控內容

方法 含義
getActiveCount() 執行緒池中正在執行任務的執行緒數量
getCompletedTaskCount() 執行緒池已完成的任務數量,該值小於等於taskCount
getCorePoolSize() 執行緒池的核心執行緒數量
getLargestPoolSize() 執行緒池曾經建立過的最大執行緒數量。通過這個資料可以知道執行緒池是否滿過,也就是達到了maximumPoolSize
getMaximumPoolSize() 執行緒池的最大執行緒數量
getPoolSize() 執行緒池當前的執行緒數量
getTaskCount() 執行緒池已經執行的和未執行的任務總數

1. 重寫執行緒池方式監控

如果我們想監控一個執行緒池的方法執行動作,最簡單的方式就是繼承這個類,重寫方法,在方法中新增動作收集資訊。

虛擬碼

public class ThreadPoolMonitor extends ThreadPoolExecutor {

    @Override
    public void shutdown() {
        // 統計已執行任務、正在執行任務、未執行任務數量
        super.shutdown();
    }

    @Override
    public List<Runnable> shutdownNow() {
        // 統計已執行任務、正在執行任務、未執行任務數量
        return super.shutdownNow();
    }

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        // 記錄開始時間
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        // 記錄完成耗時
    }
    
    ...
}

2. 基於IVMTI方式監控

這塊是監控的重點,因為我們不太可能讓每一個需要監控的執行緒池都來重寫的方式記錄,這樣的改造成本太高了。

那麼除了這個笨方法外,可以選擇使用基於JVMTI的方式,進行開發監控元件。

JVMTI:JVMTI(JVM Tool Interface)位於jpda最底層,是Java虛擬機器所提供的native程式設計介面。JVMTI可以提供效能分析、debug、記憶體管理、執行緒分析等功能。

基於jvmti提供的介面服務,運用C++程式碼(win32-add_library)在Agent_OnLoad裡開發監控服務,並生成dll檔案。開發完成後在java程式碼中加入agentpath,這樣就可以監控到我們需要的資訊內容。

環境準備

  1. Dev-C++
  2. JetBrains CLion 2018.2.3
  3. IntelliJ IDEA Community Edition 2018.3.1 x64
  4. jdk1.8.0_45 64位
  5. jvmti(在jdk安裝目錄下jdk1.8.0_45\include裡,把include整個資料夾複製到和工程案例同層級目錄下,便於 include 引用)

配置資訊:(路徑相關修改為自己的)

  1. C++開發工具Clion配置
    1.配置位置;Settings->Build,Execution,Deployment->Toolchains
    1. MinGM配置:D:\Program Files (x86)\Dev-Cpp\MinGW64
  2. java除錯時配置
    1. 配置位置:Run/Debug Configurations ->VM options
    2. 配置內容:-agentpath:E:\itstack\git\github.com\itstack-jvmti\cmake-build-debug\libitstack_jvmti.dll

2.1 先做一個監控例子

Java工程

public class TestLocationException {

    public static void main(String[] args) {
        Logger logger = Logger.getLogger("TestLocationException");
        try {
            PartnerEggResourceImpl resource = new PartnerEggResourceImpl();
            Object obj = resource.queryUserInfoById(null);
            logger.info("測試結果:" + obj);
        } catch (Exception e) {
            //遮蔽異常
 }
    }
}

class PartnerEggResourceImpl {
    Logger logger = Logger.getLogger("PartnerEggResourceImpl");
    public Object queryUserInfoById(String userId) {
        logger.info("根據使用者Id獲取使用者資訊" + userId);
        if (null == userId) {
            throw new NullPointerException("根據使用者Id獲取使用者資訊,空指標異常");
        }
        return userId;
    }
}

c++監控

#include <iostream>
#include <cstring>
#include "jvmti.h"

using namespace std;

//異常回撥函式
static void JNICALL
callbackException(jvmtiEnv *jvmti_env, JNIEnv *env, jthread thr, jmethodID methodId, jlocation location,
jobject exception, jmethodID catch_method, jlocation catch_location) {
// 獲得方法對應的類
jclass clazz;
jvmti_env->GetMethodDeclaringClass(methodId, &clazz);

// 獲得類的簽名
char *class_signature;
jvmti_env->GetClassSignature(clazz, &class_signature, nullptr);

//過濾非本工程類資訊
string::size_type idx;
string class_signature_str = class_signature;
idx = class_signature_str.find("org/itstack");
if (idx != 1) {
return;
}

//異常類名稱
char *exception_class_name;
jclass exception_class = env->GetObjectClass(exception);
jvmti_env->GetClassSignature(exception_class, &exception_class_name, nullptr);

// 獲得方法名稱
char *method_name_ptr, *method_signature_ptr;
jvmti_env->GetMethodName(methodId, &method_name_ptr, &method_signature_ptr, nullptr);

//獲取目標方法的起止地址和結束地址
jlocation start_location_ptr;    //方法的起始位置
jlocation end_location_ptr;      //用於方法的結束位置
jvmti_env->GetMethodLocation(methodId, &start_location_ptr, &end_location_ptr);

//輸出測試結果
cout << "測試結果 - 定位類的簽名:" << class_signature << endl;
cout << "測試結果 - 定位方法資訊:" << method_name_ptr << " -> " << method_signature_ptr << endl;
cout << "測試結果 - 定位方法位置:" << start_location_ptr << " -> " << end_location_ptr + 1 << endl;
cout << "測試結果 - 異常類的名稱:" << exception_class_name << endl;

cout << "測試結果-輸出異常資訊(可以分析行號):" << endl;
jclass throwable_class = (*env).FindClass("java/lang/Throwable");
jmethodID print_method = (*env).GetMethodID(throwable_class, "printStackTrace", "()V");
(*env).CallVoidMethod(exception, print_method);

}


JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) {
    jvmtiEnv *gb_jvmti = nullptr;
    //初始化
    vm->GetEnv(reinterpret_cast<void **>(&gb_jvmti), JVMTI_VERSION_1_0);
    // 建立一個新的環境
    jvmtiCapabilities caps;
    memset(&caps, 0, sizeof(caps));
    caps.can_signal_thread = 1;
    caps.can_get_owned_monitor_info = 1;
    caps.can_generate_method_entry_events = 1;
    caps.can_generate_exception_events = 1;
    caps.can_generate_vm_object_alloc_events = 1;
    caps.can_tag_objects = 1;
    // 設定當前環境
    gb_jvmti->AddCapabilities(&caps);
    // 建立一個新的回撥函式
    jvmtiEventCallbacks callbacks;
    memset(&callbacks, 0, sizeof(callbacks));
    //異常回撥
    callbacks.Exception = &callbackException;
    // 設定回撥函式
    gb_jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));
    // 開啟事件監聽(JVMTI_EVENT_EXCEPTION)
    gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, nullptr);
    return JNI_OK;
}

JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) {
}

測試結果

在 VM vptions 中配置:-agentpath:E:\itstack\git\github.com\itstack-jvmti\cmake-build-debug\libitstack_jvmti.dll

十二月 16, 2020 23:53:27 下午 org.itstack.demo.PartnerEggResourceImpl queryUserInfoById
資訊: 根據使用者Id獲取使用者資訊null
java.lang.NullPointerException: 根據使用者Id獲取使用者資訊,空指標異常
	at org.itstack.demo.PartnerEggResourceImpl.queryUserInfoById(TestLocationException.java:26)
	at org.itstack.demo.TestLocationException.main(TestLocationException.java:13)
測試結果-定位類的簽名:Lorg/itstack/demo/PartnerEggResourceImpl;
測試結果-定位方法資訊:queryUserInfoById -> (Ljava/lang/String;)Ljava/lang/Object;
測試結果-定位方法位置:0 -> 43
測試結果-異常類的名稱:Ljava/lang/NullPointerException;
測試結果-輸出異常資訊(可以分析行號):
  • 這就是基於JVMTI的方式進行監控,這樣的方式可以做到非入侵程式碼。不需要硬編碼,也就節省了人力,否則所有人都會進行開發監控內容,而這部分內容與業務邏輯並無關係。

2.2 擴充套件執行緒監控

其實方法差不多,都是基於C++開發DLL檔案,引入使用。不過這部分程式碼會監控方法資訊,並採集執行緒的執行內容。

static void JNICALL callbackMethodEntry(jvmtiEnv *jvmti_env, JNIEnv *env, jthread thr, jmethodID method) {
    // 獲得方法對應的類
    jclass clazz;
    jvmti_env->GetMethodDeclaringClass(method, &clazz);

    // 獲得類的簽名
    char *class_signature;
    jvmti_env->GetClassSignature(clazz, &class_signature, nullptr);

    //過濾非本工程類資訊
    string::size_type idx;
    string class_signature_str = class_signature;
    idx = class_signature_str.find("org/itstack");

    gb_jvmti->RawMonitorEnter(gb_lock);

    {
        //must be deallocate
        char *name = NULL, *sig = NULL, *gsig = NULL;
        jint thr_hash_code = 0;

        error = gb_jvmti->GetMethodName(method, &name, &sig, &gsig);
        error = gb_jvmti->GetObjectHashCode(thr, &thr_hash_code);

        if (strcmp(name, "start") == 0 || strcmp(name, "interrupt") == 0 ||
            strcmp(name, "join") == 0 || strcmp(name, "stop") == 0 ||
            strcmp(name, "suspend") == 0 || strcmp(name, "resume") == 0) {

            //must be deallocate
            jobject thd_ptr = NULL;
            jint hash_code = 0;
            gb_jvmti->GetLocalObject(thr, 0, 0, &thd_ptr);
            gb_jvmti->GetObjectHashCode(thd_ptr, &hash_code);

            printf("[執行緒監控]: thread (%10d) %10s (%10d)\n", thr_hash_code, name, hash_code);
        }
    }

    gb_jvmti->RawMonitorExit(gb_lock);
}

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *jvm, char *options, void *reserved) {

    // 初始化
    jvm->GetEnv((void **) &gb_jvmti, JVMTI_VERSION_1_0);
    // 建立一個新的環境
    memset(&gb_capa, 0, sizeof(jvmtiCapabilities));
    gb_capa.can_signal_thread = 1;
    gb_capa.can_get_owned_monitor_info = 1;
    gb_capa.can_generate_method_exit_events = 1;
    gb_capa.can_generate_method_entry_events = 1;
    gb_capa.can_generate_exception_events = 1;
    gb_capa.can_generate_vm_object_alloc_events = 1;
    gb_capa.can_tag_objects = 1;
    gb_capa.can_generate_all_class_hook_events = 1;
    gb_capa.can_generate_native_method_bind_events = 1;
    gb_capa.can_access_local_variables = 1;
    gb_capa.can_get_monitor_info = 1;
    // 設定當前環境
    gb_jvmti->AddCapabilities(&gb_capa);
    // 建立一個新的回撥函式
    jvmtiEventCallbacks callbacks;
    memset(&callbacks, 0, sizeof(jvmtiEventCallbacks));
    // 方法回撥
    callbacks.MethodEntry = &callbackMethodEntry;
    // 設定回撥函式
    gb_jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));

    gb_jvmti->CreateRawMonitor("XFG", &gb_lock);

    // 註冊事件監聽(JVMTI_EVENT_VM_INIT、JVMTI_EVENT_EXCEPTION、JVMTI_EVENT_NATIVE_METHOD_BIND、JVMTI_EVENT_CLASS_FILE_LOAD_HOOK、JVMTI_EVENT_METHOD_ENTRY、JVMTI_EVENT_METHOD_EXIT)
    error = gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_INIT, (jthread) NULL);
    error = gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, (jthread) NULL);
    error = gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_NATIVE_METHOD_BIND, (jthread) NULL);
    error = gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, (jthread) NULL);
    error = gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, (jthread) NULL);
    error = gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_EXIT, (jthread) NULL);

    return JNI_OK;
}
  • 從監控的程式碼可以看到,這裡有執行緒的 start、stop、join、interrupt 等,並可以記錄執行資訊。
  • 另外這裡監控的方法執行回撥,SetEventCallbacks(&callbacks, sizeof(callbacks)); 以及相應事件的新增。

六、總結

  • 如果說你所經歷的業務體量很小,那麼幾乎並不需要如此複雜的技術棧深度學習,甚至幾乎不需要擴充套件各類功能,也不需要監控。但終究有一些需要造飛機的大廠,他們的業務體量龐大,併發數高,讓原本可能就是一個簡單的查詢介面,也要做熔斷、降級、限流、快取、執行緒、非同步、預熱等等操作。
  • 知其然才敢用,如果對一個技術點不是太熟悉,就不要胡亂使用,否則遇到的OOM並不是那麼好復現,尤其是在併發場景下。當然如果你們技術體系中有各種服務,比如流量復現、鏈路追蹤等等,那麼還好。
  • 又扯到了這,一個堅持學習、分享、沉澱的男人!好了,如果有錯字、內容不準確,歡迎直接懟給我,我喜歡接受。但不要欺負我哦哈哈哈哈哈!

七、系列推薦

相關文章