Java多線(xiàn)程 ThreadLocal原理解析
1、什么是ThreadLocal變量
ThreadLoal 變量,線(xiàn)程局部變量,同一個(gè) ThreadLocal 所包含的對(duì)象,在不同的 Thread 中有不同的副本。
這里有幾點(diǎn)需要注意:
- 因?yàn)槊總€(gè)
Thread內(nèi)有自己的實(shí)例副本,且該副本只能由當(dāng)前Thread使用。這是也是ThreadLocal命名的由來(lái)。 - 既然每個(gè)
Thread有自己的實(shí)例副本,且其它Thread不可訪(fǎng)問(wèn),那就不存在多線(xiàn)程間共享的問(wèn)題。
ThreadLocal 提供了線(xiàn)程本地的實(shí)例。它與普通變量的區(qū)別在于,每個(gè)使用該變量的線(xiàn)程都會(huì)初始化一個(gè)完全獨(dú)立的實(shí)例副本。ThreadLocal 變量通常被private static修飾。當(dāng)一個(gè)線(xiàn)程結(jié)束時(shí),它所使用的所有 ThreadLocal 相對(duì)的實(shí)例副本都可被回收。
總的來(lái)說(shuō),ThreadLocal 適用于每個(gè)線(xiàn)程需要自己獨(dú)立的實(shí)例且該實(shí)例需要在多個(gè)方法中被使用,也即變量在線(xiàn)程間隔離而在方法或類(lèi)間共享的場(chǎng)景。
2、ThreadLocal實(shí)現(xiàn)原理
首先 ThreadLocal 是一個(gè)泛型類(lèi),保證可以接受任何類(lèi)型的對(duì)象。
因?yàn)橐粋€(gè)線(xiàn)程內(nèi)可以存在多個(gè) ThreadLocal 對(duì)象,所以其實(shí)是 ThreadLocal 內(nèi)部維護(hù)了一個(gè) Map ,這個(gè) Map 不是直接使用的 HashMap ,而是 ThreadLocal 實(shí)現(xiàn)的一個(gè)叫做 ThreadLocalMap 的靜態(tài)內(nèi)部類(lèi)。而我們使用的 get() 、set() 方法其實(shí)都是調(diào)用了這個(gè)ThreadLocalMap類(lèi)對(duì)應(yīng)的 get() 、set() 方法。
例如下面的 set 方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
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();
}
createMap方法:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap是個(gè)靜態(tài)的內(nèi)部類(lèi):
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;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
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);
}
...
}
最終的變量是放在了當(dāng)前線(xiàn)程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解為只是ThreadLocalMap的封裝,傳遞了變量值。
3、內(nèi)存泄漏問(wèn)題
實(shí)際上 ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱引用,弱引用的特點(diǎn)是,如果這個(gè)對(duì)象只存在弱引用,那么在下一次垃圾回收的時(shí)候必然會(huì)被清理掉。
所以如果 ThreadLocal 沒(méi)有被外部強(qiáng)引用的情況下,在垃圾回收的時(shí)候會(huì)被清理掉的,這樣一來(lái) ThreadLocalMap中使用這個(gè) ThreadLocal 的 key 也會(huì)被清理掉。但是,value 是強(qiáng)引用,不會(huì)被清理,這樣一來(lái)就會(huì)出現(xiàn) key 為 null 的 value。
ThreadLocalMap實(shí)現(xiàn)中已經(jīng)考慮了這種情況,在調(diào)用 set() 、get() 、remove() 方法的時(shí)候,會(huì)清理掉 key 為 null 的記錄。如果說(shuō)會(huì)出現(xiàn)內(nèi)存泄漏,那只有在出現(xiàn)了 key 為 null 的記錄后,沒(méi)有手動(dòng)調(diào)用 remove() 方法,并且之后也不再調(diào)用 get() 、set() 、remove() 方法的情況下。
4、使用場(chǎng)景
如上文所述,ThreadLocal 適用于如下兩種場(chǎng)景
每個(gè)線(xiàn)程需要有自己?jiǎn)为?dú)的實(shí)例
實(shí)例需要在多個(gè)方法中共享,但不希望被多線(xiàn)程共享
對(duì)于第一點(diǎn),每個(gè)線(xiàn)程擁有自己實(shí)例,實(shí)現(xiàn)它的方式很多。例如可以在線(xiàn)程內(nèi)部構(gòu)建一個(gè)單獨(dú)的實(shí)例。ThreadLoca 可以以非常方便的形式滿(mǎn)足該需求。
對(duì)于第二點(diǎn),可以在滿(mǎn)足第一點(diǎn)(每個(gè)線(xiàn)程有自己的實(shí)例)的條件下,通過(guò)方法間引用傳遞的形式實(shí)現(xiàn)。ThreadLocal 使得代碼耦合度更低,且實(shí)現(xiàn)更優(yōu)雅。
1)存儲(chǔ)用戶(hù)Session
一個(gè)簡(jiǎn)單的用ThreadLocal來(lái)存儲(chǔ)Session的例子:
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;
}
2)解決線(xiàn)程安全的問(wèn)題
比如Java7中的SimpleDateFormat不是線(xiàn)程安全的,可以用ThreadLocal來(lái)解決這個(gè)問(wèn)題:
public class DateUtil {
private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static String formatDate(Date date) {
return format1.get().format(date);
}
}
這里的DateUtil.formatDate()就是線(xiàn)程安全的了。(Java8里的 [java.time.format.DateTimeFormatter]
(http://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) 是線(xiàn)程安全的,Joda time里的DateTimeFormat也是線(xiàn)程安全的)。
public class Context {
private String name;
private String cardId;
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
public class ExecutionTask implements Runnable {
private QueryFromDBAction queryAction = new QueryFromDBAction();
private QueryFromHttpAction httpAction = new QueryFromHttpAction();
@Override
public void run() {
final Context context = new Context();
queryAction.execute(context);
System.out.println("The name query successful");
httpAction.execute(context);
System.out.println("The cardId query successful");
System.out.println("The Name is " + context.getName() + " and CardId " + context.getCardId());
}
}
public class QueryFromDBAction {
public void execute(Context context) {
try {
Thread.sleep(1000L);
String name = "Jack " + Thread.currentThread().getName();
context.setName(name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void execute(Context context) {
String name = context.getName();
String cardId = getCardId(name);
context.setCardId(cardId);
}
private String getCardId(String name) {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "444555" + Thread.currentThread().getId();
}
}
public class ContextTest {
public static void main(String[] args) {
IntStream.range(1, 5)
.forEach(i ->
new Thread(new ExecutionTask()).start()
);
}
}
The name query successful
The name query successful
The name query successful
The name query successful
The cardId query successful
The Name is Jack Thread-0 and CardId 44455511
The cardId query successful
The Name is Jack Thread-1 and CardId 44455512
The cardId query successful
The Name is Jack Thread-2 and CardId 44455513
The cardId query successful
The Name is Jack Thread-3 and CardId 44455514
問(wèn)題:需要在每個(gè)調(diào)用Context的方法中傳入進(jìn)去
public void execute(Context context) {
}
3)使用ThreadLocal重新設(shè)計(jì)一個(gè)上下文設(shè)計(jì)模式
public final class ActionContext {
private static final ThreadLocal<Context> threadLocal = new ThreadLocal() {
@Override
protected Object initialValue() {
return new Context();
}
};
public static ActionContext getActionContext() {
return ContextHolder.actionContext;
}
public Context getContext() {
return threadLocal.get();
}
private static class ContextHolder {
private final static ActionContext actionContext = new ActionContext();
}
}
public class Context {
private String name;
private String cardId;
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
public class ExecutionTask implements Runnable {
private QueryFromDBAction queryAction = new QueryFromDBAction();
private QueryFromHttpAction httpAction = new QueryFromHttpAction();
@Override
public void run() {
queryAction.execute();
System.out.println("The name query successful");
httpAction.execute();
System.out.println("The cardId query successful");
final Context context = ActionContext.getActionContext().getContext();
System.out.println("The Name is " + context.getName() + " and CardId " + context.getCardId());
}
}
public class QueryFromDBAction {
public void execute() {
try {
Thread.sleep(1000L);
String name = "Jack " + Thread.currentThread().getName();
ActionContext.getActionContext().getContext().setName(name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class QueryFromHttpAction {
public void execute() {
Context context = ActionContext.getActionContext().getContext();
String name = context.getName();
String cardId = getCardId(name);
context.setCardId(cardId);
}
private String getCardId(String name) {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "444555" + Thread.currentThread().getId();
}
}
public class ContextTest {
public static void main(String[] args) {
IntStream.range(1, 5)
.forEach(i ->
new Thread(new ExecutionTask()).start()
);
}
}
The name query successful
The name query successful
The name query successful
The name query successful
The cardId query successful
The Name is Jack Thread-3 and CardId 44455514
The cardId query successful
The cardId query successful
The Name is Jack Thread-0 and CardId 44455511
The cardId query successful
The Name is Jack Thread-2 and CardId 44455513
The Name is Jack Thread-1 and CardId 44455512
這樣寫(xiě) 執(zhí)行過(guò)程中不會(huì)看到context的定義和聲明
注意:在使用之前記得將上個(gè)線(xiàn)程中context舊值清除調(diào),否則會(huì)重復(fù)調(diào)用(比如線(xiàn)程池操作)
4)ThreadLocal注意事項(xiàng)
臟數(shù)據(jù)
線(xiàn)程復(fù)用會(huì)產(chǎn)生臟數(shù)據(jù)。由于結(jié)程池會(huì)重用Thread對(duì)象,那么與Thread綁定的類(lèi)的靜態(tài)屬性ThreadLocal變量也會(huì)被重用。如果在實(shí)現(xiàn)的線(xiàn)程run()方法體中不顯式地調(diào)用remove() 清理與線(xiàn)程相關(guān)的ThreadLocal信息,那么倘若下一個(gè)結(jié)程不調(diào)用set() 設(shè)置初始值,就可能get() 到重用的線(xiàn)程信息,包括 ThreadLocal所關(guān)聯(lián)的線(xiàn)程對(duì)象的value值。
內(nèi)存泄漏
通常我們會(huì)使用使用static關(guān)鍵字來(lái)修飾ThreadLocal(這也是在源碼注釋中所推薦的)。在此場(chǎng)景下,其生命周期就不會(huì)隨著線(xiàn)程結(jié)束而結(jié)束,寄希望于ThreadLocal對(duì)象失去引用后,觸發(fā)弱引用機(jī)制來(lái)回收Entry的Value就不現(xiàn)實(shí)了。如果不進(jìn)行remove() 操作,那么這個(gè)線(xiàn)程執(zhí)行完成后,通過(guò)ThreadLocal對(duì)象持有的對(duì)象是不會(huì)被釋放的。
以上兩個(gè)問(wèn)題的解決辦法很簡(jiǎn)單,就是在每次用完ThreadLocal時(shí), 必須要及時(shí)調(diào)用 remove()方法清理。
父子線(xiàn)程共享線(xiàn)程變量
很多場(chǎng)景下通過(guò)ThreadLocal來(lái)透?jìng)魅稚舷挛?,?huì)發(fā)現(xiàn)子線(xiàn)程的value和主線(xiàn)程不一致。比如用ThreadLocal來(lái)存儲(chǔ)監(jiān)控系統(tǒng)的某個(gè)標(biāo)記位,暫且命名為traceId。某次請(qǐng)求下所有的traceld都是一致的,以獲得可以統(tǒng)一解析的日志文件。但在實(shí)際開(kāi)發(fā)過(guò)程中,發(fā)現(xiàn)子線(xiàn)程里的traceld為null,跟主線(xiàn)程的并不一致。這就需要使用InheritableThreadLocal來(lái)解決父子線(xiàn)程之間共享線(xiàn)程變量的問(wèn)題,使整個(gè)連接過(guò)程中的traceId一致。
到此這篇關(guān)于Java多線(xiàn)程 ThreadLocal原理解析的文章就介紹到這了,更多相關(guān)Java多線(xiàn)程 ThreadLocal內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MyBatis版本升級(jí)導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓?wèn)題復(fù)盤(pán)
這篇文章主要介紹了MyBatis版本升級(jí)導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓?wèn)題復(fù)盤(pán),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
MybatisPlus出現(xiàn)Error attempting to get col
本文重點(diǎn)分析使用@EnumValue注解轉(zhuǎn)換時(shí)遇到的一下錯(cuò)誤原因,及解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-11-11
SpringMVC的Body參數(shù)攔截的問(wèn)題
SpringMVC對(duì)出參和入?yún)⒂蟹浅S押玫耐卣怪С?方便你對(duì)數(shù)據(jù)的輸入和輸出有更大的執(zhí)行權(quán),我們?nèi)绾瓮ㄟ^(guò)SpringMVC定義的結(jié)果做一系列處理呢,需要的朋友可以參考下2018-06-06
Java Map 通過(guò) key 或者 value 過(guò)濾的實(shí)例代碼
這篇文章主要介紹了Java Map 通過(guò) key 或者 value 過(guò)濾的實(shí)例代碼,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-06-06
MyBatisPlus中事務(wù)處理的實(shí)現(xiàn)
本文主要介紹了MyBatisPlus中事務(wù)處理的實(shí)現(xiàn),包括事務(wù)的開(kāi)啟、提交、回滾等操作,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-07-07

