天天看點

ThreadLocal深入解析:使用場景、細節及最佳實踐

作者:程式員的秃頭之路

1、什麼是ThreadLocal?

ThreadLocal是Java中用于實作線程局部變量的一個類,它可以為每個線程建立獨立的變量副本,進而保證線程安全。

本文将詳細解讀Java中的ThreadLocal,包括其使用場景、一些實作細節以及最佳實踐,幫助您更好地了解和運用ThreadLocal。

2、ThreadLocal用在什麼地方?

ThreadLocal是Java中一個常用的線程局部變量工具類,它可以為每個線程建立獨立的變量副本。在多線程并發場景下,ThreadLocal可以有效地避免資源競争,保證資料的線程安全。ThreadLocal常用的場景包括:

1、 資料庫連接配接池:資料庫連接配接池中的Connection對象可以使用ThreadLocal存儲,確定每個線程有各自獨立的Connection,避免多個線程共享一個Connection導緻的資料混亂。

2、 Spring事務管理:在Spring架構中,為了保證事務的一緻性,通常會使用ThreadLocal來存儲與目前線程相關的事務資訊。

3、 Web應用的Session管理:Web應用中,為了跟蹤使用者的操作狀态,可以将Session資訊存儲在ThreadLocal中,實作線程隔離,確定每個使用者通路時都有獨立的Session資訊。

2.1、示例:處理分頁參數

在SpringBoot中使用ThreadLocal結合攔截器來存儲分頁參數,首先需要建立一個攔截器類,然後實作HandlerInterceptor接口。在preHandle()方法中從請求頭中擷取page和size參數,并将它們存儲在ThreadLocal中。在afterCompletion()方法中清除ThreadLocal。以下是示例代碼:

1、 建立一個ThreadLocal的子類PageContextHolder來存儲分頁資訊:

public class PageContextHolder {

    private static final ThreadLocal<PageInfo> PAGE_INFO_HOLDER = new ThreadLocal<>();

    public static void set(PageInfo pageInfo) {
        PAGE_INFO_HOLDER.set(pageInfo);
    }

    public static PageInfo get() {
        return PAGE_INFO_HOLDER.get();
    }

    public static void remove() {
        PAGE_INFO_HOLDER.remove();
    }
}           

2、 建立一個PageInfo類來存儲分頁參數:

public class PageInfo {
    private int page;
    private int size;

    public PageInfo(int page, int size) {
        this.page = page;
        this.size = size;
    }

    // Getter和Setter方法
}           

3、 建立一個攔截器PageInterceptor:

@Component
public class PageInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        int page = Integer.parseInt(request.getHeader("page"));
        int size = Integer.parseInt(request.getHeader("size"));
        PageInfo pageInfo = new PageInfo(page, size);
        PageContextHolder.set(pageInfo);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        PageContextHolder.remove();
    }
}           

4、 在SpringBoot配置類中注冊攔截器:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private PageInterceptor pageInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(pageInterceptor).addPathPatterns("/**");
    }
}           

5、 在Service層,調用DAO接口之前擷取ThreadLocal中存儲的分頁參數:

@Service
public class UserService {

    @Autowired
    private UserDao userDao;

    public List<User> getUsers() {
        PageInfo pageInfo = PageContextHolder.get();
        int offset = (pageInfo.getPage() - 1) * pageInfo.getSize();
        return userDao.getUsers(offset, pageInfo.getSize());
    }
}           

這樣,在攔截器中,我們可以從請求頭中擷取page和size參數,并将它們存儲在ThreadLocal中。在Service層中,我們可以從ThreadLocal擷取分頁參數并在調用DAO接口時使用。同時,攔截器會在請求處理完成後清除ThreadLocal中的分頁資訊,防止記憶體洩漏。

3、源碼分析

3.1、ThreadLocal的内部結構:

ThreadLocal内部使用了一個名為ThreadLocalMap的靜态内部類來存儲每個線程的變量副本。ThreadLocalMap的每個執行個體都與一個線程關聯,是存儲線程局部變量的容器。ThreadLocalMap的鍵為ThreadLocal執行個體,值為線程局部變量。

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

3.2、ThreadLocal的set()方法:

當調用ThreadLocal的set()方法設定線程局部變量時,實際上是将變量存儲在了目前線程關聯的ThreadLocalMap中。源碼如下:

