史上最全 ThreadLocal 詳解

概述

線程本地變量。當使用 ThreadLocal 維護變量時, ThreadLocal 爲每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程。

每個線程都有一個 ThreadLocalMap ( ThreadLocal 內部類),Map 中元素的鍵爲 ThreadLocal ,而值對應線程的變量副本。

ThreadLocal 原理

如何實現線程隔離

具體關於爲線程分配變量副本的代碼如下:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap threadLocals = getMap(t);
    if (threadLocals != null) {
        ThreadLocalMap.Entry e = threadLocals.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

如果存在則直接返回很好理解, 那麼對於如何初始化的代碼又是怎樣的呢?

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

同時, ThreadLocal 還提供了直接操作 Thread 對象中的 threadLocals 的方法

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

這樣也可以不實現 initialValue:

public Connection getConnection() {
    Connection connection = dbConnectionLocal.get();
    if (connection == null) {
        try {
            connection = DriverManager.getConnection("""""");
            dbConnectionLocal.set(connection);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    return connection;
}

看過代碼之後就很清晰的知道了爲什麼 ThreadLocal 能夠實現變量的多線程隔離了; 其實就是用了 Map 的數據結構給當前線程緩存了, 要使用的時候就從本線程的 threadLocals 對象中獲取就可以了, key 就是當前線程;

當然了在當前線程下獲取當前線程裏面的 Map 裏面的對象並操作肯定沒有線程併發問題了, 當然能做到變量的線程間隔離了;

ThreadLocalMap 對象是什麼

本質上來講, 它就是一個 Map, 但是這個 ThreadLocalMap 與平時見到的 Map 有點不一樣

要了解 ThreadLocalMap 的實現, 我們先從入口開始, 就是往該 Map 中添加一個值:

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

//這裏用的是Hash衝突的開放定址法的線性探測
    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();
}

先進行簡單的分析, 對該代碼表層意思進行解讀:

瞭解完 Set 方法, 後面就是 Get 方法了:

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 的索引位置, 如果索引位置處的 entry 不爲空並且鍵與 threadLocal 是同一個對象, 則直接返回; 否則去後面的索引位置繼續查找

Entry 對象

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

    Entry(ThreadLocal<?> k, Object v) {
        super(k);//父類是WeakReference,也就是相當於new了一個弱引用(k)
        //也就相當於 map中的key是弱引用的
        value = v;
    }
}

這裏的 key 指向的 ThreadLocal 是弱引用,是爲了防止 ThreadLocal 對象永遠不會被回收。因爲,若 key 爲強引用,當 ThreadLocal 不想用了,那麼就令 tl = null,但是此時 key 中還有一個強引用指向 ThreadLocal,因此也就永遠無法進行回收 (除非 ThreadLocalMap 不用了),所以會有內存泄露;但如果 key 使用的是弱引用,只要 GC,就會回收

但是還會有內存泄漏存在,ThreadLocal 被回收,就導致 key=null, 此時 map 中也就無法訪問到 value,無法訪問到的 value 也就無用了,也就是說,這個 k-v 對無用了,那麼 value 也應該被回收,但實際上 value 可能沒有被回收,因此依然存在內存泄露

內存泄漏(Memory Leak)是指程序中已動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重後果。

弱引用:GC 時,若沒有強引用指向這個對象了,只剩下弱引用,就會直接進行回收。原因就在於 GC 時無關內存是否足夠,弱引用會被直接回收。所以,只要 tl=null 了,那麼 GC 時,key 指向的 ThreadLocal 對象就會被回收

ThreadLocal 內存泄漏的原因?

每個線程都有⼀個 ThreadLocalMap 的內部屬性,map 的 key 是 ThreaLocal ,定義爲弱引用,value 是強引用類型。垃圾回收的時候會⾃動回收 key,而 value 的回收取決於 Thread 對象的生命週期。

一般會通過線程池的方式複用線程節省資源,而如果用線程池來操作 ThreadLocal 對象確實會造成內存泄露, 因爲對於線程池裏面不會銷燬的線程, 裏面總會存在着 <ThreadLocal, LocalVariable> 的強引用, 因爲 final static 修飾的 ThreadLocal 並不會釋放, 而 ThreadLocalMap 對於 Key 雖然是弱引用, 但是強引用不會釋放, 弱引用當然也會一直有值, 同時創建的 LocalVariable 對象也不會釋放, 就造成了內存泄露; 如果 LocalVariable 對象不是一個大對象的話, 其實泄露的並不嚴重, 泄露的內存 = 核心線程數 * LocalVariable 對象的大小;

所以, 爲了避免出現內存泄露的情況, ThreadLocal 提供了一個清除線程中對象的方法, 即 remove, 其實內部實現就是調用 ThreadLocalMap 的 remove 方法:

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

應用場景

每個線程維護了一個 “序列號”

public class SerialNum {
    // The next serial number to be assigned
    private static int nextSerialNum = 0;

    private static ThreadLocal serialNum = new ThreadLocal() {
        protected synchronized Object initialValue() {
            return new Integer(nextSerialNum++);
        }
    };

    public static int get() {
        return ((Integer) (serialNum.get())).intValue();
    }
}

Session 的管理

Web 應用中的請求處理:在 Web 應用中,一個請求通常會被多個線程處理,每個線程需要訪問自己的數據,使用 ThreadLocal 可以確保數據在每個線程中的獨立性。

經典的另外一個例子:

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

在線程內部創建 ThreadLocal

線程池中的線程對象共享數據:線程池中的線程對象是可以被多個任務共享的,如果線程對象中需要保存任務相關的數據,使用 ThreadLocal 可以保證線程安全。

當然,在使用線程池時,ThreadLocal 可能會導致線程重用時的數據殘留,從而影響程序的正確性。因此,在使用線程池時,要確保在任務執行前後清理 ThreadLocal 的值,以避免線程重用時的數據殘留。

線程類內部創建 ThreadLocal,基本步驟如下:

public class ThreadLocalTest implements Runnable{
    
    ThreadLocal<Student> StudentThreadLocal = new ThreadLocal<Student>();

    @Override
    public void run() {
        String currentThreadName = Thread.currentThread().getName();
        System.out.println(currentThreadName + " is running...");
        Random random = new Random();
        int age = random.nextInt(100);
        System.out.println(currentThreadName + " is set age: "  + age);
        Student Student = getStudentt(); //通過這個方法,爲每個線程都獨立的new一個Studentt對象,每個線程的的Studentt對象都可以設置不同的值
        Student.setAge(age);
        System.out.println(currentThreadName + " is first get age: " + Student.getAge());
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println( currentThreadName + " is second get age: " + Student.getAge());
        
    }
    
    private Student getStudentt() {
        Student Student = StudentThreadLocal.get();
        if (null == Student) {
            Student = new Student();
            StudentThreadLocal.set(Student);
        }
        return Student;
    }

    public static void main(String[] args) {
        ThreadLocalTest t = new ThreadLocalTest();
        Thread t1 = new Thread(t,"Thread A");
        Thread t2 = new Thread(t,"Thread B");
        t1.start();
        t2.start();
    }
    
}

class Student{
    int age;
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    
}

java 開發手冊中推薦的 ThreadLocal

看看阿里巴巴 java 開發手冊中推薦的 ThreadLocal 的用法:

import java.text.DateFormat;
import java.text.SimpleDateFormat;
 
public class DateUtils {
    public static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };
}

然後再要用到 DateFormat 對象的地方,這樣調用:

DateUtils.df.get().format(new Date());
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/eYw8cSAysvo6dE__Nc82eg