ThreadLocal 源碼淺析

来源:https://www.cnblogs.com/fuxing/p/18264815
-Advertisement-
Play Games

多線程在訪問同一個共用變數時很可能會出現併發問題,特別是在多線程對共用變數進入寫入時,那麼除了加鎖還有其他方法避免併發問題嗎?本文將詳細講解 ThreadLocal 的使用及其源碼。 ...


前言

多線程在訪問同一個共用變數時很可能會出現併發問題,特別是在多線程對共用變數進行寫入時,那麼除了加鎖還有其他方法避免併發問題嗎?本文將詳細講解 ThreadLocal 的使用及其源碼。


一、什麼是 ThreadLocal?

ThreadLocal 是 JDK 包提供的,它提供了線程本地變數,也就是說,如果你創建了一個 ThreadLocal 變數,那麼訪問這個變數的每一個線程,都創建這個變數的一個本地副本。

這樣可以解決什麼問題呢?當多個線程操作這個變數時,實際操作的是自己線程本地記憶體里的數據,從而避免線程安全問題

如下圖,線程表中的每個線程,都有自己 ThreadLocal 變數,線程操作這個變數只是在自己的本地記憶體在,跟其他線程是隔離的。

image.png

二、如何使用 ThreadLocal

ThreadLocal 就是一個簡單的容器,使用起來也沒有難度,初始化後僅需通過 get/set 方法進行操作即可。

如下代碼,開闢兩個線程對 ThreadLocal 變數進行操作,獲取的值是不同的。

public class FuXing {

    /**
     * 初始化ThreadLocal
     */
    private static final ThreadLocal<String> myThreadLocal = new ThreadLocal<>();

    public static void main (String[] args) {
        // 線程1中操作 myThreadLocal
        new Thread(()->{
            myThreadLocal.set("thread 1");		//set方法設置值
            System.out.println(myThreadLocal.get());	//get方法獲取值"thread 1"
        },"thread 1").start();

        // 線程2中操作 myThreadLocal
        new Thread(()->{
            myThreadLocal.set("thread 2");		//set方法設置值
            System.out.println(myThreadLocal.get());	//get方法獲取值"thread 2"
        },"thread 2").start();
    }
}

三、ThreadLocal 實現原理

ThreadLocal 是如何保證操作的對象只被當前線程進行訪問呢,我們通過源碼一起進行分析學習。

一般分析源碼我們都先看它的構造方法是如何初始化的,接著通過對 ThreadLocal 的簡單使用,我們知道了關鍵的兩個方法 set/get,所以源碼分析也按照這個順序。

1. 構造方法

泛型類的空參構造,沒有什麼特別的

2. set 方法源碼

源碼如下,ThreadLocalMap 是什麼呢?由於比較複雜,這裡先不做解釋,你暫時可以理解為是一個 HashMap,其中 key 為 ThreadLocal 當前對象,value 就是我們設置的值,後面會單獨解釋源碼。

public void set(T value) {
    //獲取本地線程
    Thread t = Thread.currentThread();

    //獲取當前線程下的threadLocals對象,對象類型是ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //獲取到則添加值
        map.set(this, value);
    else
        //否則初始化ThreadLocalMap --第一次設置值
        createMap(t, value);
}
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

3. get 方法源碼

public T get() {
    //獲取本地線程
    Thread t = Thread.currentThread();

    //獲取當前線程下的threadLocals對象,對象類型是ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {

        //通過當前的ThreadLocal作為key去獲取對應value
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            //@SuppressWarnings忽略告警的註解
            //"unchecked"表示未經檢查的轉換相關的警告,通常出現在泛型編程中
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //threadLocals為空或它的Entry為空時,需要對其進行初始化操作。
    return setInitialValue();
}
private T setInitialValue() {
    //初始化為null
    T value = initialValue();
    
    //獲取當前線程
    Thread t = Thread.currentThread();
    
    //獲取當前線程下的threadLocals對象,對象類型是ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    
    //返回的其實就是個null
    return value;
}
protected T initialValue() {
    return null;
}

4. remove 方法源碼

核心也是 ThreadLocalMap 中的 remove 方法,會刪除 key 對應的 Entry,具體源碼後面統一在 ThreadLocalMap 源碼中分析。

public void remove() {
    //獲取當前線程下的threadLocals對象,對象類型是ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        //通過當前的ThreadLocal作為key調用remove
        m.remove(this);
}

5. ThreadLocalMap 源碼