public void set(T value) {
    // 擷取目前線程
    Thread t = Thread.currentThread();
    // 擷取目前線程關聯的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 将目前ThreadLocal執行個體作為key,value作為值存入ThreadLocalMap
        map.set(this, value);
    } else {
        // 如果ThreadLocalMap為null,則為目前線程建立一個ThreadLocalMap,并将變量存入
        createMap(t, value);
    }
}           

3.3、ThreadLocal的get()方法:

當調用ThreadLocal的get()方法擷取線程局部變量時,實際上是從目前線程關聯的ThreadLocalMap中擷取變量。源碼如下:

public T get() {
    // 擷取目前線程
    Thread t = Thread.currentThread();
    // 擷取目前線程關聯的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 從ThreadLocalMap中擷取變量
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 如果ThreadLocalMap為null或未找到對應的值,則調用initialValue()方法設定初始值
    return setInitialValue();
}           

3.4、ThreadLocal的remove()方法:

當調用ThreadLocal的remove()方法時,實際上是從目前線程關聯的ThreadLocalMap中移除變量。源碼如下:

public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
}           

3.5、ThreadLocal的initialValue()方法:

ThreadLocal提供了一個名為initialValue()的方法,允許使用者為線程局部變量設定一個初始值。預設情況下,此方法傳回null。使用者可以通過建立ThreadLocal的匿名子類并重寫initialValue()方法來提供初始值。

public class ThreadLocalExample {

    // 建立一個ThreadLocal的匿名子類,并重寫initialValue()方法來提供初始值
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return "Initial value";
        }
    };

    public static void main(String[] args) {
        // 啟動兩個線程來示範ThreadLocal的初始值
        new Thread(() -> {
            System.out.println("Thread-1: " + threadLocal.get()); // 輸出:Thread-1: Initial value
            threadLocal.set("Thread-1 value");
            System.out.println("Thread-1: " + threadLocal.get()); // 輸出:Thread-1: Thread-1 value
        }).start();

        new Thread(() -> {
            System.out.println("Thread-2: " + threadLocal.get()); // 輸出:Thread-2: Initial value
            threadLocal.set("Thread-2 value");
            System.out.println("Thread-2: " + threadLocal.get()); // 輸出:Thread-2: Thread-2 value
        }).start();
    }
}           

3.6、弱引用與記憶體洩漏問題:

ThreadLocalMap的鍵為ThreadLocal執行個體,是使用弱引用(WeakReference)實作的。這意味着,在沒有強引用指向ThreadLocal對象的情況下,ThreadLocal執行個體可能會被垃圾回收器回收。這樣設計的目的是避免因ThreadLocal長時間不被釋放而導緻的記憶體洩漏。但是,這也帶來了一定的問題:當ThreadLocal執行個體被回收後,其對應的值可能仍然存儲在ThreadLocalMap中,導緻記憶體洩漏。為了避免這種情況,需要在使用完ThreadLocal後主動調用remove()方法清除線程局部變量。

3.7、一個線程中使用多個ThreadLocal,threadLocals的key怎麼解決沖突的

private void set(ThreadLocal<?> key, Object value) {
    // 此處解釋了為什麼不像get()方法那樣使用快速路徑(fast path)
    // 因為使用set()方法建立新的鍵值對和替換現有鍵值對的情況都很常見,
    // 在這種情況下,快速路徑的失敗率很高。

    // 擷取ThreadLocalMap的Entry數組
    Entry[] tab = table;
    // 擷取Entry數組的長度
    int len = tab.length;
    // 計算ThreadLocal執行個體的哈希值對應的數組下标
    int i = key.threadLocalHashCode & (len - 1);

    // 周遊數組,尋找合适的位置存儲鍵值對
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        // 擷取目前Entry的key(ThreadLocal執行個體)
        ThreadLocal<?> k = e.get();

        // 如果找到了一個與目前ThreadLocal執行個體相等的key,直接替換value即可
        if (k == key) {
            e.value = value;
            return;
        }

        // 如果找到了一個key為null的Entry,說明這個Entry的ThreadLocal執行個體已經被回收
        // 可以将目前ThreadLocal執行個體和值存儲在這個位置
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 如果沒有找到相同的key,将新的Entry存儲在數組的空位置
    tab[i] = new Entry(key, value);
    // 增加ThreadLocalMap的size
    int sz = ++size;
    // 清理部分無效的Entry(key為null的Entry)
    // 如果清理後size仍然大于等于門檻值,進行rehash操作
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}           

