線程本地存儲 ThreadLocal

来源:https://www.cnblogs.com/feiyu2/archive/2022/08/08/ThreadLocal.html
-Advertisement-
Play Games

線程本地存儲 · 語雀 (yuque.com) 線程本地存儲提供了線程記憶體儲變數的能力,這些變數是線程私有的。 線程本地存儲一般用在跨類、跨方法的傳遞一些值。 線程本地存儲也是解決特定場景下線程安全問題的思路之一(每個線程都訪問本線程自己的變數)。 Java 語言提供了線程本地存儲,ThreadLo ...


線程本地存儲 · 語雀 (yuque.com)

線程本地存儲提供了線程記憶體儲變數的能力,這些變數是線程私有的。

線程本地存儲一般用在跨類、跨方法的傳遞一些值。

線程本地存儲也是解決特定場景下線程安全問題的思路之一(每個線程都訪問本線程自己的變數)。

Java 語言提供了線程本地存儲,ThreadLocal 類。

1659940416781-c0e6634b-3e1d-4982-8dd4-df6dddeaf0c7.jpeg

ThreadLocal 的使用及註意事項

public class TestClass {
    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        // 設置值
        threadLocal.set(1);
        test();
    }

    private static void test() {
        // 獲取值,返回 1
        threadLocal.get();
        // 防止記憶體泄漏
        threadLocal.remove();
    }
}

static 修飾的變數是在類在載入時就分配地址了,在類卸載才會被回收,因此使用 static 的 ThreadLocal,延長了 ThreadLocal 的生命周期,可能會導致記憶體泄漏。

分配使用了 ThreadLocal,又不調用 get()、set()、remove() 方法,並且當前線程遲遲不結束的話,那麼就會導致記憶體泄漏。

ThreadLocal 的 set() 過程

1658590040855-f69d5460-ffa7-49b9-9f49-e93b9988977b.png

每一個 Thread 實例對象中,都會有一個 ThreadLocalMap 實例對象;

ThreadLocalMap 是一個 Map 類型,底層數據結構是 Entry 數組;

一個 Entry 對象中又包含一個 key 和 一個 value

  • key 是 ThreadLocal 實例對象的弱引用
  • value 就是通過 ThreadLocal#set() 方法實際存儲的值
static class Entry extends WeakReference<ThreadLocal<?>> {
    /**
     * The value associated with this ThreadLocal.
     */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

下麵我們通過源碼分析 ThreadLocal#set() 的過程。

  • 獲取當前線程
  • 獲取當前線程的 ThreadLocalMap
  • 將存儲的值設置到 ThreadLocalMap
public void set(T value) {
    // 獲取當前線程
    Thread t = Thread.currentThread();
    // 獲取當前線程的 ThreadLocalMap
    ThreadLocal.ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 將存儲的值設置到 ThreadLocalMap
        map.set(this, value);
    } else {
        // 首次設置存儲的值,需要創建 ThreadLocalMap
        createMap(t, value);
    }
}

ThreadLocalMap 的記憶體泄露

介紹記憶體泄漏

記憶體泄漏(Memory leak)

本質上,記憶體泄漏可以定義為:當進程不再需要某些記憶體的時候,這些不再被需要的記憶體依然沒有被進程回收。

造成記憶體泄漏的原因:不再需要(沒有作用)的實例對象依然存在著強引用關係,無法被垃圾收集器回收

記憶體泄露的原因分析

ThreadLocalMap 是一個 Map 類型,底層數據結構是 Entry 數組;

一個 Entry 對象的 key 是 ThreadLocal 實例對象的弱引用。

一個對象如果只剩下弱引用,則該對象在垃圾收集時就會被回收