ThreadLocalMap 是 ThreadLocal 的一個靜態內部類,看了上面的幾個源碼解釋,可以瞭解到 ThreadLocalMap 其實才是核心。

簡單的說,ThreadLocalMap 與 HashMap 類似,如,初始容量 16,一定範圍內擴容,Entry 數組存儲等,那它與 HashMap 有什麼不同呢,下麵將對源碼進行詳解。

ThreadLocalMap 的底層數據結構:

image.png

5.1 常量

//初始容量,一定是2的冪等數。
private static final int INITIAL_CAPACITY = 16;

// Entry 數組
private Entry[] table;

//table的長度
private int size = 0;

//擴容閾值
private int threshold; 

//設置擴容閾值,長度的 2 / 3
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

//計算下一個存儲位置
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

// 計算前一個存儲位置
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

5.2 Entry 相關源碼

由於 Entry 是底層核心源碼,所有的操作幾乎都是圍繞著它來進行的,所以關於 Entry 的源碼會比較多,我一一拆分進行分析講解。

靜態內部類 Entry

這個是 ThreadLocalMap 的底層數據結構,Entry 數組,每個 Entry 對象,這裡的 Entry 繼承了 WeakReference,關於弱引用不懂得,可以看我的另一篇文章《Java 引用》

然後將 Entry 的 key 設置承了 弱引用,這有什麼作用呢?作用是當 ThreadLocal 失去強引用後,在系統GC時,只要發現弱引用,不管系統堆空間使用是否充足,都會回收掉 key,進而 Entry 被內部清理。

//靜態內部類Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        // key為弱引用
        super(k);
        value = v;
    }
}

獲取 Entry

拿到當前線程中對應的 ThreadLocal 所在的 Entry,找不到的話會重新尋找,因為當前的 Entry 可能已經擴容,擴容後會重新計算索引位置,詳情見擴容機制源碼。

源碼中的計算索引位置的演算法我沒有解釋,這個我會放在後面解釋,涉及到瞭如何解決 Hash 衝突的問題,這個和我們熟知的 HashMap 是不同的。

//獲取Entry
private Entry getEntry(ThreadLocal<?> key) {
    //計算索引位置
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];

    //找到了就返回Entry
    if (e != null && e.get() == key)
            return e;
    else
        //沒找到則重新尋找,因為可能發生擴容導致索引重新計算
        return getEntryAfterMiss(key, i, e);
}

//重新獲取Entry --從當前索引i的位置向後搜索
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    //迴圈遍歷,獲取對應的 ThreadLocal 所在的 Entry
    while (e != null) {
        //獲取Entry對象的弱引用,WeakReference的方法
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            //清除無效 Entry,詳解見下方
            expungeStaleEntry(i);
        else
            //計算下一個索引位置
            i = nextIndex(i, len);
        
        //可以理解為指針後移
        e = tab[i];
    }
    return null;
}

清除無效 Entry

expunge 刪除,抹去,stale 陳舊的,沒有用的

第 1 個方法:
根據索引刪除對應的桶位,並從給定索引開始,遍歷清除無效的 Entry,何為無效?就是當 Entry 的 key 為 null 時,代表 key 已經被 GC 掉了,對應的 Entry 就無效了。

第 2 個方法:
刪除Entry數組中所有無效的Entry,方法中的e.get() == null,代表key被回收了。

第 3 個方法:
清除一些失效桶位,它執行對數數量的掃描,向後遍歷logn個位置,如8,4,2,1。

方法 2、3 最後都通過方法 1 進行桶位的刪除。

//根據索引刪除對應的桶位
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    //刪除該桶位的元素,並將數組長度減1
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    Entry e;
    int i;
    //從當前索引開始,直到當前 Entry為null才會停止遍歷
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        //獲取Entry對象的弱引用,WeakReference的方法
        ThreadLocal<?> k = e.get();
        if (k == null) {//說明key已失效
            //刪除該桶位的元素,並將數組長度減1
            e.value = null;
            tab[i] = null;
            size--;
        } else {//說明key有效,需要將其Rehash
            //計算rehash後索引位置
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;
                //移動元素位置,若rehash後索引位置有其他元素,則繼續向後移動,直至為空
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    //直到當前 Entry為null才會停止遍歷,i為其索引
    return i;
}

//刪除Entry數組中所有無效的Entry,用於rehash時
private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        //獲取Entry對象的弱引用,Entry不為空而弱引用為空,代表被GC了
        if (e != null && e.get() == null)
            //根據索引刪除對應的桶位
            expungeStaleEntry(j);
    }
}