在ThreadLocal中,通過使用ThreadLocalMap來存儲線程局部變量。ThreadLocalMap的key是ThreadLocal執行個體,而value是線程局部變量。當在一個線程中使用多個ThreadLocal時,ThreadLocalMap會通過特定的雜湊演算法來計算每個ThreadLocal執行個體的哈希值,進而避免key沖突。

在上面的代碼中,可以看到set()方法首先計算ThreadLocal執行個體的哈希值,然後根據哈希值在ThreadLocalMap中查找對應的位置。ThreadLocalMap底層實際上是一個Entry數組(tab),每個Entry包含一個ThreadLocal執行個體(弱引用)和一個對應的值。當查找到某個位置時,會進行以下操作:

  • 如果找到了一個與目前ThreadLocal執行個體相等(k == key)的Entry,說明已經存在相同的key,直接替換value即可。
  • 如果找到了一個key為null的Entry(k == null),說明這個Entry的ThreadLocal執行個體已經被回收,可以将目前ThreadLocal執行個體和值存儲在這個位置。
  • 如果沒有找到相同的key,會在ThreadLocalMap中找到一個空位置(tab[i])存儲新的Entry。

通過這種方法,ThreadLocal可以避免在同一個線程中使用多個ThreadLocal時key沖突的問題。ThreadLocalMap還會在适當的時候進行rehash操作以保持數組的負載因子在合理範圍内,進而確定查找和插入操作的效率。

ThreadLocalMap采用了開放尋址法(open addressing)來解決哈希沖突。這意味着當發生哈希沖突時,會選擇其他可用的位置來存儲鍵值對。

在上述set()方法的源碼中,可以看到這一過程:

for (Entry e = tab[i];
     e != null;
     e = tab[i = nextIndex(i, len)]) {
    ThreadLocal<?> k = e.get();
    ...
}           

當找到的數組下标i對應的Entry不為空時(即發生了哈希沖突),會調用nextIndex(i, len)方法尋找下一個可用的位置。nextIndex()方法的實作如下:

private int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}           

nextIndex()方法實際上是簡單地将目前下标加1,如果到達數組邊界,則回到數組起始位置。這樣,在遇到哈希沖突時,ThreadLocalMap會順序查找數組中的下一個可用位置。

通過這種方法,ThreadLocalMap可以在發生哈希沖突時找到其他可用位置來存儲鍵值對。需要注意的是,為了保證查找和插入操作的效率,ThreadLocalMap還會在适當的時候進行rehash操作以保持數組的負載因子在合理範圍内。

3.8、hash沖突,get怎麼擷取的

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

private Entry getEntry(ThreadLocal<?> key) {
    // 計算ThreadLocal執行個體的哈希值對應的數組下标
    int i = key.threadLocalHashCode & (table.length - 1);
    // 擷取下标對應的Entry
    Entry e = table[i];
    // 如果Entry不為空且key與目前ThreadLocal執行個體相等,傳回該Entry
    if (e != null && e.get() == key)
        return e;
    // 否則,處理哈希沖突的情況
    else
        return getEntryAfterMiss(key, i, e);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    // 擷取ThreadLocalMap的Entry數組
    Entry[] tab = table;
    // 擷取Entry數組的長度
    int len = tab.length;

    // 當Entry不為空時,處理哈希沖突
    while (e != null) {
        // 擷取目前Entry的key(ThreadLocal執行個體)
        ThreadLocal<?> k = e.get();
        // 如果找到了一個與目前ThreadLocal執行個體相等的key,傳回該Entry
        if (k == key)
            return e;
        // 如果key為null,說明目前Entry的ThreadLocal執行個體已經被回收,清理該Entry
        if (k == null)
            expungeStaleEntry(i);
        // 否則,将下标移動到下一個位置以處理哈希沖突
        else
            i = nextIndex(i, len);
        // 擷取下一個位置的Entry
        e = tab[i];
    }
    // 如果沒有找到相應的Entry,傳回null
    return null;
}           

通過這段源碼,我們可以看到ThreadLocalMap是如何通過雜湊演算法和開放尋址法來解決在一個線程中使用多個ThreadLocal時get操作的key沖突問題。在getEntry方法中,如果找到的Entry不為空且key與目前ThreadLocal執行個體不相等,将調用getEntryAfterMiss方法來處理哈希沖突。在getEntryAfterMiss方法中,會順序查找數組中的下一個位置,直到找到相應的Entry或周遊完整個數組。

4、ThreadLocal一些細節

1、 ThreadLocal内部使用的是弱引用(WeakReference),這意味着在沒有強引用指向對象的情況下,ThreadLocal中的變量可能會被垃圾回收器回收。

