Java多線程 | 詳解ThreadLocal實現原理

一、ThreadLocal的簡介:

一般情況下,我們創建的變量都是可以給任何線程訪問並修改的,如果我們想讓線程擁有自己的私有本地變量,那我們就可以使用ThreadLocal類是實現這樣的想法。

ThreadLocal類主要解決的是讓每個線程綁定自己的值,可以將ThreadLocal類形象的比喻成存放數據的盒子,盒子中可以存儲每個線程的私有數據。

我們來看看一個示例代碼瞭解它的使用:

import java.text.SimpleDateFormat;
import java.util.Random;

/**
 * ThreadLocal示例類
 */
public class ThreadLocalExample {

    //SimpleDateFormat不是線程安全的,所以要爲每個線程都創建一個本地副本
    private static final ThreadLocal<SimpleDateFormat> formarter =
            ThreadLocal.withInitial(()->new SimpleDateFormat("yyyyMMdd HHmm"));

    public static void main(String [] args) throws InterruptedException {

        ThreadLocalExample example = new ThreadLocalExample();
        for (int i = 0;i < 10; ++i){
            //創建線程
            Thread t = new Thread(()->{
                System.out.println("Thread Name="+Thread.currentThread().getName()+" deafult Formatter=" +
                        formarter.get().toPattern());
                try {
                    Thread.sleep(new Random().nextInt(1000));
                }catch (Exception e){
                    e.printStackTrace();
                }

                formarter.set(new SimpleDateFormat());

                System.out.println("Thread Name="+Thread.currentThread().getName()+" formatter=" +
                        formarter.get().toPattern());
            },""+i);

            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }

    }

}


我們從結果中,可以看出,線程0使用了set方法來修改後,變量的值變了,但是對於其他線程在沒有調用set方法之前,get取到的值都是一開始主線程初始化的默認值。

二、ThreadLocal的實現原理

我們先來看看ThreadLocal類裏面的一個很重要的靜態內部類ThreadLocalMap

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 static final int INITIAL_CAPACITY = 16;
        private Entry[] table;
        private int size = 0;
        .......

這個ThreadLocalMap我們可以將它當作一個類似Map的數據結構,他的key爲ThreadLocal對象,值爲Object對象。

其實在Thread類中也有兩個ThreadLocalMap的對象,我們看看:

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;
...

默認情況下這兩個變量都是null,只有當前線程調用ThreadLocal類的set或get方法時才創建它們。

實際上的話,ThreadLocal存放的每一個線程的私有變量就是存在當前線程對象的threadLocals對象中的,當調用ThreadLocalset方法時,就是先通過Thread.currentThread()來獲取當前的線程t,然後再使用getMap(t)方法來獲取當前線程對應的ThreadLocalMap,然後對這個map進行操作的:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

我們來看看上面那個通過線程來獲取ThreadLocalMapgetMap()方法的實現:

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

這個方法就是直接獲取了線程對象裏面的threadLocals,這個在我們剛剛看Thread類的時候已經說到了這個對象了。

因此我們可以得出一個結論就是:最終的變量是放在了當前線程的ThreadLocalMap
中,並不是存在ThreadLocal上,ThreadLocal可以理解爲只是ThreadLocalMap的封裝,傳遞了變量值。ThrealLocal類中可以通過Thread.currentThread()獲取到當前線程對象後,直接通過getMap(Thread t)可以訪問到該線程的ThreadLocalMap對象。

比如我們在同⼀個線程中聲明瞭兩個ThreadLocal對象的話,會使⽤Thread內部都是使⽤僅有那個ThreadLocalMap存放數據的,ThreadLocalMap的key就是ThreadLocal對象,value就是ThreadLocal對象調⽤set⽅法設置的值。


三、關於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;
            }
        }

ThreadLocalMap中使用的key爲ThreadLocal的弱引用,而value是強引用。所以,如果ThreadLocal沒有被外部強引用的情況下,在垃圾回收的時候,key會被清理掉,而value不會被清理掉。這樣⼀來,ThreadLocalMap中就會出現key爲null的Entry。假如我們不做任何措施的話,value永遠無法被GC回收,這個時候就可能會產⽣內存泄露。ThreadLocalMap實現中已經考慮了這種情況,在調用set()、get()、remove()方法的時候,會清理掉key爲null的記錄。使用完ThreadLocal方法後最好手動調用remove()方法。

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