你真的熟悉ThreadLocal嗎?





ThreadLocal

ThreadLocal == 本地線程?額,好像是這麼一回事,如果你要這麼翻譯,我只能說沒毛病,誰讓你英語這麼好呢,但是在你要繼續問,這玩意是不是用來解決多線程問題的,我得打斷你,老鐵,他還真的不是叫做本地線程,在代碼體系裏,他是用來解決多線程共享變量的線程安全問題的,聽不懂?別急,咱們坐下來慢慢談。

介紹

概念

引用JDK官方原話:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

百度翻譯:

這個類提供線程局部變量。這些變量與普通的變量不同,因爲每個訪問一個變量的線程(通過其“get”或“set”方法)都有自己的、獨立初始化的變量副本。ThreadLocal實例通常是類中的私有靜態字段,希望將狀態與線程(例如,用戶ID或事務ID)相關聯。

大部人喜歡用圖文說話,通俗易懂,咱們不妨直接來個圖:

Thread2
Thread1
ThreadLocalMap2
ThreadLocalMap1

看懂上圖了嗎?人工翻譯:每個線程內部含有自己的副本(ThreadLocalMap),這樣線程間這個副本是隔離的

也就實現了所謂的local含義,這樣在多線程的情況下,就能確保數據的線程安全。

應用場景

  1. 多個線程下,可能存在線程安全問題的一些共享變量
  2. 多個線程下,需要公用的引用對象(多線程多實例 ,一個線程一個實例,線程間不共享實例)

對比

ThreadLocalSynchronized對比:

  1. Synchronized主要是通過線程等待,犧牲時間來解決多線程問題
  2. ThreadLocal主要是通過每個線程單獨一份存儲空間,犧牲空間來解決衝突,並且相比於SynchronizedThreadLocal具有線程隔離的效果,只有在線程內才能獲取到對應的值,線程外則不能訪問到想要的值

使用

首先看下下面的代碼,20個線程去做count,大家可以多次執行看看,可以對比下執行結果,看看是不是每次結果都是一樣的:

  private ExecutorService executorService =
      new ThreadPoolExecutor(20, 20, 0L, TimeUnit.SECONDS, new LinkedBlockingDeque<>());

  private static Integer num = 0;

  @Test
  public void test1() {
    for (int i = 0; i < 20; i++) {
      executorService.execute(() -> {
        System.out.println(Thread.currentThread() + ",start:" + num);
        for (int j = 0; j < 2; j++) {
          num++;
        }
        System.out.println(Thread.currentThread() + ",end:" + num);
      });
    }
    System.out.println(Thread.currentThread() + ",all:" + num);
  }

抽樣日誌,發現多次執行這段測試類,結果是不一樣的,測試代碼存在線程安全問題。

Thread[pool-1-thread-8,5,main],start:6
Thread[pool-1-thread-8,5,main],end:8
Thread[main,5,main],all:8
Thread[pool-1-thread-16,5,main],start:8
Thread[pool-1-thread-16,5,main],end:10
Thread[pool-1-thread-5,5,main],start:10
Thread[pool-1-thread-5,5,main],end:12

想必大家都知道發生線程安全問題的根本原因是目標資源存在競爭了,也知道很多解決線程安全問題的方法。既然這篇文章主要介紹ThreadLocal,那麼,我們就用來他來解決這個問題,而ThreadLocal解決線程問題的根本思路就是一個線程一個實例,線程間不共享實例。