2、 為了防止記憶體洩漏,需要注意在使用完ThreadLocal後,要及時調用其remove()方法清理線程局部變量。

3、 ThreadLocal并不解決共享對象的線程安全問題。即使每個線程都有獨立的變量副本,如果這個副本指向的是一個可變對象,那麼多個線程對這個對象的操作仍然可能引發線程安全問題。

5、ThreadLocal的最佳實踐

1、 使用靜态修飾符:為了確定ThreadLocal在類的所有執行個體中隻有一個執行個體,可以使用static修飾符。這樣可以避免不必要的執行個體化,節省記憶體資源。

2、 使用匿名子類初始化ThreadLocal:當需要為ThreadLocal設定初始值時,可以使用匿名子類的方式重寫initialValue()方法,提供初始值。

3、 及時清理資源:使用完ThreadLocal後,一定要記得調用remove()方法清理資源,防止記憶體洩漏。

6、子父線程共享資料

要讓父子線程共享ThreadLocal資料,可以考慮使用InheritableThreadLocal類。InheritableThreadLocal是ThreadLocal的子類,它允許子線程繼承父線程的ThreadLocal變量的值。

當建立一個新的子線程時,InheritableThreadLocal會将父線程的值複制到子線程中。請注意,這種繼承僅在建立子線程時發生。父線程和子線程在之後的操作中對ThreadLocal變量的修改是互相獨立的。

以下是使用InheritableThreadLocal的示例:

public class InheritableThreadLocalExample {

    private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        inheritableThreadLocal.set("Parent thread value");

        Thread childThread = new Thread(() -> {
            System.out.println("Child thread initial value: " + inheritableThreadLocal.get());
            inheritableThreadLocal.set("Child thread value");
            System.out.println("Child thread updated value: " + inheritableThreadLocal.get());
        });

        childThread.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("Parent thread value: " + inheritableThreadLocal.get());
    }
}           

在這個示例中,父線程将inheritableThreadLocal的值設定為"Parent thread value"。然後建立一個子線程。在子線程中,首先列印從父線程繼承的值,然後修改它。最後,在父線程中列印inheritableThreadLocal的值。

輸出結果如下:

Child thread initial value: Parent thread value
Child thread updated value: Child thread value
Parent thread value: Parent thread value           

從輸出可以看出,子線程确實繼承了父線程的ThreadLocal值。在子線程修改值後,父線程的值保持不變,表明它們是互相獨立的。

需要注意的是,InheritableThreadLocal的預設行為是對父線程的值進行淺拷貝。這意味着如果ThreadLocal值是一個可變對象(例如清單、映射等),子線程和父線程将共享同一個對象執行個體。要改變這種行為,可以通過覆寫InheritableThreadLocal的childValue方法來實作自定義的繼承政策,例如進行深拷貝。

7、思考細節

ThreadLocal是Java中一個重要的線程局部變量工具類,它在多線程并發場景下發揮着重要作用。然而,ThreadLocal并不是萬能的,它無法解決共享對象的線程安全問題。是以,在使用ThreadLocal時,我們需要根據實際情況選擇合适的方法保證線程安全。

在實際開發中,可以結合以下方法來保證線程安全:

1、 使用線程安全的資料結構:例如,使用java.util.concurrent包中提供的線程安全集合類,如ConcurrentHashMap、CopyOnWriteArrayList等。

2、 使用同步機制:通過synchronized關鍵字、ReentrantLock等鎖機制,確定在同一時刻隻有一個線程能通路共享資源。

3、 使用原子操作類:如AtomicInteger、AtomicLong等,可以保證對基本資料類型的原子性操作。

4、 使用分離技術:通過将共享資源分離成多個獨立的資源,降低資源競争的風險。例如,使用ThreadLocal為每個線程配置設定獨立的變量副本。

總結:

ThreadLocal提供了一種在多線程環境下實作線程局部變量的機制,它通過為每個線程建立獨立的變量副本來保證線程安全。ThreadLocal内部使用了一個名為ThreadLocalMap的靜态内部類來存儲線程局部變量,每個線程都關聯一個ThreadLocalMap執行個體。ThreadLocal的set(), get(), remove()方法分别用于設定、擷取和移除線程局部變量。需要注意的是,ThreadLocal使用了弱引用,是以在使用完ThreadLocal後,應主動調用remove()方法清除線程局部變量,以防止記憶體洩漏。

繼續閱讀