史上最全 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();
}
-
首先獲取當前線程對象 t, 然後從線程 t 中獲取到 ThreadLocalMap 的成員屬性 threadLocals
-
如果當前線程的 threadLocals 已經初始化 (即不爲 null) 並且存在以當前 ThreadLocal 對象爲 Key 的值, 則直接返回當前線程要獲取的對象 (本例中爲 Connection);
-
如果當前線程的 threadLocals 已經初始化 (即不爲 null) 但是不存在以當前 ThreadLocal 對象爲 Key 的的對象, 那麼重新創建一個 Connection 對象, 並且添加到當前線程的 threadLocals Map 中, 並返回
-
如果當前線程的 threadLocals 屬性還沒有被初始化, 則重新創建一個 ThreadLocalMap 對象, 並且創建一個 Connection 對象並添加到 ThreadLocalMap 對象中並返回。
如果存在則直接返回很好理解, 那麼對於如何初始化的代碼又是怎樣的呢?
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;
}
-
首先調用上面寫的重載過後的 initialValue 方法
-
繼續查看當前線程的 threadLocals 是不是空的, 如果 ThreadLocalMap 已被初始化, 那麼直接將產生的對象添加到 ThreadLocalMap 中, 如果沒有初始化, 則創建並添加對象到其中;
同時, 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 有點不一樣
-
它沒有實現 Map 接口;
-
它沒有 public 的方法, 最多有一個 default 的構造方法, 因爲這個 ThreadLocalMap 的方法僅僅在 ThreadLocal 類中調用, 屬於靜態內部類
-
ThreadLocalMap 的 Entry 實現繼承了 WeakReference<ThreadLocal<?>>
-
該方法僅僅用了一個 Entry 數組來存儲 Key, Value; Entry 並不是鏈表形式, 而是每個 bucket 裏面僅僅放一個 Entry;
要了解 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();
}
先進行簡單的分析, 對該代碼表層意思進行解讀:
-
看下當前 threadLocal 的在數組中的索引位置 比如: i = 2,看 i = 2 位置上面的元素 (Entry) 的 Key 是否等於 threadLocal 這個 Key, 如果等於就很好說了, 直接將該位置上面的 Entry 的 Value 替換成最新的就可以了;
-
如果當前位置上面的 Entry 的 Key 爲空, 說明 ThreadLocal 對象已經被回收了, 那麼就調用 replaceStaleEntry
-
如果清理完無用條目 (ThreadLocal 被回收的條目)、並且數組中的數據大小 > 閾值的時候對當前的 Table 進行重新哈希 所以, 該 HashMap 是處理衝突檢測的機制是向後移位, 清除過期條目 最終找到合適的位置;
瞭解完 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,基本步驟如下:
-
在多線程的類 (如 ThreadDemo 類) 中,創建一個 ThreadLocal 對象 threadXxx,用來保存線程間需要隔離處理的對象 xxx。
-
在 ThreadDemo 類中,創建一個獲取要隔離訪問的數據的方法 getXxx(),在方法中判斷,若 ThreadLocal 對象爲 null 時候,應該 new() 一個隔離訪問類型的對象,並強制轉換爲要應用的類型。
-
在 ThreadDemo 類的 run() 方法中,通過調用 getXxx() 方法獲取要操作的數據,這樣可以保證每個線程對應一個數據對象,在任何時刻都操作的是這個對象。
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