ThreadLocalMap 使用 ThreadLocal 實例對象的弱引用作為 key 時,如果一個 ThreadLocal 實例對象沒有強引用引用它,比如手動將 ThreadLocal A 這個對象賦值為 null,那麼系統垃圾收集時,這個 ThreadLocal A 勢必會被回收,這樣一來 ThreadLocalMap 中就出現了 key 為 null 的 Entry,Java 程式沒有辦法訪問這些 key 為 null 的 Entry,故沒有辦法刪除 Entry 對 value 的強引用,則這個 value 無法被回收,直到線程的生命周期結束。

  • 如果當前線程遲遲不結束的話(比如使用了線程池,或者當前線程還在執行其他耗時的任務)那麼這些 key 為 null 的 Entry 的 value 就會一直存在一條強引用鏈,導致 value 無法被回收。
  • 只有當前線程結束以後,ThreadRef 就不存在於棧中了,強引用斷開,Thread 對象、ThreadLocalMap 對象、Entry 數組、Entry 對象、value 依次回收。

造成記憶體泄漏的原因是:由於 ThreadLocalMap 的生命周期跟 Thread 一樣長,當 Thread 的生命周期過長時,導致 value 無法回收,而不是因為弱引用。

  • Entry 對象的 key 是 ThreadLocal 實例對象的弱引用,造成 value 無法被回收。實際是 ThreadLocalMap 的設計中,已經考慮到了這種情況,也加上了一些防護措施,我們在下麵記憶體泄漏的解決辦法中介紹。
  • 如果 Entry 對象的 key 是 ThreadLocal 實例對象的強引用的話,那麼會造成 key 和 value 都無法被回收。

強引用鏈如下圖紅線所示:

強引用鏈的表述如下:

ThreadRef 引用 Thread,Thread 引用 ThreadLocalMap,ThreadLocalMap 引用 Entry,Entry 引用 value

1658636013370-07d35558-51e5-4188-aaca-d7bbaa115f11.png

記憶體泄露的解決辦法

Entry 對象的 key 是 ThreadLocal 實例對象的弱引用,造成 value 無法被回收。

實際是 ThreadLocalMap 的設計中,已經考慮到了這種情況,也加上了一些防護措施。

在調用 ThreadLocal 的 get()、set() 方法操作數據,從指定位置開始遍歷 Entry 時,會找到 Entry 不為 null,但 key 為 null 的 Entry,並刪除 key 為 null 的 Entry 的 value 和對應的 Entry。


但是,如果 ThreadLocal 實例對象的強引用被刪除後,線程長時間存活,又沒有再對該線程的 ThreadLocalMap 實例對象進行操作,也就是沒有再調用 get()、set() 方法,那麼依然會存在記憶體泄漏。

所以,避免記憶體泄漏最好的做法是:主動調用 ThreadLocal 對象的 remove() 方法,將設置的線程本地變數的值刪除。

/**
 * Remove the entry for key.
 */
private void remove(ThreadLocal<?> key) {
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

get()、set()、remove() 實際都會調用 ThreadLocalMap#expungeStaleEntry() 方法來進行刪除 Entry,下麵我們來看一下代碼實現。

// 入參 staleSlot 是當前被刪除對象在 Entry 數組中的位置
private int expungeStaleEntry(int staleSlot) {
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;

    // 刪除 staleSlot 位置的 value,key 已經在進入該方法前刪除了 / 已經被回收
    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    // 將 Entry 對象賦值為 null,斷開 Entry 實例對象的強引用
    tab[staleSlot] = null;
    // Entry 數組大小 - 1
    size--;

    // Rehash until we encounter null
    ThreadLocal.ThreadLocalMap.Entry e;
    int i;
    // for 迴圈的作用是從當前位置開始向後迴圈處理 Entry 中的 ThreadLocal 對象
    // 將從指定位置開始,遇到 null 之前的所有 ThreadLocal 對象 rehash
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        // 獲取 ThreadLocal 的虛引用引用的實例對象
        ThreadLocal<?> k = e.get();
        if (k == null) {
            // 虛引用引用的實例對象為 null,說明 ThreadLocal 已經被回收了
            // 則刪除 value 和 Entry,讓虛擬機能夠回收
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // rehash
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;
                // 從當前 h 的位置向後找,找到一個 null 的位置將 e 填入
                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null) {
                    h = nextIndex(h, len);
                }

                tab[h] = e;
            }
        }
    }
    return i;
}

