ThreadLocal 概述
概述
- ThreadLocal類用來提供線程內(nèi)部的局部變量,不同的線程之間不會(huì)相互干擾
- 這種變量在多線程環(huán)境下訪問(通過get和set方法訪問)時(shí)能保證各個(gè)線程的變量相對(duì)獨(dú)立于其他線程內(nèi)的變量
- 在線程的生命周期內(nèi)起作用实苞,可以減少同一個(gè)線程內(nèi)多個(gè)函數(shù)或組件之間一些公共變量傳遞的復(fù)雜度
常用方法
方法名 | 描述 |
---|---|
ThreadLocal() | 創(chuàng)建ThreadLocal對(duì)象 |
public void set( T value) | 設(shè)置當(dāng)前線程綁定的局部變量 |
public T get() | 獲取當(dāng)前線程綁定的局部變量 |
public T remove() | 移除當(dāng)前線程綁定的局部變量,該方法可以幫助JVM進(jìn)行GC |
protected T initialValue() | 返回當(dāng)前線程局部變量的初始值 |
為什么要用ThreadLocal?
但在并發(fā)的場(chǎng)景中丈莺,如果有多個(gè)線程同時(shí)修改公共變量瓮具,可能會(huì)出現(xiàn)線程安全問題定硝,即該變量最終結(jié)果可能出現(xiàn)異常晨雳。
為了解決線程安全問題,JDK出現(xiàn)了很多技術(shù)手段紧卒,比如:使用synchronized或Lock侥衬,給訪問公共資源的代碼上鎖,保證了代碼的原子性跑芳。
但在高并發(fā)的場(chǎng)景中轴总,如果多個(gè)線程同時(shí)競(jìng)爭(zhēng)一把鎖,這時(shí)會(huì)存在大量的鎖等待博个,可能會(huì)浪費(fèi)很多時(shí)間怀樟,讓系統(tǒng)的響應(yīng)時(shí)間一下子變慢。
因此盆佣,JDK還提供了另外一種用空間換時(shí)間的新思路:ThreadLocal往堡。
它的核心思想是:共享變量在每個(gè)線程都有一個(gè)副本,每個(gè)線程操作的都是自己的副本共耍,對(duì)另外的線程沒有影響虑灰。
ThreadLocal的原理是什么?
set(T value)和get()方法
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
//獲取當(dāng)前線程
Thread t = Thread.currentThread();
//獲取當(dāng)前線程的成員變量ThreadLocalMap對(duì)象
ThreadLocalMap map = getMap(t);
if (map != null) {
//根據(jù)threadLocal對(duì)象從map中獲取Entry對(duì)象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//獲取保存的數(shù)據(jù)
T result = (T)e.value;
return result;
}
}
//初始化數(shù)據(jù)
return setInitialValue();
}
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
//獲取當(dāng)前線程
Thread t = Thread.currentThread();
//獲取當(dāng)前線程的成員變量ThreadLocalMap對(duì)象
ThreadLocalMap map = getMap(t);
//如果map不為空
if (map != null)
//將值設(shè)置到map中痹兜,key是this瘩缆,即threadLocal對(duì)象,value是傳入的value值
map.set(this, value);
else
//如果map為空佃蚜,則需要?jiǎng)?chuàng)建新的map對(duì)象
createMap(t, value);
}
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
//獲取要初始化的數(shù)據(jù)
T value = initialValue();
//獲取當(dāng)前線程
Thread t = Thread.currentThread();
//獲取當(dāng)前線程的成員變量ThreadLocalMap對(duì)象
ThreadLocalMap map = getMap(t);
//如果map不為空
if (map != null)
//將初始值設(shè)置到map中,key是this着绊,即threadLocal對(duì)象谐算,value是初始值
map.set(this, value);
else
//如果map為空,則需要?jiǎng)?chuàng)建新的map對(duì)象
createMap(t, value);
return value;
}
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocal的get方法归露、set方法和setInitialValue方法洲脂,其實(shí)最終操作的都是ThreadLocalMap類中的數(shù)據(jù)。
每個(gè)線程中都有一個(gè)ThreadLocalMap數(shù)據(jù)結(jié)構(gòu),當(dāng)執(zhí)行set方法時(shí)恐锦,其值是保存在當(dāng)前線程的threadLocals變量中往果,當(dāng)執(zhí)行g(shù)et方法中,是從當(dāng)前線程的threadLocals變量獲取一铅。
ThreadLoalMap
/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*/
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
...
}
ThreadLocalMap里面包含一個(gè)靜態(tài)的內(nèi)部類Entry陕贮,該類繼承于WeakReference類,說明Entry是一個(gè)弱引用潘飘。
在ThreadLoalMap中肮之,也是初始化一個(gè)大小16的Entry數(shù)組,Entry對(duì)象用來保存每一個(gè)key-value鍵值對(duì)卜录,只不過這里的key永遠(yuǎn)都是ThreadLocal對(duì)象戈擒,通過ThreadLocal對(duì)象的set方法,結(jié)果把ThreadLocal對(duì)象自己當(dāng)做key艰毒,放進(jìn)了ThreadLoalMap中筐高。
從上圖中看出,在每個(gè)Thread類中丑瞧,都有一個(gè)ThreadLocalMap的成員變量柑土,該變量包含了一個(gè)Entry數(shù)組,該數(shù)組真正保存了ThreadLocal類set的數(shù)據(jù)嗦篱。
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
}
Entry是由threadLocal和value組成冰单,其中threadLocal對(duì)象是弱引用,在GC的時(shí)候灸促,會(huì)被自動(dòng)回收诫欠。而value就是ThreadLocal類set的數(shù)據(jù)。
這里需要注意的是浴栽,ThreadLoalMap的Entry是繼承WeakReference荒叼,和HashMap很大的區(qū)別是,Entry中沒有next字段典鸡,所以就不存在鏈表的情況了被廓。
為什么用ThreadLocal做key?
如果在你的應(yīng)用中萝玷,一個(gè)線程中只使用了一個(gè)ThreadLocal對(duì)象嫁乘,那么使用Thread做key也未嘗不可。
@Service
public class ThreadLocalService {
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
}
但實(shí)際情況中球碉,你的應(yīng)用蜓斧,一個(gè)線程中很有可能不只使用了一個(gè)ThreadLocal對(duì)象。這時(shí)使用Thread做key不就有問題睁冬?
@Service
public class ThreadLocalService {
private static final ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
private static final ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
}
一個(gè)使用類挎春,有兩個(gè)共享變量,也就是說用了兩個(gè)ThreadLocal成員變量的話。如果用線程id作為ThreadLocalMap的key直奋,怎么區(qū)分哪個(gè)ThreadLocal成員變量呢能庆?因此還是需要使用ThreadLocal作為Key來使用。每個(gè)ThreadLocal對(duì)象脚线,都可以由threadLocalHashCode屬性唯一區(qū)分的搁胆,每一個(gè)ThreadLocal對(duì)象都可以由這個(gè)對(duì)象的名字唯一區(qū)分。
Entry的key為什么設(shè)計(jì)成弱引用?
前面說過殉挽,Entry的key丰涉,傳入的是ThreadLocal對(duì)象,使用了WeakReference對(duì)象斯碌,即被設(shè)計(jì)成了弱引用一死。
那么,為什么要這樣設(shè)計(jì)呢傻唾?
我們先來回憶一下四種引用:
強(qiáng)引用:我們平時(shí)new了一個(gè)對(duì)象就是強(qiáng)引用投慈,例如 Object obj = new Object();即使在內(nèi)存不足的情況下,JVM寧愿拋出OutOfMemory錯(cuò)誤也不會(huì)回收這種對(duì)象冠骄。
軟引用:如果一個(gè)對(duì)象只具有軟引用伪煤,則內(nèi)存空間足夠,垃圾回收器就不會(huì)回收它凛辣;如果內(nèi)存空間不足了抱既,就會(huì)回收這些對(duì)象的內(nèi)存。
弱引用:具有弱引用的對(duì)象擁有更短暫的生命周期扁誓。如果一個(gè)對(duì)象只有弱引用存在了防泵,則下次GC將會(huì)回收掉該對(duì)象(不管當(dāng)前內(nèi)存空間足夠與否)。
虛引用:如果一個(gè)對(duì)象僅持有虛引用蝗敢,那么它就和沒有任何引用一樣捷泞,在任何時(shí)候都可能被垃圾回收器回收。虛引用主要用來跟蹤對(duì)象被垃圾回收器回收的活動(dòng)寿谴。
假如key對(duì)ThreadLocal對(duì)象的弱引用锁右,改為強(qiáng)引用。
我們都知道ThreadLocal變量對(duì)ThreadLocal對(duì)象是有強(qiáng)引用存在的讶泰。
即使ThreadLocal變量生命周期完了咏瑟,設(shè)置成null了,但由于key對(duì)ThreadLocal對(duì)象還是強(qiáng)引用痪署。
就會(huì)存在這樣的強(qiáng)引用鏈:Thread變量 -> Thread對(duì)象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal對(duì)象响蕴。
當(dāng)ThreadLocal的對(duì)象被回收了,但是ThreadLocalMap還持有ThreadLocal對(duì)象的強(qiáng)引用的話那么惠桃,ThreadLocal對(duì)象和ThreadLocalMap都將不會(huì)被GC回收,于是產(chǎn)生了內(nèi)存泄露問題。
如果key是弱引用辜王,當(dāng)ThreadLocal變量指向null之后劈狐,在GC做垃圾清理的時(shí)候,key會(huì)被自動(dòng)回收呐馆,其值也被設(shè)置成null肥缔。
如何避免內(nèi)存泄露?
ThreadLocal的get()、set()可能會(huì)清除ThreadLocalMap中key為null的Entry對(duì)象汹来,這樣對(duì)應(yīng)的value就沒有GC Roots可達(dá)了续膳,下次GC的時(shí)候就可以被回收,當(dāng)然如果調(diào)用remove方法收班,肯定會(huì)刪除對(duì)應(yīng)的Entry對(duì)象坟岔。
如果使用ThreadLocal的set方法之后,沒有顯示的調(diào)用remove方法摔桦,就有可能發(fā)生內(nèi)存泄露社付,所以養(yǎng)成良好的編程習(xí)慣十分重要,使用完ThreadLocal之后邻耕,記得調(diào)用remove方法鸥咖。
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("占小狼");
// 其它業(yè)務(wù)邏輯
} finally {
localName.remove();
}
假如ThreadLocalMap中存在很多key為null的Entry,但后面的程序兄世,一直都沒有調(diào)用過有效的ThreadLocal的get啼辣、set或remove方法。
那么御滩,Entry的value值一直都沒被清空鸥拧。
所以會(huì)存在這樣一條強(qiáng)引用鏈:Thread變量 -> Thread對(duì)象 -> ThreadLocalMap -> Entry -> value -> Object。
其結(jié)果就是:Entry和ThreadLocalMap將會(huì)長(zhǎng)期存在下去艾恼,會(huì)導(dǎo)致內(nèi)存泄露住涉。
實(shí)際上,我們的內(nèi)存泄漏的根本原因是钠绍,不再被使用的Entry舆声,沒有從線程的ThreadLocalMap中刪除。一般刪除不再使用的Entry有這兩種方式:
一種就是柳爽,使用完ThreadLocal媳握,手動(dòng)調(diào)用remove(),把Entry從ThreadLocalMap中刪除
另外一種方式就是:ThreadLocalMap的自動(dòng)清除機(jī)制去清除過期Entry.(ThreadLocalMap的get(),set()時(shí)都會(huì)觸發(fā)對(duì)過期Entry的清除)
ThreadLocal是如何解決hash沖突的呢磷脯?
我們看看getEntry是怎么做的:
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal<?> key) {
//通過hash算法獲取下標(biāo)值
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//如果下標(biāo)位置上的key正好是我們所需要尋找的key
if (e != null && e.get() == key)
//說明找到數(shù)據(jù)了蛾找,直接返回
return e;
else
//說明出現(xiàn)hash沖突了,繼續(xù)往后找
return getEntryAfterMiss(key, i, e);
}
再看看getEntryAfterMiss方法:
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//判斷Entry對(duì)象如果不為空赵誓,則一直循環(huán)
while (e != null) {
ThreadLocal<?> k = e.get();
//如果當(dāng)前Entry的key正好是我們所需要尋找的key
if (k == key)
//說明這次真的找到數(shù)據(jù)了
return e;
if (k == null)
//如果key為空打毛,則清理臟數(shù)據(jù)
expungeStaleEntry(i);
else
//如果還是沒找到數(shù)據(jù)柿赊,則繼續(xù)往后找
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
關(guān)鍵看看nextIndex方法:
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
當(dāng)通過hash算法計(jì)算出的下標(biāo)小于數(shù)組大小,則將下標(biāo)值加1幻枉。否則碰声,即下標(biāo)大于等于數(shù)組大小,下標(biāo)變成0了熬甫。下標(biāo)變成0之后胰挑,則循環(huán)一次,下標(biāo)又變成1椿肩。瞻颂。。
ThreadLocal是如何定位數(shù)據(jù)的郑象?
在ThreadLocal的get贡这、set、remove方法中都有這樣一行代碼:
int i = key.threadLocalHashCode & (len-1);
通過key的hashCode值與數(shù)組的長(zhǎng)度減1扣唱。其中key就是ThreadLocal對(duì)象藕坯,與數(shù)組的長(zhǎng)度減1,相當(dāng)于除以數(shù)組的長(zhǎng)度減1噪沙,然后取模炼彪。
ThreadLocal從數(shù)組中找數(shù)據(jù)的過程大致是這樣的:
1.通過key的hashCode取余計(jì)算出一個(gè)下標(biāo)。
2.通過下標(biāo)正歼,在數(shù)組中定位具體Entry辐马,如果key正好是我們所需要的key,說明找到了局义,則直接返回?cái)?shù)據(jù)喜爷。
3.如果第2步?jīng)]有找到我們想要的數(shù)據(jù),則從數(shù)組的下標(biāo)位置萄唇,繼續(xù)往后面找檩帐。
4.如果第3步中找key的正好是我們所需要的key,說明找到了另萤,則直接返回?cái)?shù)據(jù)湃密。
5.如果還是沒有找到數(shù)據(jù),再繼續(xù)往后面找四敞。如果找到最后一個(gè)位置泛源,還是沒有找到數(shù)據(jù),則再?gòu)念^忿危,即下標(biāo)為0的位置达箍,繼續(xù)從前往后找數(shù)據(jù)。
6.直到找到第一個(gè)Entry為空為止铺厨。
父子線程如何共享數(shù)據(jù)缎玫?
在Thread類中除了成員變量threadLocals之外硬纤,還有另一個(gè)成員變量:inheritableThreadLocals
。
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
Thread類中的init方法
/**
* Initializes a Thread.
*
* @param g the Thread group
* @param target the object whose run() method gets called
* @param name the name of the new Thread
* @param stackSize the desired stack size for the new thread, or
* zero to indicate that this parameter is to be ignored.
* @param acc the AccessControlContext to inherit, or
* AccessController.getContext() if null
* @param inheritThreadLocals if {@code true}, inherit initial values for
* inheritable thread-locals from the constructing thread
*/
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
/**
* Factory method to create map of inherited thread locals.
* Designed to be called only from Thread constructor.
*
* @param parentMap the map associated with parent thread
* @return a map containing the parent's inheritable bindings
*/
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
}
可以發(fā)現(xiàn)碘梢,當(dāng)parent的inheritableThreadLocals不為null時(shí)咬摇,就會(huì)將parent的inheritableThreadLocals,賦值給前線程的inheritableThreadLocals煞躬。說白了,就是如果當(dāng)前線程的inheritableThreadLocals不為null逸邦,就從父線程哪里拷貝過來一個(gè)過來恩沛,類似于另外一個(gè)ThreadLocal,數(shù)據(jù)從父線程那里來的缕减。
public class ThreadLocalTest {
public static void main(String[] args) {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
threadLocal.set(6);
System.out.println("父線程獲取數(shù)據(jù):" + threadLocal.get());
new Thread(() -> {
System.out.println("子線程獲取數(shù)據(jù):" + threadLocal.get());
}).start();
}
}
父線程獲取數(shù)據(jù):6
子線程獲取數(shù)據(jù):null
public class ThreadLocalTest {
public static void main(String[] args) {
InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
threadLocal.set(6);
System.out.println("父線程獲取數(shù)據(jù):" + threadLocal.get());
new Thread(() -> {
System.out.println("子線程獲取數(shù)據(jù):" + threadLocal.get());
}).start();
}
}
父線程獲取數(shù)據(jù):6
子線程獲取數(shù)據(jù):6
ThreadLocal有哪些用途雷客?
1.在spring事務(wù)中,保證一個(gè)線程下桥狡,一個(gè)事務(wù)的多個(gè)操作拿到的是一個(gè)Connection搅裙。
2.在hiberate中管理session。
3.在JDK8之前裹芝,為了解決SimpleDateFormat的線程安全問題部逮。
4.獲取當(dāng)前登錄用戶上下文。
5.臨時(shí)保存權(quán)限數(shù)據(jù)嫂易。
6.使用MDC保存日志信息兄朋。
ThreadLocal的應(yīng)用場(chǎng)景和使用注意點(diǎn)
ThreadLocal的很重要一個(gè)注意點(diǎn),就是使用完怜械,要手動(dòng)調(diào)用remove()颅和。
而ThreadLocal的應(yīng)用場(chǎng)景主要有以下這幾種:
使用日期工具類,當(dāng)用到SimpleDateFormat缕允,使用ThreadLocal保證線性安全
全局存儲(chǔ)用戶信息(用戶信息存入ThreadLocal峡扩,那么當(dāng)前線程在任何地方需要時(shí),都可以使用)
保證同一個(gè)線程障本,獲取的數(shù)據(jù)庫連接Connection是同一個(gè)教届,使用ThreadLocal來解決線程安全的問題
使用MDC保存日志信息。