首頁 > 軟體

JDK原始碼白話解讀之ThreadLocal篇

2022-02-23 19:02:14

引言

因此本文主要結合常見的一些疑問、ThreadLocal原始碼、應用範例以注意事項來全面而深入地再詳細講解一遍ThreadLocal。希望大家看完本文後可以徹底掌握ThreadLocal

ThreadLocal是什麼?它能幹什麼?

在闡述ThreadLocal之前,我們先來看下它的設計者是怎麼描述ThreadLocal的吧。

看完官方的描述後,結合自己的理解,ThreadLocal提供了一種對應獨立執行緒內的資料存取機制,實現了變數線上程之間隔離,線上程生命週期內獨立獲取或者設定的能力。如果我們想線上程內傳遞引數但是有不想作為方法引數的時候,ThreadLocal就可以排上用場了。不過值得注意的是ThreadLocal並不會解決變數共用問題。實際上從ThreadLocal的名稱上面來看,執行緒本地變數也已經大致說明了它的作用,所以變數的命名還是非常重要的,要做到顧名思義。如果覺得還不是很理解,沒關係,我們可以通過以下的場景再加深下理解。

假如有以下的場景,假設只有一個資料庫連線,使用者端1、2、3都需要獲取資料庫連線來進行具體的資料庫操作,但是同一時間點只能有一個執行緒獲取連線,其他執行緒只能等待。因此就會出現資料庫存取效率不高的問題。

那我們有沒有什麼辦法能夠避免執行緒等待的情況呢?上述問題的根本原因是資料庫連線是共用變數,同事只能有一個執行緒可以進行操作。那如果三個執行緒都有自己的資料庫連線,互相隔離,那不就不會出現等待的問題了嘛。那麼此時我麼可以使用ThreadLocal實現在不同執行緒中的變數隔離。可以看出來,ThreadLocal是一種已空間換取時間的做法。

ThreadLocal實現執行緒隔離的祕密

從上文中,我們瞭解到ThreadLocal可以實現變數存取的執行緒級別的隔離。那麼它是到底如何實現的呢?這還需要結合Thread以及ThreadLocal的原始碼來分析才能揭開ThreadLocal實現執行緒隔離的神祕面紗。

public class Thread implements Runnable {
    ...
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ...
    
}

Thread原始碼中我們發現,它有一個threadLocals變數,它的型別是ThreadLocal中的內部類ThreadLocalMap。我們在看下ThreadLocalMap的定義是怎樣的。從原始碼中我們可以看出來,ThreadLocalMap實際上就是Entry陣列,這個Entry對應的key實際就是ThreadLocal的範例,value就是實際的變數值。

public class ThreadLocal<T> {
  ...
    
   static class ThreadLocalMap {
     
      static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
       ...
       //底層資料結構是陣列
       private Entry[] table;
       ...
     
   }
  ...
  
}

通過檢視上述的原始碼,如果還不太好理解的話,我們再結合下現實中的例子來理解。大家都有支付寶賬戶,我們通過它來管理著我們的銀行卡、餘額、花唄這些金融服務。

我們以支付寶以及支付寶賬戶進行類比,假設ThreadLocal就是支付寶,每個支付寶賬戶實際就是單獨的執行緒,而賬戶中的餘額屬性就相當於Thread的私有屬性ThreadLocalMap。我們在日常生活中,進行賬戶餘額的充值或者消費,並不是直接通過賬戶進行操作的,而是藉助於支付寶進行維護的。這就相當於每個執行緒對ThreadLocalMap進行操作的時候也不是直接操作的,而是藉助於ThreadLocal來操作。

那麼Thread到底是怎麼藉助ThreadLocal進行私有屬性管理的呢?還是需要進一步檢視Thread進行set以及get操作的原始碼。從以下的ThreadLocal的原始碼中我們可以看出,在進行操作之前,需要獲取當前的執行操作的執行緒,再根據執行緒或者執行緒中私有的ThreadLocalMap屬性來進行操作。

在進行資料獲取的時候,也是按照同樣的流程,先獲取當前的執行緒,再獲取執行緒中對應的ThreadLocalMap屬性來進行後續的值的獲取。

經過上述的原始碼的分析,我們可以得出這樣的結論,ThreadLocal之所以可以實現變數的執行緒隔離存取,實際上就是藉助於Thread中的ThreadLocalMap屬性來進行操作。由於都是操作執行緒本身的屬性,因此並不會影響其他執行緒中的變數值,因此可以實現執行緒級別的資料修改隔離。

為什麼ThreadLocal會出現OOM的問題?

記憶體漏失演示

我們都知道,ThreadLocal如果使用不當的話會出現記憶體漏失的問題,那麼我們就通過下面的這段程式碼來分析下,記憶體漏失的原因到底是什麼。

/**
 * @author mufeng
 * @description 測試ThreadLocal記憶體溢位
 * @date 2022/1/16 19:01
 * @since
 */
