ThreadLocal原理解析

概述

ThreadLocal,線程本地存儲區(Thread Local Storage,簡稱爲TLS),通過它可以在指定的線程中存儲數據,數據存儲之後,只能在指定的線程中可以獲取到存儲的數據,對於其他線程來說則無法獲取到數據。

使用

ThreadLocal 提供了 get(),set(T value),remove() 3個對外方法,來看一個簡單的例子:

public class Main {

    //定義兩個ThreadLocal
    private static ThreadLocal<Integer> mThreadLocal = new ThreadLocal<>();
    private static ThreadLocal<Integer> mThreadLocal2 = new ThreadLocal<>();

    public static void main(String[] args) {

        //主線程
        mThreadLocal.set(0);//mThreadLocal存值
        System.out.println(Thread.currentThread().getName() + " - " + mThreadLocal.get());////mThreadLocal取值

        mThreadLocal2.set(1);//mThreadLocal2存值
        System.out.println(Thread.currentThread().getName() + " - " + mThreadLocal2.get());////mThreadLoca2取值

        //子線程1
        new Thread(() -> {
            mThreadLocal.set(2);//mThreadLocal存值
            System.out.println(Thread.currentThread().getName() + " - " + mThreadLocal.get());//mThreadLocal取值
        }).start();

        //子線程2
        new Thread(() -> System.out.println(Thread.currentThread().getName() + " - " + mThreadLocal.get())).start();//mThreadLocal取值
    }
}

/* 輸出結果:*/
main - 0
main - 1
Thread-0 - 2
Thread-1 - null

上面代碼中,有三個線程,分別爲主線程(main)、子線程1(Thread-0)、子線程2(Thread-1),有兩個不同的ThreadLocal實例,分別爲mThreadLocal、mThreadLocal2,根據輸出結果,得出以下結論:

  • 1、在同一線程中,通過不同的ThreadLocal存值,則通過相應的ThreadLocal取出的值也不一樣,例如這裏在主線程通過分別設置mThreadLocal的值爲0,mThreadLocal2的值爲1,從輸出結果可以看出mThreadLocal取出的值還是0,mThreadLocal2取出的值還是1;
  • 2、在不同線程中,訪問的是同一個ThreadLocal對象,但通過同一個ThreadLocal獲取的值卻不一樣,例如這裏在子線程1設置mThreadLocal的值2,在子線程2沒有設置mThreadLocal的值,從輸出結果可以看出通過同一個ThreadLocal獲取的值不一樣,一個爲2,一個爲null;

這裏給出先給出解釋:

在java中,線程的表示是用java.lang.Thread類來表示,在Thread類中定義了一個ThreadLocalMap字段,如下:

//Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap是ThreadLocal中的一個靜態內部類,它是一個簡化版的HashMap容器,也就是說這個容器是以key-value來存值的,每個線程都管理着自己的容器,我們可以從外部拿到這個threadLocals,然後往這個容器中存值、取值等,而ThreadLocal就是這樣做的,當我們通過ThreadLocal的set方法存值時,它會以當前ThreadLocal實例爲key,要存的值爲value,把這個映射保存進相應的線程的容器中,當我們通過ThreadLocal的get方法取值時,它會以當前ThreadLocal實例爲Key,然後去相應線程的容器中查找這個鍵爲Key的value值返回。

所以:

  • 1、在上面的主線程中,mThreadLocal和mThreadLocal2這兩個ThreadLocal實例都往主線程的容器中存值,但由於mThreadLocal和mThreadLocal2的兩個實例不一樣,導致key不一樣,所以在容器中存放value的位置也不一樣,這樣就可以根據相應的key獲取出相應的value;
  • 2、在上面的子線程1和子線程2中,都是通過mThreadLoca這個ThreadLocal實例來存取值,但是由於線程實例不一樣,導致獲取的容器也不一樣,所以根據同一個key從不同的容器中獲取的value也就不一樣。

如果不是很理解,就來看一下下面關於ThreadLocal的源碼分析:

源碼分析

1、ThreadLocal