ThreadLocalMap 的哈希衝突

ThreadLocalMap 里處理 hash 衝突的機制不是像 HashMap 一樣使用鏈表(拉鏈法)。

它採用的是另一種經典的處理方式,沿著衝突的索引向後查找空閑的位置(開放定址法中的線性探測法)。

下麵我們通過 ThreadLocal 的 set()、get() 方法源碼,分析 ThreadLocalMap 的哈希衝突解決方案。

// set() 的關鍵方法,被 set(Object value) 調用
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    // 計算 key 在數組中的下標,其實就是 ThreadLocal 的 hashCode 和 數組大小-1 取餘
    int i = key.threadLocalHashCode & (len - 1);

    // 整體策略:查看 i 索引位置有沒有值,有值的話,索引位置 + 1,直到找到沒有值的位置
    // 這種解決 hash 衝突的策略,也導致了其在 get 時查找策略有所不同,體現在 getEntryAfterMis
    // nextIndex() 就是讓在不超過數組長度的基礎上,把數組的索引位置 + 1
    for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        // 找到記憶體地址一樣的 ThreadLocal,直接替換
        // 即,修改線程本地變數
        if (k == key) {
            e.value = value;
            return;
        }

        // 當前 key 是 null,說明 ThreadLocal 被清理了,直接替換掉並返回
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 當前 i 位置是無值的,可以被當前 thradLocal 使用
    tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
    int sz = ++size;
    // 當數組大小大於等於擴容閾值(數組大小的三分之二)時,進行擴容
    if (!cleanSomeSlots(i, sz) && sz >= threshold) {
        rehash();
    }
}

上面源碼我們註意幾點:

  1. 是通過遞增的 AtomicInteger 作為 ThreadLocal 的 hashCode 的;
  2. 計算數組索引位置的公式是:hashCode 取模 數組大小-1,由於 hashCode 不斷自增,所以不同的 hashCode 大概率上會計算到同一個數組的索引位置(但這個不用擔心,在實際項目中,ThreadLocal 都很少,基本上不會衝突);
  3. 通過 hashCode 計算的索引位置 i 處如果已經有值了,會從 i 開始,通過 +1 不斷的往後尋找,直到找到索引位置為空的地方,把當前 ThreadLocal 作為 key 放進去。

// get 的關鍵方法,被 get() 方法調用

// 得到當前 thradLocal 對應的值,值的類型是由 thradLocal 的泛型決定的
// 首先嘗試根據 hashcode 取模 數組大小-1 = 索引位置 i 尋找,找不到的話,自旋把 i+1,直到找到
private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    ThreadLocal.ThreadLocalMap.Entry e = table[i];
    // e 不為空,並且 e 的 ThreadLocal 的記憶體地址和 key 相同,直接返回,否則就是沒有找到,繼續尋找
    if (e != null && e.get() == key) {
        return e;
    } else {
        // 這個取數據的邏輯,是因為 set 時數組索引位置衝突造成的
        return getEntryAfterMiss(key, i, e);
    }
}

// 自旋 i+1,直到找到為止
private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal<?> key, int i, ThreadLocal.ThreadLocalMap.Entry e) {
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        // 記憶體地址一樣,表示找到了
        if (k == key) {
            return e;
        }
        // 刪除不再使用的 Entry,避免記憶體泄漏
        if (k == null) {
            expungeStaleEntry(i);
        } else {
            // 繼續使索引位置 + 1
            i = nextIndex(i, len);
        }
        e = tab[i];
    }
    return null;
}

ThreadLocalMap 的擴容策略

// set() 的部分源碼
if (!cleanSomeSlots(i, sz) && sz >= threshold){
    rehash();
}

// 稱為啟髮式清理,從指定下標開始遍歷
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        ThreadLocal.ThreadLocalMap.Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

