還不理解ThreadLocal的看過來

ThreadLocal是什麼?

ThreadLocal是Java類庫提供的在多線程環境下保證對共享資源安全訪問的類

ThreadLocal與Thread、ThreadLocalMap是什麼關係?

通過對源碼分析發現,ThreadLocalMap是每一個線程Thread類的成員變量,裏面有一個鍵值對數據Entry[] table,可以認爲是一個map。
一個Thread對象持有一個ThreadLocalMap成員變量,而ThreadLocalMap依託Entry靜態類來存儲數據,Entry結構中key表示ThreadLocal,value表示要存儲的數據

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 有哪些常用方法?

initialValue
該方法會返回當前線程對應的初始值。

此方法默認實現返回null。

 protected T initialValue() {
        return null;
    }

可以使用匿名內部類的方式重寫initialValue(),以便在後續使用中可以初始化副本對象。

這是一個延遲加載的方法,只有在調動get方法的時候纔會出觸發。
當線程第一次使用get訪問變量時,將調用此方法。若線程先調用了set方法,則不會再調用initialValue方法。

請看源代碼,便一目瞭然爲什麼這麼說了

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
	 private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

當第一次調用get()時,map爲null,代碼流轉到setInitialValue方法,在setInitialValue方法中首先會去讀取initialValue()初始化的值。如果有重寫initialValue方法,則會走到我們重寫的方法裏

每個線程最多調用一次這個方法。但是如果已經調用了remove()後,再調用get()依然可以調用此方法。

public class ThreadLocalDemo {

//    private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
//        @Override
//        protected Integer initialValue() {
//            return 0;
//        }
//    };

    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> {
        System.out.println("我是InitialValue方法");
        return 0;
    });


    public static void main(String[] args) {

//        threadLocal.set(1);

        System.out.println(threadLocal.get());
        System.out.println(threadLocal.get());
        System.out.println("即將執行remove方法");
        threadLocal.remove();
        System.out.println(threadLocal.get());
    }
}

輸出結果:

我是InitialValue方法
0
0
即將執行remove方法
我是InitialValue方法
0

set(T t) 爲線程設置一個新值
T get() 得到這個線程對應的value
void remove() 刪除對應這個線程的值



ThreadLocal使用須知

1、在ThreadLocal使用之前,一定要使用initialValue初始化或set(T t)賦初值,否則可能會報空指針異常

public class ThreadLocalDemo {

    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();


    private static int getValue(){
        return threadLocal.get();
    }

    public static void main(String[] args) {
        System.out.println(ThreadLocalDemo.getValue());
    }
}

結論:ThreadLocal#initialValue默認實現返回null,而Integer->int需要拆箱,誘發空指針異常。若getValue()返回Integer,在上面的程序不會報錯,但在使用這個數據時依然可能報錯。

2、不要重複造輪子,優先使用框架提供出來的工具類。



ThreadLocal使用舉例

就以SimpleDateFormat爲例,看看ThreadLocal是怎麼幫助其實現線程安全的?

演示多線程下使用SimpleDateFormat格式化時間

public class ThreadNotSafeDemo{
private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    public static void main(String[] args) {
        BasicThreadFactory threadFactory = new BasicThreadFactory.Builder().namingPattern("wojiushiwo-pool-%d").build();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 15, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(20),
                threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());


        for (int i = 1; i <= 50; i++) {
            long num = i;
            executor.submit(() -> {
                String format = dateFormat.format(System.currentTimeMillis()+num*1000);
                System.out.println(Thread.currentThread().getName() + "當前時間:" + format);
            });
        }

        executor.shutdown();


    }
}       

上面的代碼演示了使用線程池執行50個解析時間的任務。由於每個任務中的時間都是System.currentTimeMillis()+num*1000不會重複,所以解析出來的時間也應該不重複纔對。
輸出結果:

