ThreadLocal詳解

ThreadLocal是一個關于創建線程局部變量的類。

通常情況下,我們創建的變量是可以被任何一個線程訪問并修改的。而使用ThreadLocal創建的變量只能被當前線程訪問,其他線程則無法訪問和修改。

ThreadLocal是如何為每個線程創建變量的副本的:
  首先,在每個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,鍵值為當前ThreadLocal變量,value為變量副本(即T類型的變量)。
  初始時,在Thread里面,threadLocals為空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,并且以當前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals。


以下代碼展示了如何創建一個ThreadLocal變量:

    private ThreadLocal<String> myThreadLocal = new ThreadLocal<>();
    @Test
    public void testx() {
        Thread t = new Thread() {
            @Override
            public void run() {
                myThreadLocal.set("icecrea");
                System.out.println(myThreadLocal.get());
            }
        };
        t.start();
        Thread t2 = new Thread() {
            @Override
            public void run() {
                System.out.println("t2------"+myThreadLocal.get());
            }
        };
        t2.start();
    }

我們可以看到,通過這段代碼實例化了一個ThreadLocal對象。我們只需要實例化對象一次,并且也不需要知道它是被哪個線程實例化。雖然所有的線程都能訪問到這個ThreadLocal實例,但是每個線程卻只能訪問到自己通過調用ThreadLocal的set()方法設置的值。即使是兩個不同的線程在同一個ThreadLocal對象上設置了不同的值,他們仍然無法訪問到對方的值。
當然,我們也可以復習方法設置初始值,這樣上面的t2線程就會打印出初始值。

    private ThreadLocal<String> myThreadLocal = new ThreadLocal<String>(){
        @Override
        public String initialValue(){
            return "This is the initial value";
        }
    };

源碼解讀:
ThreadLocal的set方法,分為下面三步:

  • 首先獲取當前線程
  • 利用當前線程作為句柄獲取一個ThreadLocalMap的對象
  • 如果上述ThreadLocalMap對象不為空,則設置值,否則創建這個ThreadLocalMap對象并設置值

注意: 這里set方法里,第二行獲取的是當前線程里的threadlocals這個變量副本,第四行傳入了this,即threadlocal對象作為key,之后注入到線程的變量副本里。通過ThreadLocal.set()將這個新創建的對象的引用保存到各線程的自己的一個map中,每個線程都有這樣一個map,執行ThreadLocal.get()時,各線程從自己的map中取出放進去的對象,因此取出來的是各自自己線程中的對象,ThreadLocal實例是作為map的key來使用的。

為什么threadLocals的類型ThreadLocalMap的鍵值為ThreadLocal對象?因為每個線程中可有多個threadLocal變量。

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

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

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

        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
  ...
}

同理,ThreadLocal的get方法,
獲取當前線程,獲取線程持有的ThreadLocalMap,獲取值

    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();
    }

在Thread類中,持有ThreadLocal.ThreadLocalMap的引用變量。實際上ThreadLocal的值是放入了當前線程的一個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

是不是說ThreadLocal的值只能被一個線程訪問呢?
使用InheritableThreadLocal可以實現多個線程訪問ThreadLocal的值。
原因是Thread類的Init方法(此處只列相關代碼),

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

可以看出,使用InheritableThreadLocal可以將某個線程的ThreadLocal值在其子線程創建時傳遞過去。

    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

     private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

下面代碼子線程可以訪問到父線程中InheritableThreadLocal的值。打印icecrea。(此處如果用threadLocal實例返回的則是Null)

    @Test
    public void testInheritableThreadLocal() {
        final ThreadLocal threadLocal = new InheritableThreadLocal();
        threadLocal.set("icecrea");
        Thread t = new Thread() {
            @Override
            public void run() {
                super.run();
                System.out.println(threadLocal.get());
            }
        };

        t.start();
    }

ThreadLocalMap


