面試再問ThreadLocal,別說你不會

ThreadLocal是什么

以前面試的時候問到ThreadLocal總是一臉懵逼,只知道有這個哥們,不了解他是用來做什么的,更不清楚他的原理了。表面上看他是和多線程,線程同步有關的一個工具類,但其實他與線程同步機制無關。線程同步機制是多個線程共享同一個變量,而ThreadLocal是為每個線程創建一個單獨的變量副本,每個線程都可以改變自己的變量副本而不影響其它線程所對應的副本。

官方API上是這樣介紹的:該類提供了線程局部(thread-local)變量。這些變量不同于它們的普通對應物,因為訪問某個變量(通過其 get 或 set 方法)的每個線程都有自己的局部變量,它獨立于變量的初始化副本。

ThreadLocal實例通常是類中的 private static 字段,它們希望將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。


ThreadLocal的API

ThreadLocal定義了四個方法:

  • get():返回此線程局部變量當前副本中的值
  • set(T value):將線程局部變量當前副本中的值設置為指定值
  • initialValue():返回此線程局部變量當前副本中的初始值
  • remove():移除此線程局部變量當前副本中的值
  • ThreadLocal還有一個特別重要的靜態內部類ThreadLocalMap,該類才是實現線程隔離機制的關鍵。get()、set()、remove()都是基于該內部類進行操作,ThreadLocalMap用鍵值對方式存儲每個線程變量的副本,key為當前的ThreadLocal對象,value為對應線程的變量副本。
    試想,每個線程都有自己的ThreadLocal對象,也就是都有自己的ThreadLocalMap,對自己的ThreadLocalMap操作,當然是互不影響的了,這就不存在線程安全問題了,所以ThreadLocal是以空間來交換安全性的解決思路。

使用實例

假設每個線程都需要一個計數值記錄自己做某件事做了多少次,各線程運行時都需要改變自己的計數值而且相互不影響,那么ThreadLocal就是很好的選擇,這里ThreadLocal里保存的當前線程的局部變量的副本就是這個計數值。

public class SeqCount {

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


    public int nextSeq() {
        seqCount.set(seqCount.get() +1);
        return seqCount.get();
    }

    public static void main(String [] args) {
        SeqCount seqCount = new SeqCount();

        SeqThread seqThread1 = new SeqThread(seqCount);
        SeqThread seqThread2 = new SeqThread(seqCount);
        SeqThread seqThread3 = new SeqThread(seqCount);
        SeqThread seqThread4 = new SeqThread(seqCount);

        seqThread1.start();
        seqThread2.start();
        seqThread3.start();
        seqThread4.start();
    }

    public static class SeqThread extends Thread {

        private SeqCount seqCount;

        public SeqThread(SeqCount seqCount) {
            this.seqCount = seqCount;
        }

        @Override
        public void run() {
            for (int i=0; i<3; i++) {
                System.out.println(Thread.currentThread().getName()+" seqCount:"+seqCount.nextSeq());
            }
        }
    }
 }

運行結果:


解決SimpleDateFormat的線程安全

我們知道SimpleDateFormat在多線程下是存在線程安全問題的,那么將SimpleDateFormat作為每個線程的局部變量的副本就是每個線程都擁有自己的SimpleDateFormat,就不存在線程安全問題了。

public class SimpleDateFormatDemo {

    private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";

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

    /**
     * 獲取線程的變量副本,如果不覆蓋initialValue方法,第一次get將返回null,故需要創建一個DateFormat,放入threadLocal中
     * @return
     */
    public DateFormat getDateFormat() {
        DateFormat df = threadLocal.get();
        if (df == null) {
            df = new SimpleDateFormat(DATE_FORMAT);
            threadLocal.set(df);
        }
        return df;
    }

    public static void main(String [] args) {
        SimpleDateFormatDemo formatDemo = new SimpleDateFormatDemo();

        MyRunnable myRunnable1 = new MyRunnable(formatDemo);
        MyRunnable myRunnable2 = new MyRunnable(formatDemo);
        MyRunnable myRunnable3 = new MyRunnable(formatDemo);

        Thread thread1= new Thread(myRunnable1);
        Thread thread2= new Thread(myRunnable2);
        Thread thread3= new Thread(myRunnable3);
        thread1.start();
        thread2.start();
        thread3.start();
    }


    public static class MyRunnable implements Runnable {

        private SimpleDateFormatDemo dateFormatDemo;

        public MyRunnable(SimpleDateFormatDemo dateFormatDemo) {
            this.dateFormatDemo = dateFormatDemo;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+" 當前時間:"+dateFormatDemo.getDateFormat().format(new Date()));
        }
    }
}

運行結果:


源碼分析

ThreadLocalMap

ThreadLocalMap內部是利用Entry來進行key-value的存儲的。

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

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

上面源碼中key就是ThreadLocal,value就是值,Entry繼承WeakReference,所以Entry對應key的引用(ThreadLocal實例)是一個弱引用。

set(ThreadLocal key, Object value)

/**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            //根據ThreadLocal的散列值,查找對應元素在數組中的位置
            int i = key.threadLocalHashCode & (len-1);
            //采用線性探測法尋找合適位置
            for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                //key存在,直接覆蓋
                if (k == key) {
                    e.value = value;
                    return;
                }
                // key == null,但是存在值(因為此處的e != null),說明之前的ThreadLocal對象已經被回收了
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //ThreadLocal對應的key實例不存在,new一個
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //清楚陳舊的Entry(key == null的)
            // 如果沒有清理陳舊的 Entry 并且數組中的元素大于了閾值,則進行 rehash
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

這個set操作和集合Map解決散列沖突的方法不同,集合Map采用的是鏈地址法,這里采用的是開放定址法(線性探測)。set()方法中的replaceStaleEntry()和cleanSomeSlots(),這兩個方法可以清除掉key ==null的實例,防止內存泄漏。

getEntry()

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

由于采用了開放定址法,當前keu的散列值和元素在數組中的索引并不是一一對應的,首先取一個猜測數(key的散列值),如果所對應的key是我們要找的元素,那么直接返回,否則調用getEntryAfterMiss

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

這里一直在探測尋找下一個元素,知道找的元素的key是我們要找的。這里當key==null時,調用expungeStaleEntry有利于GC的回收,用于防止內存泄漏。


ThreadLocal為什么會內存泄漏

ThreadLocalMap的key為ThreadLocal實例,他是一個弱引用,我們知道弱引用有利于GC的回收,當key == null時,GC就會回收這部分空間,但value不一定能被回收,因為他和Current Thread之間還存在一個強引用的關系。由于這個強引用的關系,會導致value無法回收,如果線程對象不消除這個強引用的關系,就可能會出現OOM。有些時候,我們調用ThreadLocalMap的remove()方法進行顯式處理。


總結

ThreadLocal不是用來解決共享變量的問題,也不是協調線程同步,他是為了方便各線程管理自己的狀態而引用的一個機制。

每個ThreadLocal內部都有一個ThreadLocalMap,他保存的key是ThreadLocal的實例,他的值是當前線程的局部變量的副本的值。


在此我向大家推薦一個架構學習交流群。交流學習群號:833145934 里面資深架構師會分享一些整理好的錄制視頻錄像和BATJ面試題:有Spring,MyBatis,Netty源碼分析,高并發、高性能、分布式、微服務架構的原理,JVM性能優化、分布式架構等這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多。

文章出處:https://juejin.im/post/5d427f306fb9a06b122f1b94

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容