public class ThreadLocalOOM {

    /**
     * 測試執行緒池
     */
    private static Executor threadPool = new ThreadPoolExecutor(3, 3, 40,
            TimeUnit.SECONDS, new LinkedBlockingDeque<>());


    static class Info {
        private byte[] info = new byte[10 * 1024 * 1024];
    }

    private  static ThreadLocal<Info> infoThreadLocal = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            threadPool.execute(() -> {
                infoThreadLocal.set(new Info());
                System.out.println("Thread started:" + Thread.currentThread().getName());
            });
            Thread.sleep(100);
        }

    }
}

手動進行GC之後,我們可以發現堆中仍然有超過30M的堆記憶體佔用,如上面的程式碼,線上程池中活躍的執行緒會有三個,對應的value為10M,說明線上程還存活的情況下,對應的value並沒有被回收,因此存在記憶體漏失的情況,如果存在大量執行緒的情況,就會出現OOM

當我們修改程式碼線上程中進行remove操作,手動GC之後我們發現堆記憶體趨近於0了,之前沒有被回收的物件已經被回收了。

記憶體漏失問題分析

以上是對於ThreadLocal發生記憶體漏失問題的演示,那麼再來仔細分析下背後的原因是什麼。ThreadLocal中實際儲存資料的是ThreadLocalMap,實際上Map對應的key是一個虛參照,在GC的時候可以被回收掉,但是問題就在於key所對應的value,它是強參照,只要執行緒存活,那麼這條參照鏈就會一致存在,如果出現大量執行緒的時候就會有OOM的風險。 所以在使用ThreadLocal的時候一定記得要顯式的呼叫remove方法進行清理,防止記憶體漏失。

父子執行緒的引數傳遞

到這裡,我相信大家對於ThreadLocal的原理有了比較深入的理解了。結合上文中的ThreadLocal程式碼,不知道大家有沒有思考過一個問題,我們在使用ThreadLocal的時候都是在同一個執行緒內進行了set以及get操作,那麼如果set操作與get操作在父子執行緒中是否還可以正常的獲取呢?帶著這樣的疑問,我們來看下如下的程式碼。

/**
 * @author mufeng
 * @description 父子執行緒引數傳遞
 * @date 2022/1/16 9:54
 * @since
 */
public class InheritableThreadLocalMain {

    private static final ThreadLocal<String> count = new ThreadLocal<>();

    public static void main(String[] args) {

        count.set("父子執行緒引數傳遞!!!");
        System.out.println(Thread.currentThread().getName() + ":" + count.get());

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ":" + count.get());
        }).start();

    }

}

與之前程式碼有所不同,ThreadLocal的設值是在main執行緒中進行的,但是獲取操作實際是在主執行緒下的子執行緒中進行的,大家可以分析一下執行結果是怎麼樣的。

看到這個執行結果,不知道大家分析的對不對呢。實際上如果理解了上文的核心的話,這個問題應該很好分析的。ThreadLocal獲取資料的時候,首先是需要獲取當前的執行緒的,根據執行緒獲取實際儲存資料的ThreadLocalMap,上文程式碼中設定和獲取在父子執行緒中進行,那肯定是獲取不到設定的資料的。但是在現實的專案開發中,我們會經常遇到需要將父執行緒的變數值傳遞給子執行緒進行處理,那麼應該要怎麼來實現呢?這個時候InheritableThreadLocal就派上用場了。

/**
 * @author mufeng
 * @description 父子執行緒引數傳遞
 * @date 2022/1/16 9:54
 * @since
 */
public class InheritableThreadLocalMain {

    private static final ThreadLocal<String> count = new InheritableThreadLocal<>();

    public static void main(String[] args) {

        count.set("父子執行緒引數傳遞!!!");
        System.out.println(Thread.currentThread().getName() + ":" + count.get());

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ":" + count.get());
        }).start();

    }

}

那麼InheritableThreadLocal到底是如何實現父子執行緒的引數傳遞的呢?我麼還是的看看原始碼中的實現原理。實際上在Thread原始碼中,除了有Threadlocal私有屬性還有InheritableThreadLocal私有屬性。

public class Thread implements Runnable {
    
     /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
...
    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }
    
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        ...
        //關鍵
         if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); 
        ...    
        
    }
    ...
    
}

實際在進行子執行緒建立的時候,線上程初始化過程中,判斷了父執行緒中的inheritableThreadLocals屬性是否為空,如果不為空的話需要進行值的複製,這樣便實現了父子執行緒的值傳遞。

總結

本文主要對ThreadLocal進行了相對全面的分析,從它的使用場景、原理以及原始碼分析、產生OOM的原因以及一些使用上的注意,相信通過本文的學習,大家對於ThreadLocal會有更加深刻的理解。

到此這篇關於JDK原始碼白話解讀之ThreadLocal篇的文章就介紹到這了,更多相關Java ThreadLocal內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com