ThreadLocal使用場景與原理

目錄

ThreadLocal的使用場景

ThreadLocal與synchronized的區別

Thread、ThreadLocal及ThreadLocalMap的關係

調用remove()方法避免內存泄漏


ThreadLocal的使用場景

  • ThreadLocal 用作保存每個線程獨享的對象,爲每個線程都創建一個副本,這樣每個線程都可以修改自己所擁有的副本, 而不會影響其他線程的副本,確保了線程安全。
  • ThreadLocal 用作每個線程內需要獨立保存信息,以便供其他方法更方便地獲取該信息的場景。每個線程獲取到的信息可能都是不一樣的,前面執行的方法保存了信息後,後續方法可以通過 ThreadLocal 直接獲取到,避免了傳參,類似於全局變量的概念。

通過例子驗證一下:

我們知道SimpleDateFormat在多線程併發訪問下會出現線程安全問題。

/**
 * 線程不安全demo
 *
 * @author hujy
 * @version 1.0
 * @date 2020-06-29 20:57
 */
public class ThreadNotSafeDemo {

    private static ExecutorService threadPool = Executors.newFixedThreadPool(16);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");

    public String date(int seconds) {
        // 創建不同的date
        Date date = new Date(1000 * seconds);
        return dateFormat.format(date);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(() -> {
                String date = new ThreadNotSafeDemo().date(finalI);
                System.out.println(date);
            });
        }
        threadPool.shutdown();
    }
}

打印運行結果: 

上面代碼中每次循環都會創建不同的date對象,但是在多線程併發創建的場景下,打印的結果中出現了大量重複值,說明產生了線程安全問題。

通過ThreadLocal保證線程安全:

/**
 * ThreadLocal線程安全demo
 *
 * @author hujy
 * @version 1.0
 * @date 2020-06-29 21:19
 */
public class ThreadSafeDemo {

    private static ExecutorService threadPool = Executors.newFixedThreadPool(16);

    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return dateFormat.format(date);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(() -> {
                try {
                    String date = new ThreadSafeDemo().date(finalI);
                    System.out.println(date);
                } finally {
                    ThreadSafeFormatter.dateFormatThreadLocal.remove();
                }
            });
        }
        threadPool.shutdown();
    }
}

class ThreadSafeFormatter {
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = 
            ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
}

打印運行結果:

運行結果都是唯一的值,說明通過ThreadLocal可以實現共享變量的線程安全。


ThreadLocal與synchronized的區別

  • ThreadLocal 是通過讓每個線程獨享自己的副本,避免了資源的競爭。
  • synchronized 主要用於臨界資源的分配,在同一時刻限制最多隻有一個線程能訪問該資源。
  • ThreadLocal 並不是用來解決共享資源的多線程訪問的問題,因爲每個線程中的資源只是副本,並不共享。因此ThreadLocal適合作爲線程上下文變量,簡化線程內傳參。

Thread、ThreadLocal及ThreadLocalMap的關係

想要了解Threadlocal的工作原理,就必須瞭解Thread、ThreadLocal以及ThreadLocalMap這三個類之間的關係。

ThreadLocalMap是ThreadLocal類的靜態內部類,本質是一個Map,key的類型就是我們定義的ThreadLocal對象,value則是我們具體要保存的變量參數。

 public class ThreadLocal<T> {
    ...
    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        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;
        }
    }

而Thread中含有ThreadLocal.ThreadLocalMap類型的成員變量threadLocals。

public class Thread implements Runnable {    
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;

    ...
}

因此這三個類的關係可以總結爲:

一個 Thread 裏面只有一個ThreadLocalMap ,而在一個 ThreadLocalMap 裏面卻可以有很多的 ThreadLocal,每一個 ThreadLocal 都對應一個 value。因爲一個 Thread 是可以調用多個 ThreadLocal 的,所以 Thread 內部就採用了 ThreadLocalMap 這樣 Map 的數據結構來存放 ThreadLocal 和 value。

另外ThreadLocalMap在解決hash衝突的方式與HashMap不同,HashMap採用的是拉鍊發,而ThreadLocalMap採用的是線性探索法,即發生衝突時,向下繼續尋找空的位置。


調用remove()方法避免內存泄漏

通過ThreadLocalMap的源碼可以看到,Entry中的key被定義爲弱引用類型,當發生GC時,key會被直接回收,無需手動清理。

而value屬於強引用類型,被當前的Thread對象關聯,所以說value的回收取決於Thread對象的生命週期。如果說一個線程執行完畢,線程Thread隨之被釋放,那麼value便不存在內存泄漏的問題。然而,我們一般會通過線程池的方式來複用Thread對象來節省資源,這就會導致一個Thread對象的生命週期會非常長,隨着任務的執行,value就有可能越來越多且無法釋放,最終導致內存泄漏。

因此,我們在使用完ThreadLocal變量後,要手動調用remove()方法來清理ThreadLocalMap(一般在finally代碼塊中)。

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章