1.1、get方法

該方法用於獲取當前線程TLS區域的數據,該方法的源碼如下:

//ThreadLocal.java
public T get() {
    //1、獲取當前線程
    Thread t = Thread.currentThread();
    //2、 以當前線程爲參數,獲取一個 ThreadLocalMap 對象
    ThreadLocalMap map = getMap(t)if (map != null) {
            //2.1、map不爲空,則以當前 ThreadLocal 對象實例作爲key值,去map中取值,有找到直接返回
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                T result = (T)e.value;
                return result;
            }
        }
    //2.2、 map 爲空或者在map中取不到值,那麼走這裏,返回默認初始值
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    //getMap方法返回傳進來的線程中的threadlocals字段,threadlocals是一個ThreadLocalMap對象,它就是我們上面提到的每個線程中保存數據的Map容器
    return t.threadLocals;
}

ThreadLocal的get方法,首先獲取當前線程,接着以當前線程爲參數調用getMap方法,getMap方法返回當前線程中保存數據的Map容器,調用getMap之後就得到了當前線程的數據存儲容器即map,然後判斷它是否爲null:

  • 註釋2.1:當map不爲空時,就以當前ThreadLocal實例爲參數調用map.getEntry方法,該方法返回一個ThreadLocalMap.Entry對象,Entry就是線程容器中表示key-value映射的類,它裏面有一個key和一個value值,而value值就是我們需要的數據;
  • 註釋2.2: 當map爲空時或者在map中找不到數據即map.getEntry返回了null,就調用setInitialValue方法返回默認初始值,該方法源碼如下:
//ThreadLocal.java
private T setInitialValue() {
    //1. 獲取初始值,默認返回Null,允許重寫
    T value = initialValue();
    //2、獲取當前線程並以當前線程爲參數,獲取一個ThreadLocalMap 對象
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    //3、當map不爲空,設置初始值給map
    if (map != null)
        map.set(this, value);
    else//當map爲空, 創建當前線程的數據存儲容器map
        createMap(t, value);
    //返回初始值
    return value;
}

protected T initialValue() {
     return null;
}

該方法分爲以下3步:

1、 調用initialValue(),可以看到它默認返回null,但是我們可以重寫該方法並返回你想要的初始值

2、獲取當前線程,再次以當前線程爲參數,獲取一個ThreadLocalMap 對象;

3、再次判斷map是否爲空,如下:

  • 當map不爲空時,以當前ThreadLocal實例爲key,initialvalue方法獲取到的初始值爲value,將(key - value)值保存到map中;
  • 當map爲空時,就調用createMap方法, ThreadLocal 中的 createMap() 方法就是對當前Thread 中的 threadLocals成員變量賦值,該方法源碼如下:
void createMap(Thread t, T firstValue) {
    //以當前ThreadLocal實例對象爲key,傳進來的value爲值,創建一個ThreadLocalMap實例
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

Thread 中的 threadLocal 成員變量初始值爲 null,並且在 Thread 類中沒有任何賦值的地方,只有在 ThreadLocal 中的 createMap方法中對其賦值,而調用createMap方法的地方就兩個:set 和 setInitialValue方法,而調用 setInitialValue() 方法的地方只有 get方法。

到此,get方法就已經講完了。

1.2、set方法

set方法是將value存儲到當前線程的TLS區域,在上面的get方法中,ThreadLocal會根據線程取出線程的容器,然後再根據key(ThreadLocal實例)去容器中取值,如果取不到值,就會返回初始值,初始值默認是null,那是因爲ThreadLocal要調用set方法後,容器中才有我們想要的值,set方法的源碼如下:

//ThreadLocal.java
public void set(T value) {	
    //1. 取當前線程對象
    Thread t = Thread.currentThread();
    //2. 取當前線程的數據存儲容器
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //2.1. 如果map不爲空,以當前ThreadLocal實例對象爲key,存值
        map.set(this, value);
    else
        //2.2. 如果map爲空,新建一個當前線程的數據存儲容器
        createMap(t, value);
}

set方法的步驟是不是感覺似曾相識,沒錯,它和get方法中所講的setInitialValue方法幾乎一模一樣,只是沒有調用initialValue方法返回初始值,因爲set方法的參數value就是我們想要保存的值,而不用調用initialValue方法設置默認初始值。

至此,set方法講解完畢。

get和set兩個方法內部都會自動根據當前線程選擇相對應的容器存取,所以其實ThreadLocal的核心還是ThreadLocalMap對象,get方法會調用ThreadLocalMap的**getEntry(ThreadLocal<?>)**根據ThreadLocal實例獲取一個Entry對象,該Entry對象保存了key-value映射,set方法會調用ThreadLocalMap的**set(ThreadLocal<?>, Object)**保存key-value映射到ThreadLocalMap中,下面我們就簡單的講解一下ThreadLocalMap的組成。

2、ThreadLocal::ThreadLocalMap

//ThreadLocal::ThreadLocalMap
static class ThreadLocalMap {
  
    private static final int INITIAL_CAPACITY = 16;//初始容量爲16,必須爲2^n
    private int size = 0;//table中Entry的數量
    private int threshold;//擴容閾值, 當size >= threshold時就會觸發擴容邏輯
    private Entry[] table;//table數組
    
    private void setThreshold(int len) {
         //ThreadLocalMap的threshold爲table數組長度的2/3
         threshold = len * 2 / 3;
    }
    
    //以下爲ThreadLocal調用ThreadLocalMap的主要方法
    //分別對應ThreadLocal的get、set、remove方法
    
    private Entry getEntry(ThreadLocal<?> key) {
        //...
    }
    
    private void set(ThreadLocal<?> key, Object value) {
        //...
    }

    private void remove(ThreadLocal<?> key) {
        //...
    }
    
    //...
}

ThreadLocalMap 就是一個用於存儲數據的容器類, 它是ThreadLocal中的靜態內部類, 它的底層實現類似於hashMap的實現,也是基於哈希算法,裏面table數組就是真正的存儲每個線程的數據,數組的每個元素類型就是一個具有(key-value)鍵值對的Entry,key對應ThreadLocal實例,value對應要存儲的數據,Entry在數組中的index值是根據key的threadLocalHashCodehash算法算出來的,threadLocalHashCode是ThreadLocal中的一個字段,如下:

//ThreadLocal.java
private final int threadLocalHashCode = nextHashCode();//threadLocalHashCode的值等於nextHashCode方法的返回值
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
    //每次調用nextHashCode方法都會在原本的int值加上0x61c88647後再返回
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

而ThreadLocalMap 的hash算法,即計算index值的算法如下:

//hash算法:threadLocalHashCode與(table數組長度-1)相與
//這樣會使得i均勻的分佈在數組的長度之內
int i = key.threadLocalHashCode & (table.length - 1);

當出現衝突時,ThreadLocalMap是使用線性探測法來解決衝突的,即如果i位置已經有了key-value映射,就會在i + 1位置找,直到找到一個合適的位置。

我們看一下Entry的實現,如下:

static class Entry extends WeakReference<ThreadLocal> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        //key是弱引用
        super(k);
        value = v;
    }
}

Entry 繼承至 WeakReference,並且它的key是弱引用,但是value是強引用,所以如果key關聯的ThreadLocal實例沒有強引用,只有弱引用時,在gc發生時,ThreadLocal實例就會被gc回收,當ThreadLocal實例被gc回收後,由於value是強引用,導致table數組中存在着null - value這樣的映射,稱之爲髒槽,這種髒槽會浪費table數組的空間,所以需要及時清除,所以ThreadLocalMap 中提供了expungeStaleEntry方法和expungeStaleEntries方法去清理這些髒槽,每次ThreadLocalMap 運行getEntry、set、remove等方法時,都會主動的間接使用這些方法去清理髒槽,從而釋放更多的空間,避免無謂的擴容操作。

java中有4種引用,分別爲:強引用、軟引用、弱引用和虛引用.

3、小結

根據使用實例和源碼分析,我們得出以下兩個結論:

1、使用同一個ThreadLocal對象,可以維護着不同線程的數據副本:

這是因爲,這些數據本來就是存儲在各自線程中了,ThreadLocal 的 get() 方法內部其實會先去獲取當前的線程對象,然後直接將線程存儲數據的容器(ThreadLocalMap)取出來,如果爲空就會先創建並將初始值和當前 ThreadLocal 對象綁定存儲進去,這樣不同線程即使調用了同一 ThreadLocal 對象的get方法,取的數據也是各自線程的數據副本,這樣自然就可以達到維護不同線程各自相互獨立的數據副本,且以線程爲作用域的效果了。

2、在同一線程中不同ThreadLocal對象雖然共用同一個線程中的容器,但卻可以相互獨立運作:

這是因爲,ThreadLocal 的 get() 方法內部根據線程取出map後,當map不爲空時,會根據ThreadLocal實例去map中查找value,換句話說,在將數據存儲到線程的容器map中是以當前 ThreadLocal 對象實例爲 key 存儲,這樣,即使在同一線程中調用了不同的 ThreadLocal 對象的 get() 方法,所獲取到的數據也是不同的,達到同一線程中不同 ThreadLocal 雖然共用一個容器,但卻可以相互獨立運作的效果。

使用場景

如果你是單線程環境,那麼不用考慮使用ThreadLocal了,ThreadLocal是用來在多線程環境下的。

在多線程環境下,如果某個變量只在特定的某個線程中使用,即我們對這個變量的操作只限定在同一個線程內,那麼就不需要使用同步來保證這個變量的正確性,因爲沒有存在競爭,這時我們可以把這個變量直接存儲在線程內部中,要使用這個變量時直接從線程內部拿出來後再操作,這就避免了使用同步帶來的性能消耗,典型的例子有Android中的Looper,通過Looper.myLooper方法就可以返回當前線程關聯的Looper。

總的來說,當某些數據是以線程爲作用域並且不同線程具有不同的數據副本的時候,就可以考慮採用 ThreadLocal。

正確使用

上面介紹ThreadLocalMap時提到,如果ThreadLocalMap中的key關聯的ThreadLoca實例被回收了,就會導致ThreadLocalMap還殘留着這個key對應的value實例,出現了髒槽,而髒槽是通過ThreadLocalMap主動的調用expungeStaleEntry或expungeStaleEntries方法清理,而這兩個方法只會在主動調用ThreadLocalMap的set(ThreadLocal , Object)、getEntry(ThreadLocal)和remove(ThreadLocal)等方法時纔會被調用,而ThreadLocalMap的set、getEntry、remove方法只有在調用ThreadLocal的set、get、remove方法時纔會被調用。

所以想象這樣的一種情況:我們使用ThreadLocal的set方法往線程的ThreadLocalMap中保存了一個非常大的數據,從這之後,我沒有再調用過ThreadLocal的set、get、remove等方法,當這個數據對應的ThreadLocal實例被gc回收後,ThreadLocalMap中還殘留這這個null-value映射,並且這個線程的生命週期是和程序同步的,直到程序結束它纔會結束,這樣就導致了內存泄漏的發生,產生內存浪費。

所以我們平常使用完ThreadLocal後,應該手動的調用remove方法把映射刪除,如下:

private static ThreadLocal<Integer> mThreadLocal = new ThreadLocal<>();
try {
    //調用mThreadLocal的set方法
} finally {
    threadLocal對象.remove();
}

爲什麼ThreadLocal要定義爲靜態變量?,可以參考:

總結

本文從使用到源碼簡單的分析了一下ThreadLocal,介紹了ThreadLocal的使用場景和正確使用方法,從ThreadLocal的get和set方法中都可以看出,它們所操作的對象都是當前線程中的容器ThreadLocalMap,所以在不同線程中訪問同一個ThreadLocal的get和set方法,它們對ThreadLocal所做的讀寫操作僅限與線程內部。

參考資料:

ThreadLocal 相關的各種面試問法瞭解一下?

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