wojiushiwo-pool-1當前時間:2020-07-04 02:08:19
wojiushiwo-pool-2當前時間:2020-07-04 02:08:20
wojiushiwo-pool-3當前時間:2020-07-04 02:08:21
wojiushiwo-pool-4當前時間:2020-07-04 02:08:22
wojiushiwo-pool-5當前時間:2020-07-04 02:08:23
wojiushiwo-pool-6當前時間:2020-07-04 02:08:24
wojiushiwo-pool-7當前時間:2020-07-04 02:08:25
wojiushiwo-pool-8當前時間:2020-07-04 02:08:26
wojiushiwo-pool-9當前時間:2020-07-04 02:08:27
wojiushiwo-pool-10當前時間:2020-07-04 02:08:28
wojiushiwo-pool-1當前時間:2020-07-04 02:08:29
wojiushiwo-pool-1當前時間:2020-07-04 02:08:30
wojiushiwo-pool-3當前時間:2020-07-04 02:08:31
wojiushiwo-pool-3當前時間:2020-07-04 02:08:32
wojiushiwo-pool-5當前時間:2020-07-04 02:08:33
wojiushiwo-pool-6當前時間:2020-07-04 02:08:35
wojiushiwo-pool-7當前時間:2020-07-04 02:08:35
wojiushiwo-pool-8當前時間:2020-07-04 02:08:36
wojiushiwo-pool-9當前時間:2020-07-04 02:08:37
wojiushiwo-pool-10當前時間:2020-07-04 02:08:39
wojiushiwo-pool-1當前時間:2020-07-04 02:08:39
wojiushiwo-pool-2當前時間:2020-07-04 02:08:40
wojiushiwo-pool-4當前時間:2020-07-04 02:08:41
wojiushiwo-pool-4當前時間:2020-07-04 02:08:42
wojiushiwo-pool-5當前時間:2020-07-04 02:08:43
wojiushiwo-pool-6當前時間:2020-07-04 02:08:44
wojiushiwo-pool-7當前時間:2020-07-04 02:08:45
wojiushiwo-pool-8當前時間:2020-07-04 02:08:46
wojiushiwo-pool-9當前時間:2020-07-04 02:08:47
wojiushiwo-pool-9當前時間:2020-07-04 02:08:48
wojiushiwo-pool-1當前時間:2020-07-04 02:08:50
wojiushiwo-pool-2當前時間:2020-07-04 02:08:50
wojiushiwo-pool-1當前時間:2020-07-04 02:08:52
wojiushiwo-pool-3當前時間:2020-07-04 02:08:52
wojiushiwo-pool-1當前時間:2020-07-04 02:08:53
wojiushiwo-pool-6當前時間:2020-07-04 02:08:55
wojiushiwo-pool-1當前時間:2020-07-04 02:08:55
wojiushiwo-pool-8當前時間:2020-07-04 02:08:56
wojiushiwo-pool-1當前時間:2020-07-04 02:08:57
wojiushiwo-pool-8當前時間:2020-07-04 02:08:58
wojiushiwo-pool-2當前時間:2020-07-04 02:08:59
wojiushiwo-pool-4當前時間:2020-07-04 02:09:00
wojiushiwo-pool-2當前時間:2020-07-04 02:09:01
wojiushiwo-pool-3當前時間:2020-07-04 02:09:02
wojiushiwo-pool-7當前時間:2020-07-04 02:09:03
wojiushiwo-pool-6當前時間:2020-07-04 02:09:04
wojiushiwo-pool-10當前時間:2020-07-04 02:09:05
wojiushiwo-pool-9當前時間:2020-07-04 02:09:07
wojiushiwo-pool-1當前時間:2020-07-04 02:09:07
wojiushiwo-pool-8當前時間:2020-07-04 02:09:08

發現結果中時間2020-07-04 02:08:50有重複現象,說明SimpleDateFormat在多線程環境下不是線程安全的。

令SimpleDateFormat線程安全的方式有多種,這裏主要討論ThreadLocal

下面以ThreadLocal來演示實現SimpleDateFormat線程安全的輸出時間

public class ThreadSafeDemo {


    private static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));

    public static void main(String[] args) {
        BasicThreadFactory threadFactory = new BasicThreadFactory.Builder().namingPattern("wojiushiwo-pool-%d").build();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 15, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(20),
                threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());


        for (int i = 1; i <= 50; i++) {
            long num = i;
            executor.submit(() -> {
                String format = threadLocal.get().format(System.currentTimeMillis()+num*1000);
                System.out.println(Thread.currentThread().getName() + "當前時間:" + format);
            });
        }

        executor.shutdown();


    }

}

輸出結果:

wojiushiwo-pool-15當前時間:2020-07-04 02:20:23
wojiushiwo-pool-15當前時間:2020-07-04 02:19:59
wojiushiwo-pool-15當前時間:2020-07-04 02:20:00
wojiushiwo-pool-15當前時間:2020-07-04 02:20:01
wojiushiwo-pool-15當前時間:2020-07-04 02:20:02
wojiushiwo-pool-15當前時間:2020-07-04 02:20:03
wojiushiwo-pool-15當前時間:2020-07-04 02:20:04
wojiushiwo-pool-15當前時間:2020-07-04 02:20:05
wojiushiwo-pool-15當前時間:2020-07-04 02:20:06
wojiushiwo-pool-15當前時間:2020-07-04 02:20:07
wojiushiwo-pool-15當前時間:2020-07-04 02:20:08
wojiushiwo-pool-15當前時間:2020-07-04 02:20:09
wojiushiwo-pool-15當前時間:2020-07-04 02:20:10
wojiushiwo-pool-14當前時間:2020-07-04 02:20:22
wojiushiwo-pool-1當前時間:2020-07-04 02:19:49
wojiushiwo-pool-15當前時間:2020-07-04 02:20:11
wojiushiwo-pool-14當前時間:2020-07-04 02:20:12
wojiushiwo-pool-1當前時間:2020-07-04 02:20:13
wojiushiwo-pool-15當前時間:2020-07-04 02:20:14
wojiushiwo-pool-14當前時間:2020-07-04 02:20:15
wojiushiwo-pool-1當前時間:2020-07-04 02:20:16
wojiushiwo-pool-15當前時間:2020-07-04 02:20:17
wojiushiwo-pool-14當前時間:2020-07-04 02:20:18
main當前時間:2020-07-04 02:20:24
wojiushiwo-pool-5當前時間:2020-07-04 02:19:53
wojiushiwo-pool-5當前時間:2020-07-04 02:20:25
wojiushiwo-pool-4當前時間:2020-07-04 02:19:52
wojiushiwo-pool-15當前時間:2020-07-04 02:20:26
wojiushiwo-pool-14當前時間:2020-07-04 02:20:27
wojiushiwo-pool-5當前時間:2020-07-04 02:20:28
wojiushiwo-pool-4當前時間:2020-07-04 02:20:29
wojiushiwo-pool-1當前時間:2020-07-04 02:20:30
wojiushiwo-pool-15當前時間:2020-07-04 02:20:31
wojiushiwo-pool-14當前時間:2020-07-04 02:20:32
wojiushiwo-pool-5當前時間:2020-07-04 02:20:33
wojiushiwo-pool-4當前時間:2020-07-04 02:20:34
wojiushiwo-pool-1當前時間:2020-07-04 02:20:35
wojiushiwo-pool-7當前時間:2020-07-04 02:19:55
wojiushiwo-pool-7當前時間:2020-07-04 02:20:36
wojiushiwo-pool-14當前時間:2020-07-04 02:20:37
wojiushiwo-pool-5當前時間:2020-07-04 02:20:38
wojiushiwo-pool-10當前時間:2020-07-04 02:19:58
wojiushiwo-pool-11當前時間:2020-07-04 02:20:19
wojiushiwo-pool-6當前時間:2020-07-04 02:19:54
wojiushiwo-pool-3當前時間:2020-07-04 02:19:51
wojiushiwo-pool-12當前時間:2020-07-04 02:20:20
wojiushiwo-pool-8當前時間:2020-07-04 02:19:56
wojiushiwo-pool-13當前時間:2020-07-04 02:20:21
wojiushiwo-pool-2當前時間:2020-07-04 02:19:50
wojiushiwo-pool-9當前時間:2020-07-04 02:19:57

ThreadLocal爲什麼會內存泄露?

前面有討論過ThreadLocalMap中靜態Entry類,這裏詳細說下

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

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

首先Entry繼承自弱引用,並且其構造函數中k是使用WeakReference賦值的。所以可以斷定Entry中key是弱引用。而value毫無疑問是強引用。

由JVM知識可以得知,弱引用在垃圾回收時會被主動回收,而強引用只有當GC觸發時纔會被回收。

正常情況下,當線程終止時,保存在ThreadLocal裏的value會被垃圾回收。但是,如果線程不終止(如線程需要保持很久),那麼key對應的value就不會被回收,就會導致內存無法被回收,最終可能出現OOM。
幸好,ThreadLocal中set、remove、rehash方法中會掃描key爲null的Entry,並將對應的value也設置爲null,這樣value就可以被回收了

若像上面說的,ThreadLocal不再使用,但線程未終止而且會顯式調用set、remove、rehash等方法,那麼內存中的調用鏈就一直存在,極易引起內存泄露。

ThreadLocal如何避免內存泄露?

在使用完ThreadLocal之後手動調用remove方法,刪除對應的Entry對象。




以上,若存在表述不明確或表述錯誤的地方,請指正,謝謝!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章