ThreadLocalMap有靜態內部類Entry,是ThreadLocal的弱引用類型,持有Object類型的引用。持有Entry[]數組。

 /**
         * 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;

構造方法如下。通過ThreaLocal和Object值來構造ThreadLocalMap,再回顧上面的ThreadLocal的get方法,就是通過獲取ThreadLocalMap,在調用它的getEntry方法,計算HASH值,定位Entry在table數組中的位置返回,獲取value的值。

   ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }


ThreadLocal會內存泄露么

內存泄漏的定義:對象已經沒有被應用程序使用,但是垃圾回收器沒辦法移除它們,因為還在被引用著。

threadlocal里面使用了一個存在弱引用的map,當釋放掉threadlocal的強引用以后,map里面的value卻沒有被回收.而這塊value永遠不會被訪問到了. 所以存在著內存泄露. 最好的做法是將調用threadlocal的remove方法.



  每個thread中都存在一個map, map的類型是ThreadLocal.ThreadLocalMap. Map中的key為一個threadlocal實例. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每個key都弱引用指向threadlocal. 當把threadlocal實例置為null以后,沒有任何強引用指向threadlocal實例,所以threadlocal將會被gc回收. 但是,我們的value卻不能回收,因為存在一條從current thread連接過來的強引用. 只有當前thread結束以后, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將全部被GC回收.
  所以得出一個結論就是只要這個線程對象被gc回收,就不會出現內存泄露,但在threadLocal設為null和線程結束這段時間不會被回收的,就發生了我們認為的內存泄露。最要命的是線程對象不被回收的情況,這就發生了真正意義上的內存泄露。比如使用線程池的時候,線程結束是不會銷毀的,會再次使用的。就可能出現內存泄露。

Java為了最小化減少內存泄露的可能性和影響,在ThreadLocal的get,set的時候都會清除線程Map里所有key為null的value。所以最怕的情況就是,threadLocal對象設null了,開始發生“內存泄露”,然后使用線程池,這個線程結束,線程放回線程池中不銷毀,這個線程一直不被使用,或者分配使用了又不再調用get,set方法,那么這個期間就會發生真正的內存泄露。

對于單獨的java文件,要如下設置(參數不能放在Test后面)
java -Xms64m -Xmx256m Test


Linux tomcat下:
在/usr/local/apache-tomcat-5.5.23/bin目錄下的catalina.sh添加:JAVA_OPTS='-Xms512m -Xmx1024m'要加“m”說明是MB,否則就是KB了,在啟動tomcat時會報內存不足。

初始堆大小-Xms64m
最大堆大小 -Xmx256m

不知道springboot下如何設置tomcat?試了下面這個和類似的都沒成功
mvn spring-boot:run -DXms=64m -DXmx=256m
我的解決方案是:mvn package, 然后java -jar -Xms64m -Xmx256m xxx.war 這樣設置成功
測試代碼如下:

    private ThreadLocal<List<String>> buffer=new ThreadLocal<>();
    @RequestMapping("threadLocal")
    @ResponseBody
    public String threadLocal(){
        List<String> list= Lists.newArrayList();
        for(int i=0;i<1024000;i++){
            list.add(String.valueOf(i));
        }
        buffer.set(list);
        return "success";
    }

報錯信息如下

Exception in thread "http-nio-8080-exec-4" java.lang.OutOfMemoryError: GC overhead limit exceeded
        at javax.management.ObjectName.quote(ObjectName.java:1832)
        at org.apache.coyote.AbstractProtocol.getName(AbstractProtocol.java:385)
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.register(AbstractProtocol.java:1087)
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:857)
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1459)
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:748)

觀察發現,value大內存并沒有回收



解決方案:添加 buffer.remove();方法手動回收Entry,解決了value無法回收的問題。

  /**
         * Remove the entry for key.
         */
        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

應用場景

解決 數據庫連接、Session管理問題
數據庫連接如果用常規方式,多線程訪問要加鎖,要互相等待,降低效率。可以使用threadlocal,線程不用相互等待,且他們之間沒有關聯。但增加了內存開銷。

private static ThreadLocal<Connection> connectionHolder= new ThreadLocal<Connection>() {
  public Connection initialValue() {
      return DriverManager.getConnection(DB_URL);
  }
};
 
public static Connection getConnection() {
  return connectionHolder.get();
}
private static final ThreadLocal threadSession = new ThreadLocal();
 
public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if (s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}

參考文章:
https://www.cnblogs.com/dolphin0520/p/3920407.html
https://www.cnblogs.com/onlywujun/p/3524675.html

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容