//清楚一些清除桶位,它執行對數數量的掃描
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    //向後遍歷logn個位置,如8,4,2,1
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        //獲取Entry對象的弱引用,Entry不為空而弱引用為空,代表被GC了
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            //根據索引刪除對應的桶位
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);//對數遞減
    return removed;
}

替換無效 Entry

替換失效元素,用在對 Entry 進行 set 操作時,如果 set 的 key 是失效的,則需要用新的替換它。

這裡不僅僅處理了當前的失效元素,還會將其他失效的元素進行清理,因為這裡是當 key 為 null 時才進行的替換操作。

那什麼時候 key 為 null 呢?這個除了主動的 remove 之外,就只有 ThreadLocal 的弱引用被 GC 掉了。

這裡是在 set 操作時出現的,還出現了 key 為 null 的無效元素,代表已經之前發生過 GC 了,很可能Entry 數組中還可能出現其他無效元素,所以源碼中會出現向前遍歷和向後遍歷的情況。

向前遍歷好理解,就是通過遍歷找第一個失效元素的索引。向後遍歷比較難理解,這裡我先簡單說一下 ThreadLocal 用的開放地址的方式來解決 hash 衝突的,具體原理我後面會在講 hash 衝突時單獨講。

這種情況下,很可能當前的失效元素對應的並不是 hascode 在 staleSlot 的Entry。因為 hash 衝突後,Entry 會後移,那麼此元素的 hascode 對應的桶位很有可能往後移了,所以我們要向後找到它,並且和當前的 staleSlot 進行替換。

如果不進行此操作的話,很有可能在 set 操作時,在 ThreadLocalMap 中會出現兩個桶位,都被某個ThreadLocal 指向。

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    //記錄失效元素的索引
    int slotToExpunge = staleSlot;
    //從失效元素位置向前遍歷,直到當前 Entry為null才會停止遍歷
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            //更新失效元素的索引,目的是找第一個失效的元素
            slotToExpunge = i;

    //從失效元素向後遍歷
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        //找到了對應key
        if (k == key) {
            //更新該位置的value
            e.value = value;
            //把失效元素換到當前位置
            tab[i] = tab[staleSlot];
            //把當前Entry移動到失效元素位置
            tab[staleSlot] = e;
            
            //slotToExpunge是第一個失效元素的索引,若條件成立,向前沒有失效元素
            if (slotToExpunge == staleSlot)
                //從當前索引開始,清理失效元素
                slotToExpunge = i;
            
            // 清理失效元素,詳情見清除無效Entry相關源碼
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
        
        //代表向前遍歷沒有找到第一個失效元素的位置
        if (k == null && slotToExpunge == staleSlot)
            //所以條件成立的i是向後遍歷的的第一個失效元素的位置
            slotToExpunge = i;
    }
    
    //沒找到key,則在失效元素索引的位置,新建Entry
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
    
    // 條件成立說明在找到了staleSlot前面找到了其他的失效元素
    if (slotToExpunge != staleSlot)
        
        // 清理失效元素,詳情見清除無效Entry相關源碼
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

5.3 構造方法

還有一個基於 parentMap 的構造方法,由於目前僅在創建 InheritableThreadLocal 時調用,關於它這裡不詳細展開,後續會針對該類進行詳解。

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 初始化數組
    table = new Entry[INITIAL_CAPACITY];

    //計算存儲位置
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

    //存儲元素,並將size設置為1
    table[i] = new Entry(firstKey, firstValue);
    size = 1;

    //設置擴容閾值
    setThreshold(INITIAL_CAPACITY);
}

5.4 set 方法源碼

設置 key,vlaue,key 就是 ThreadLocal 對象。

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    //計算索引位置
    int i = key.threadLocalHashCode & (len-1);

    //從當前索引開始,直到當前Entry為null才會停止遍歷
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        //如果key存在且等於當前key,代表之前存在的,直接覆蓋
        if (k == key) {
            e.value = value;
            return;
        }
        //如果key不存在,說明已失效,需要替換,詳情見替換無效Entry源碼
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    //沒有key則新建一個Entry即可
    tab[i] = new Entry(key, value);
    int sz = ++size;

    //清理一些失效元素,若清理失敗且達到常量中的擴容閾值,則進行rehash操作
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

//刪除Entry數組中所有無效的Entry並擴容
private void rehash() {
    //刪除Entry數組中所有無效的Entry
    expungeStaleEntries();
    if (size >= threshold - threshold / 4)
        //擴容,詳情見下麵的擴容機制源碼
        resize();
}