下面這段代碼,是修改過後的邏輯:

  private ExecutorService executorService =
      new ThreadPoolExecutor(6, 6, 0L, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
   
  private static final ThreadLocal<Integer> threadLocalNum = new ThreadLocal<>();

  @Test
  public void test2() {
    for (int i = 0; i < 20; i++) {
      executorService.execute(() -> {
        threadLocalNum.set(0);
        System.out.println(Thread.currentThread() + ",start:" + threadLocalNum.get());
        for (int j = 0; j < 2; j++) {
          Integer temp = threadLocalNum.get();
          threadLocalNum.set(++temp);
        }
        System.out.println(Thread.currentThread() + ",end:" + threadLocalNum.get());
      });
    }
    System.out.println(Thread.currentThread() + ",all:" + threadLocalNum.get());
  }

多執行幾次,發現結果沒有發生變化,且主線程的值爲null,證明了不同的線程的內部副本獨立性。

Thread[main,5,main],all:null
Thread[pool-1-thread-5,5,main],start:0
Thread[pool-1-thread-5,5,main],end:2
Thread[pool-1-thread-9,5,main],start:0
Thread[pool-1-thread-9,5,main],end:2
Thread[pool-1-thread-1,5,main],start:0
Thread[pool-1-thread-1,5,main],end:2

分析

實現

類圖:
類圖

ThreadLocal類圖看,它本身是主要設計爲一個儲存類,對外提供get()set()方法,對內存在靜態內部類ThreadLocalMap,從ThreadLocalMap的設計來看,他纔是核心的數據結構封裝,內部核心實現是一個Entry數組,
對外部封裝了數據的基本操作方法。

首先我們來看下ThreadLocalget()set(),具體實現,以此來分析ThreadLocalThreadLocalMap的關係:

public class ThreadLocal<T> {    
    //...
	public T get() {
        // 當前線程
        Thread t = Thread.currentThread();
        // 從當前線程中取出ThreadLocalMap對象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 以當前的ThreadLocal對象爲key,從ThreadLocalMap中獲取對應的Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 從Entry取出其value
                T result = (T)e.value;
                return result;
            }
        }
        // 如果拿不到ThreadLocalMap或者Entry,則說明未初始化,這個方法負責初始化
        return setInitialValue();
    }

    private T setInitialValue() {
        Object var1 = this.initialValue();
        Thread var2 = Thread.currentThread();
        ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
        if (var3 != null) {
            // ThreadLocalMap已初始化的情況,直接將初始的var1篩入ThreadLocalMap中
            var3.set(this, var1);
        } else {
            // ThreadLocalMap未初始化的情況,new一個ThreadLocalMap賦值給Thread.threadLocals
            this.createMap(var2, var1);
        }

        return var1;
    }

    void createMap(Thread var1, T var2) {
        var1.threadLocals = new ThreadLocal.ThreadLocalMap(this, var2);
    }

	public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // ThreadLocalMap已初始化的情況,直接set
            map.set(this, value);
        else
            // ThreadLocalMap未初始化的情況,new一個ThreadLocalMap賦值給Thread.threadLocals
            createMap(t, value);
    }

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

看了上面的源碼,和博主的註釋,相信對於上面的內容,大家很好理解,get()set()的設計避免了ThreadLocalMapThread的直接暴露,這樣對於使用者,無需關心線程和具體存放的實現。

爲了研究ThreadLocalMap本身是如何維護數據結構的,下面我們接着看ThreadLocalMap的實現:

public class ThreadLocal<T> {
         //...
         private final int threadLocalHashCode = nextHashCode();
   
         // 斐波那契散列乘數,它的優點是通過它散列(hash)出來的結果分佈會比較均勻,可以很大程度上避免hash衝突
         private static final int HASH_INCREMENT = 0x61c88647;
    
         private static AtomicInteger nextHashCode = new AtomicInteger();
    
         private static int nextHashCode() {
            return nextHashCode.getAndAdd(HASH_INCREMENT);
         }
    
         static class ThreadLocalMap {
      		//...
            private Entry getEntry(ThreadLocal<?> key) {
                // 索引計算,key.threadLocalHashCode與(table.length - 1)位運算
                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);
            }
             
            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;
            }
             
            private void set(ThreadLocal<?> key, Object value) {
                // We don't use a fast path as with get() because it is at
                // least as common to use set() to create new entries as
                // it is to replace existing ones, in which case, a fast
                // path would fail more often than not.
                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)]) {
                    ThreadLocal<?> k = e.get();

                    if (k == key) {
                        e.value = value;
                        return;
                    }

                    if (k == null) {
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }

                tab[i] = new Entry(key, value);
                int sz = ++size;
                // 清除一些無用的值
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }

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

上述源碼中,可以清晰的見到,table的索引是如何計算出來,其中還包括對於Miss情況的處理,Miss的情況源碼中也有詳細的邏輯,感興趣的可以翻開源碼繼續深究,除了getset方法,ThreadLocalMap還提供了remove方法,在當前線程不用時,可以調用此方法刪除。

看完了源碼,大致清楚了ThreadLocal的實現機制,對於其中的一些細節和疑問,後續篇幅再給大家梳理下。

ThreadLocal的實現機制總結如下:

ThreadLocal本身不做數據結構的實現,只是封裝了靜態內部類ThreadLocalMapThread的調用邏輯,提供給使用者使用,ThreadLocalMap利用Entry[]數組實現了對象實例的存儲,其中索引的計算利用斐波那契散列乘數來較大程度的避免Hash衝突,ThreadLocalMap對外提供了Entry[]的數據存儲維護方法。

回收機制