private void rehash() {
    // 探測式清理,從數組的下標為 0 處開始遍歷,清理所有無用的 Entry
    expungeStaleEntries();

    // 擴容使用較低的閾值,以避免遲滯
    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

由上面源碼我們可以看出,ThreadLocalMap 擴容的時機是,ThreadLocalMap 中的 ThreadLocal 的個數超過閾值,並且 cleanSomeSlots() 返回 false(啟髮式清理),然後嘗試清理所有 key 為 null 的 Entry,清理完之後 ThreadLocal 的個數仍然大於閾值的四分之三,ThreadLocalMap 就要開始擴容了, 我們一起來看下擴容的邏輯:

// 擴容
private void resize() {
    // 拿出舊的數組
    ThreadLocal.ThreadLocalMap.Entry[] oldTab = table;
    int oldLen = oldTab.length;
    // 新數組的大小為老數組的兩倍
    int newLen = oldLen * 2;
    // 初始化新數組
    ThreadLocal.ThreadLocalMap.Entry[] newTab = new ThreadLocal.ThreadLocalMap.Entry[newLen];
    int count = 0;
    // 老數組的值拷貝到新數組上
    for (int j = 0; j < oldLen; ++j) {
        ThreadLocal.ThreadLocalMap.Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                // 計算 ThreadLocal 在新數組中的位置
                int h = k.threadLocalHashCode & (newLen - 1);
                // 如果索引 h 的位置值不為空,往後+1,直到找到值為空的索引位置
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                // 給新數組賦值
                newTab[h] = e;
                count++;
            }
        }
    }
    // 給新數組初始化下次擴容閾值,為數組長度的三分之二
    setThreshold(newLen);
    size = count;
    table = newTab;
}

源碼註解也比較清晰,我們註意兩點:

  1. 擴容後數組大小是原來數組的兩倍,下一次的擴容閾值為數組長度的三分之二;
  2. 擴容時是沒有線程安全問題的,因為 ThreadLocalMap 是線程的一個屬性,一個線程同一時刻只能對 ThreadLocalMap 進行操作,因為同一個線程執行業務邏輯必然是串列的,那麼操作 ThreadLocalMap 必然也是串列的。

ThreadLocalMap 擴容策略的語言描述:

在 ThreadLocalMap.set() 方法的最後,如果執行完啟髮式清理工作後,未清理到任何 Entry,且當前數組中 Entry 的數量已經達到了擴容閾值(數組長度的三分之二),就開始執行 rehash() 邏輯。

rehash() 首先是會進行探測式清理工作,從數組的起始位置開始遍歷,查找 key 為 null 的 Entry 並清理。清理完成之後如果 ThreadLocal 的個數仍然大於等於擴容閾值的四分之三,那麼就進行擴容操作,擴容為原來數組長度的兩倍,並且設置下一次的擴容閾值為新數組長度的三分之二。

InheritableThreadLocal 與繼承性

通過 ThreadLocal 創建的線程變數,其子線程是無法繼承的。

也就是說你線上程中通過 ThreadLocal 創建了線程變數 V,而後該線程創建了子線程,你在子線程中是無法通過 ThreadLocal 來訪問父線程的線程變數 V 的。

public class TestClass {
    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        threadLocal.set(1);
        // 返回 1
        threadLocal.get();

        new Thread(new Runnable() {
            @Override
            public void run() {
                // 返回 null
                threadLocal.get();
            }
        }).start();
    }
}

如果你需要子線程繼承父線程的線程變數,那該怎麼辦呢?

JDK 的 InheritableThreadLocal 類可以完成父線程到子線程的值傳遞。

InheritableThreadLocal 是 ThreadLocal 子類,所以用法和 ThreadLocal 相同。

使用時,改為 ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>(); 即可。

InheritableThreadLocal 在創建子線程的時候(初始化線程時),在 Thread#init() 方法中拷貝父線程中本地變數的值到子線程的本地變數中,子線程就擁有了和父線程一樣的本地變數。

下麵是 Thread#init() 中,和 ThreadLocal 相關的代碼,我們一起來看下這個功能是怎麼實現的

public class Thread implements Runnable {
    // 如果是使用 ThreadLocal 進行 set(),則使用該變數保存
    ThreadLocal.ThreadLocalMap threadLocals = null;
    // 如果是使用 InheritableThreadLocal 進行 set(),則使用該變數保存
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