5.5 remove 方法源碼

刪除key對應的entry

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    //計算存儲位置
    int i = key.threadLocalHashCode & (len-1);
    
    //從當前索引開始,直到當前Entry為null才會停止遍歷
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            //清除該對象的強引用,下次在通過get方法獲取引用則返回null
            e.clear();

            //清除無效元素
            expungeStaleEntry(i);
            return;
        }
    }
}

5.6 擴容機制源碼

將元素轉移到新的Entry 數組,長度是原來的兩倍。

private void resize() {
    //創建原數組長度兩倍的新數組
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;	//計算當前元素數量
    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {	//key失效則值也順便設為null
                e.value = null; 	// Help the GC
            } else {
                //重新計算索引位置
                int h = k.threadLocalHashCode & (newLen - 1);

                //移動元素位置,若rehash後索引位置有其他元素,則繼續向後移動,直至為空
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }
    setThreshold(newLen);
    size = count;
    table = newTab;
}

四、ThreadLocalMap 的 Hash 衝突

Java 中大部分都是使用拉鏈法法解決 Hash 衝突的,而 ThreadLocalMap 是通過開放地址法來解決 Hash 衝突,這兩者有什麼不同,下麵我講介紹一下。

1. 拉鏈法

拉鏈法也叫鏈地址法,經典的就是 HashMap 解決 Hash 衝突的方法,如下圖。將所有的 hash 值相同的元素組成一個鏈表,除此外 HashMap 還進行了鏈表轉紅黑樹的優化。

image.png

2. 開放地址法

原理是當發生hash衝突時,不引入額外的數據結構,會以當前地址為基準,通過“多次探測”來處理哈希衝突,探測方式主要包括線性探測、平方探測和多次哈希等,ThreadLocalMap 使用的是線性探測法。

image.png

簡單說,就是一旦發生了衝突,就去探測尋找下一個空的散列地址,根據上面的源碼也能大致瞭解該處理方式。
源碼中的公式是key.threadLocalHashCode & (length - 1)

公式類似 HashMap 的定址演算法,詳情見HashMap源碼,由於數組長度是 2 的 n 次冪,所以這裡的與運算就是取模,得到索引 i,這樣做是為了分佈更均勻,減少衝突產生。

threadLocalHashCode 源碼如下:

private final int threadLocalHashCode = nextHashCode();

//初始化線程安全的Integer
private static AtomicInteger nextHashCode =
    new AtomicInteger();

//斐波那契散列乘數 --結果分佈更均勻
private static final int HASH_INCREMENT = 0x61c88647;