在上述源碼中,可見ThreadLocalMap對象實例,實際上是存放在每個Thread中的,只是被定義在ThreadLocal中。既然是這樣的,那麼ThreadLocalMap的生命週期與Thread一樣長,在Thread被銷燬的時候,ThreadLocalMap也會隨之被回收。

這麼看來,好像ThreadLocalMap永遠也不會發生內存泄露的問題呀?如果這麼想,那離下次內存泄露事故不久了,咱們回頭在斟酌下生命週期:ThreadLocalMap的生命週期與Thread一樣長。這裏你會不會產生一個疑問,如果Thread放在線程池裏,ThreadLocalMap一直往裏面set值,從不主動調用remove,是不是就發生了內存泄露?話不多說,咱們模擬下,看看結果。

  static class LocalVariable {
    private Long[] a = new Long[4 * 1024 * 1024];
  }

  final static ThreadPoolExecutor poolExecutor =
      new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());

  final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();

  /**
   * jvm參數 -Xms100m -Xmx100m
   *
   * @throws InterruptedException
   */
  @Test
  public void test4() throws InterruptedException {
    for (int i = 0; i < 5000; ++i) {
      poolExecutor.execute(() -> {
        localVariable.set(new LocalVariable());
        System.out.println("use local varaible:" + localVariable.get());
//        localVariable.remove();
      });
      Thread.sleep(1000);
    }
    System.out.println("pool execute over");
  }

打印日誌:

use local varaible:thread.Test01$LocalVariable@759d4ea7
use local varaible:thread.Test01$LocalVariable@6b5a2f85
use local varaible:thread.Test01$LocalVariable@5171dc8b
use local varaible:thread.Test01$LocalVariable@4a7b8c4f
use local varaible:thread.Test01$LocalVariable@1cfaa313
Exception in thread "pool-1-thread-9" java.lang.OutOfMemoryError: Java heap space
use local varaible:thread.Test01$LocalVariable@492267bb
...

Jprofiler監控:

在這裏插入圖片描述

從日誌看,出現了我們喜歡的字樣OOM。從Jprofiler看,儘管GC活動很活躍,但是由於存在大量無法回收的堆內存,導致其一直處於緊張狀態。我們再把localVariable.remove();的註釋去除,跑一段時間試試。

打印日誌:

沒有發生OOM

Jprofiler監控:

在這裏插入圖片描述

Jprofiler看,堆內存使用正常,GC可以正常回收掉不在需要的內存。

好了,到此我們證明了,ThreadLocalMap使用不當,會發生內存泄露。

既然發生了OOM,我們不妨則dump出堆,看下是不是由於ThreadLocalMap使用不當導致,進行反向驗證,修改啓動參數:

-Xms100m -Xmx100m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dump.hprof

Dump文件使用JDK自帶的jvisualvm查看:

在這裏插入圖片描述

在這裏插入圖片描述

首先我們查出排名前20的最大對象,然後我們發現大小基本差不多,所以我們隨便選擇了一個 Thread,通過對一個實例的分析,最終查到ThreadLocalMap大小異常,幾乎佔滿了線程,通過對ThreadLocalMap內部結構分析,最終看到了LocalVariable這個大對象。

好,我們對以上實驗做下簡單的總結:

ThreadLocalMapThread的生命週期一樣長,所以不管線程是否重用,我們都應該,在使用完靜態變量後,都應該主動加remove掉不需要的內容,以確保不會發生內存泄露問題。

誤區

  1. ThreadLocalMapThreadLocal內?

    首先,這個問題本身有問題,應該拆分爲下面兩個問題:

    A、ThreadLocalMap定義在ThreadLocal內?

    B、運行時,ThreadLocalMap的實例對象是在ThreadLocal實例對象內,還是在Thread實例對象內?

    對於問題A,不言而喻,下方源碼證明了這一點,ThreadLocalMap確實是ThreadLocal的靜態內部類。

    在這裏插入圖片描述

    對於問題B,同樣咱們分析源碼,從源碼中getset中可見,ThreadLocalMap都是從Thread中獲取所得,由此可見證明了:運行時,ThreadLocalMap的實例對象是在Thread實例對象內

        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();
        }
    
    	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;
        }
    
  2. ThreadLocalMap的索引及table,能否有一個較好的總結?

  • 對於某一ThreadLocal來講,他的索引值i是確定的,在不同線程之間訪問時訪問的是不同的table數組的同一位置即都爲table[i],只不過這個不同線程之間的table是獨立的。

  • 對於同一線程的不同ThreadLocal來講,這些ThreadLocal實例共享一個table數組,然後每個ThreadLocal實例在table中的索引i是不同的。

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