    private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc) {
        // ...
        Thread parent = currentThread();
        // ...
        if (parent.inheritableThreadLocals != null) {
            // 根據 parent.inheritableThreadLocals 重新 new 一個 ThreadLocalMap 對象
            this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        }
        // ...
    }
}

不過,完全不建議你線上程池中使用 InheritableThreadLocal,不僅僅是因為它具有 ThreadLocal 相同的缺點:可能導致記憶體泄露,更重要的原因是:線程池中線程的創建是動態的,很容易導致繼承關係錯亂,如果你的業務邏輯依賴 InheritableThreadLocal,那麼很可能導致業務邏輯計算錯誤,而這個錯誤往往比記憶體泄露更要命。

同時,如果父線程的本地變數是引用數據類型的話,父子線程共用相同的數據,存線上程安全問題,甚至導致業務邏輯計算錯誤。要想做到父子線程的本地變數互不影響,則需要繼承 InheritableThreadLocal 並重寫 childValue() 方法實現對象的深拷貝 。

並且對於使用線程池等會池化復用線程的執行組件的情況,線程由線程池創建好,並且線程是池化起來反覆使用的;這時父子線程關係的 ThreadLocal 值傳遞已經沒有意義,應用需要的實際上是把任務提交給線程池時的ThreadLocal 值傳遞到任務執行時。阿裡開源的 TransmittableThreadLocal 類繼承並加強 InheritableThreadLocal 類,解決上述的問題。

TransmittableThreadLocal

TransmittableThreadLocal 的 GitHub:https://github.com/alibaba/transmittable-thread-local

TransmittableThreadLocal 的 API 文檔:https://alibaba.github.io/transmittable-thread-local

TransmittableThreadLocal 是阿裡開源的一個增強 InheritableThreadLocal 的庫。

TransmittableThreadLocal 的功能:在使用線程池等會池化復用線程的執行組件情況下,提供 ThreadLocal 值的傳遞功能,解決非同步執行時上下文傳遞的問題。

TTL 的使用及註意事項

TTL 的 User Guide:https://github.com/alibaba/transmittable-thread-local#-user-guide

TransmittableThreadLocal 有三種使用方式(具體使用見 GitHub 的 README):

  • 修飾 Runnable 或 Callable
  • 修飾線程池
  • 使用 Java Agent 來修飾 JDK 線程池實現類

註意事項:

使用 TtlRunnable 和 TtlCallable 來修飾傳入線程池的 Runnable 和 Callable 時,即使是同一個 Runnable 任務多次提交到線程池時,每次提交時都需要通過修飾操作(即TtlRunnable.get(task))以抓取這次提交時的 TransmittableThreadLocal 上下文的值;即如果同一個任務下一次提交時不執行修飾而仍然使用上一次的 TtlRunnable,則提交的任務運行時會是之前修飾操作所抓取的上下文。

修飾線程池其實本質上也是修飾 Runnable,只是將這個邏輯移到了 ExecutorServiceTtlWrapper.submit() 方法內,對所有提交的 Runnable 進行修飾。


public class Main {
    static int val = 0;

    public static void main(String[] args) {
        TransmittableThreadLocal<String> ttl = new TransmittableThreadLocal();

        ExecutorService executorService = Executors.newFixedThreadPool(1);
        Runnable task = new Runnable() {
            @Override
            public void run() {
                System.out.println("child thread get " + ttl.get());
            }
        };
        for (int i = 0; i < 5; i++) {
            val++;
            ttl.set("value-set-in-parent " + val);
            executorService.execute(TtlRunnable.get(task));
        }
        executorService.shutdown();
    }
}

TTL 的原理

1659933918179-bf853ad8-db00-4320-b20d-3e7479fd3f3c.png

TTL 做的是,使用裝飾器模式裝飾 Runnable 等任務,將原本與 Thread 綁定的線程變數,緩存一份到 TtlRunnable 對象中,每次調用任務的 run() 前後進行 set() 和還原數據。