//自增返回下一個hash code
private static int nextHashCode() {
    
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

線性探測法的缺點:

  1. 不適用於存儲大量數據,容易產生“聚集現象”;
  2. 刪除元素需要清除無效元素;

五、註意事項

1. 關於記憶體泄漏

在瞭解了 ThreadLocal 的內部實現以後,我們知道了數據其實存儲在 ThreadLocalMap 中。這就意味著,線程只要不退出,則引用一直存在。

當線程退出時,Thread 類會對一些資源進行清理,其中就有threadLocals,源碼如下:

private void exit() {
    if (group != null) {
        group.threadTerminated(this);
        group = null;
    }
    target = null;
    //加速一些資源的清理
    threadLocals = null;
    inheritableThreadLocals = null;
    inheritedAccessControlContext = null;
    blocker = null;
    uncaughtExceptionHandler = null;
}

因此,當使用的線程一直沒有退出(如使用線程池),這時如果將一些大對象放入 ThreadLocal 中,且沒有及時清理,就可能會出現記憶體泄漏的風險

所以我們要養成習慣每次使用完 ThreadLocal 都要調用 remove 方法進行清理。

2. 關於數據混亂

通過對記憶體泄漏的解釋,我們瞭解了當使用的線程一直沒有退出,而又沒有即使清理 ThreadLocal,則其中的數據會一直存在。

這除了記憶體泄漏還有什麼問題呢?我們在開發過程中,請求一般都是通過 Tomcat 處理,而其在處理請求時採用的就是線程池。

這就意味著請求線程被 Tomcat 回收後,不一定會立即銷毀,如果不在請求結束後主動 remove 線程中的 ThreadLocal 信息,可能會影響後續邏輯,拿到臟數據。

我在開發過程中就遇到了這個問題,詳情見ThreadLocal中的用戶信息混亂問題。所以無論如何,在每次使用完 ThreadLocal 都要調用 remove 方法進行清理。

3. 關於繼承性

同一個 ThreadLocal 變數,在父線程中被設置值後,在子線程其實是獲取不到的。通過源碼我們也知道,我們操作的都是當前線程下的 ThreadLocalMap ,所以這其實是正常的。

測試代碼如下:

public class FuXing {

    /**
     * 初始化ThreadLocal
     */
    private static final ThreadLocal<String> myThreadLocal = new ThreadLocal<>();

    public static void main (String[] args) {
        myThreadLocal.set("father thread");
        System.out.println(myThreadLocal.get()); 	//father thread

        new Thread(()->{
            System.out.println(myThreadLocal.get());	//null
        },"thread 1").start();
    }
}

那麼這可能會導致什麼問題呢?比如我們在本服務調用外部服務,或者本服務開啟新線程去進行非同步操作,其中都無法獲取 ThreadLocal 中的值。

雖然都有其他解決方法,但是有沒有讓子線程也能直接獲取到父線程的 ThreadLocal 中的值呢?這就用到了 InheritableThreadLocal。

public class FuXing {

    /**
     * 初始化ThreadLocal
     */
    private static final InheritableThreadLocal<String> myThreadLocal 
            = new InheritableThreadLocal<>();

    public static void main (String[] args) {
        myThreadLocal.set("father thread");
        System.out.println(myThreadLocal.get()); 	//father thread

        new Thread(()->{
            System.out.println(myThreadLocal.get());	//father thread
        },"thread 1").start();
    }
}

InheritableThreadLocal 就是繼承了 ThreadLocal,在創建和獲取變數實例 inheritableThreadLocals 而不再是threadLocals,源碼如下。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

總結

本文主要講述了 ThreadLocal 的使用以及對其源碼進行了詳解,瞭解了 ThreadLocal 可以線程隔離的原因。通過對 ThreadLocalMap 的分析,知道了其底層數據結構和如何解決 Hash 衝突的。

最後通過對 ThreadLocal 特點的分析,瞭解到有哪些需要註意的點,避免以後開發過程中遇到類似問題,若發現其他問題歡迎指正交流。


參考:

[1] 翟陸續/薛賓田. Java併發編程之美.

[2] 葛一鳴/郭超. 實戰Java高併發程式設計.

[3] 靳宇棟. Hello 演算法.


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

-Advertisement-
Play Games
更多相關文章
  • 表格配置屬性說明文檔 頁面添加引用: import BaseTable from ‘@/components/BaseTable/index.vue 1、grid-edit-width 表格操作欄寬度 例如:grid-edit-width:250 2、gridOtherConfig 屬性 說明 示例 ...
  • Spring Cloud是一個相對比較成熟的微服務框架。雖然,Spring Cloud於2016年才推出1.0的release版本, 時間最短, 但是相比Dubbo等RPC框架, Spring Cloud提供的全套的分散式系統解決方案。 Spring Cloud是一系列框架的有序集合。它利用Spri ...
  • Redis 的單線程與多線程之爭 為什麼 Redis 使用單線程 Redis 單線程為什麼還那麼快 Redis 6.0 引入多線程的原因 Redis 的網路模型 結語 ...
  • 一、如何使用代理方式打開網頁 在 playwright.chromium.launch() 中傳入 proxy 參數即可,示例代碼如下: 1、同步寫法: from playwright.sync_api import sync_playwright proxy = {'server': 'http: ...
  • Charapter 1: 端倪 最近一直在用Pyglet做一個小的案例,但是實際運行起來時發現了嚴重的記憶體泄漏。經調查後發現平均每秒會爆出80-120不等的頁面錯誤。且可以觀察到記憶體正在不斷地以0.2-0.4mb不等的速度增長。 能夠複原此問題的代碼如下: # 導入庫 import pyglet w ...
  • 壓縮 PDF 文件能有效減小文件大小並提高文件傳輸的效率,同時還能節省電腦存儲空間。除了使用一些專業工具對PDF文件進行壓縮,我們還可以通過 Python 來執行該操作,實現自動化、批量處理PDF文件。 本文將分享一個簡單有效的使用 Python 壓縮 PDF 文件的方法。需要用到 Spire.P ...
  • 本文摘要:本文首先對I2C協議的通信模式和AT24C16-EEPROM晶元時序控制進行分析和理解,設計了一個i2c通信方案。人為按下寫操作按鍵後,FPGA(Altera EP4CE10)對EEPROM指定地址寫入位元組數據,並接後按下讀操作按鍵,讀取該地址上的一個位元組數據在數位管低兩位顯示出來。其中包 ...
  • 刷機 溫馨提示:如果你不知道root的意義在哪,建議不要解鎖和root,到時候救磚或者回鎖都挺麻煩。 刷全量包 最新版的系統沒有更新推送,所以去一加社區[0]找了個全量包來刷,。安裝方式可以看帖子里的內容,說的比較詳細,這裡截圖一部分: 解鎖bootloader 在系統與更新-》開發者選項 里勾選O ...
一周排行
    -Advertisement-
    Play Games
  • 問題 有很多應用程式在驗證JSON數據的時候用到了JSON Schema。 在微服務架構下,有時候各個微服務由於各種歷史原因,它們所生成的數據對JSON Object屬性名的大小寫規則可能並不統一,它們需要消費的JSON數據的屬性名可能需要大小寫無關。 遺憾的是,目前的JSON Schema沒有這方 ...
  • 首先下載centos07鏡像,建議使用阿裡雲推薦的地址: https://mirrors.aliyun.com/centos/7.9.2009/isos/x86_64/?spm=a2c6h.25603864.0.0.59b5f5ad5Nfr0X 其實這裡就已經出現第一個坑了 centos 07 /u ...
  • 相信很多.NETer看了標題,都會忍不住好奇,點進來看看,並且順便準備要噴作者! 這裡,首先要申明一下,作者本人也非常喜歡Linq,也在各個項目中常用Linq。 我愛Linq,Linq優雅萬歲!!!(PS:順便吐槽一下,隔壁Java從8.0版本推出的Streams API,抄了個四不像,一點都不優雅 ...
  • 在人生的重要時刻,我站在了畢業的門檻上,望著前方的道路,心中涌動著對未來的無限憧憬與些許忐忑。面前,兩條道路蜿蜒伸展:一是繼續在職場中尋求穩定,一是勇敢地走出一條屬於自己的創新之路。儘管面臨年齡和現實的挑戰,我仍舊選擇勇往直前,用技術這把鑰匙,開啟新的人生篇章。 迴首過去,我深知時間寶貴,精力有限。 ...
  • 單元測試 前言 時隔多個月,終於抽空學習了點新知識,那麼這次來記錄一下C#怎麼進行單元測試,單元測試是做什麼的。 我相信大部分剛畢業的都很疑惑單元測試是乾什麼的?在小廠實習了6個月後,我發現每天除了寫CRUD就是寫CRUD,幾乎用不到單元測試。寫完一個功能直接上手去測,當然這隻是我個人感受,僅供參考 ...
  • 一:背景 1. 講故事 最近在分析dump時,發現有程式的卡死和WeakReference有關,在以前只知道怎麼用,但不清楚底層邏輯走向是什麼樣的,藉著這個dump的契機來簡單研究下。 二:弱引用的玩法 1. 一些基礎概念 用過WeakReference的朋友都知道這裡面又可以分為弱短和弱長兩個概念 ...
  • 最近想把ET打表工具的報錯提示直接調用win系統彈窗,好讓策劃明顯的知道表格哪裡填錯數據,彈窗需要調用System.Windows.Forms庫。操作如下: 需要在 .csproj 文件中添加: <UseWindowsForms>true</UseWindowsForms> 須將目標平臺設置為 Wi ...
  • 從C#3開始,拓展方法這一特性就得到了廣泛的應用。 此功能允許你能夠使用實例方法的語法調用某個靜態方法,以下是一個獲取/創建文件的靜態方法: public static async Task<StorageFile> GetOrCreateFileAsync(this StorageFolder f ...
  • 在Windows 11下,使用WinUI2.6以上版本的ListView長這樣: 然而到了Win10上,儘管其他控制項的樣式沒有改變,但ListViewItem變成了預設樣式(初代Fluent) 最重大的問題是,Win10上的HorizontalAlignment未被設置成Stretch,可能造成嚴重 ...
  • 前言 周六在公司加班,幹完活後越顯無聊,想著下載RabbiitMQ做個小項目玩玩。然而這一下就下載了2個小時,真讓人頭痛。 簡單的講一下如何安裝吧,網上教程和踩坑文章還是很多的,我講我感覺有用的文章放在本文末尾。 安裝地址 erlang 下載 - Erlang/OTP https://www.erl ...