TTL 的需求場景

需求場景說明

總結

使用 ThreadLocal 庫友好地解決了線程本地存儲的問題,但是它還存在父子線程值傳遞丟失的問題,於是 JDK 又引入了 InheritableThreadLocal 對象。

InheritableThreadLocal 的出現又引出了下一個問題,那就是涉及到線程池等復用線程場景時,還是會存在變數複製混亂的缺陷。阿裡巴巴提供瞭解決方案,用 TransmittableThreadLocal 來增強 InheritableThreadLocal 對象。

參考資料

30 | 線程本地存儲模式:沒有共用,就沒有傷害-極客時間 (geekbang.org)

ThreadLocal原理分析及記憶體泄漏演示-極客時間 (geekbang.org)

ThreadLocal如何在父子線程及線程池中傳遞?-極客時間 (geekbang.org)

https://github.com/alibaba/transmittable-thread-local

本文來自博客園,作者:真正的飛魚,轉載請註明原文鏈接:https://www.cnblogs.com/feiyu2/p/ThreadLocal.html


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

-Advertisement-
Play Games
更多相關文章
  • 有時候我們需要把自己寫的類或者函數給別人使用,但又不希望讓別人知道具體的實現,那麼封裝成庫就是一個很好的方法。本文描述了怎麼去把一個C++程式封裝成一個靜態庫並且如何去使用這些靜態庫。 ...
  • 精華筆記: package:聲明包 作用:避免類的命名衝突 同包中的類不能同名,但不同包中的類可以同名 類的全稱:包名.類名,包名常常有層次結構 建議:包名所有字母都小寫 import:導入類 同包中的類可以直接訪問 不同包中的類不能直接訪問,若想訪問: 先import導入類再使用類 建議 類的全稱 ...
  • Java常用類 5.其他常用類 5.1Math類 java.lang.Math提供了一系列靜態方法用於科學計算;其方法的參數和返回值類型一般為double型。如果需要更加強大的數學運算能力,計算高等數學中相關內容,可以使用apache commons下麵的Math類庫。 package li.nor ...
  • 課程導讀 俗話說:工欲善其事必先利其器。想要快速寫出好的代碼,更是離不開一個好的工具。在這個快速發展的社會,一個好的工具,能幫我們在開發過程中節省大量的開發時間。本套課程給同學們帶來Java目前最流行,最好用的集成開發工具Intellij Idea。(PS:這套課程是面向所有階段的學員的哦~) ht ...
  • 集成 Spring Doc 介面文檔和 knife4j 前面已經集成 MyBatis Plus、Druid 數據源,開發了 5 個介面。在測試這 5 個介面時使用了 HTTP Client 或 PostMan,無論是啥都比較麻煩:得自己寫請求地址 URL、請求參數等,於是多年前就出現了 Swagg... ...
  • 前言 😋 嗨嘍,大家好呀~這裡是愛看美女的茜茜吶 環境開發: Python 3.8 Pycharm 模塊使用: requests parsel csv 基本流程思路: 告訴你 實現程式 應該怎麼去操作 一. 數據來源分析: 分析我們想要數據內容在哪裡? 請求那個網站, 可以得到相應的數據 抓包分析 ...
  • 精華筆記: 向上造型: 代碼復用 超類型的引用指向派生類的對象 能點出來什麼,看引用的類型 這是規定,記住就OK 何時向上造型: 多種角色能幹的事都一樣的時候,可以將多種角色統一造型到超類數組中,實現代碼復用 eg: 學生/老師/醫生都是輸出名字+問好 乾的事都一樣, ​ 就可以將學生/老師/醫生統 ...
  • 前兩天一個鄰居發出了靈魂質問:“為什麼我買的180平和你的169平看上去一樣大?” “因為咱倆的套內面積都是138平......” 我們去看房子,比較不同樓盤的價格,看的都是單價,可這個單價,卻是用(總價 ÷ 建築面積)計算的。而我們實際買到手裡的,是套內面積。 套內面積 = 使用面積+牆體厚度+陽 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...