前言
當(dāng)多線程訪問共享可變數(shù)據(jù)時,涉及到線程間同步的問題,并不是所有時候,都要用到共享數(shù)據(jù),所以就需要線程封閉出場了。本文中的ThreadLocal就起到了線程封閉的作用。它提供了線程內(nèi)的局部變量,不同線程之間不會相互干擾,這種變量在線程的生命周期內(nèi)起作用,減少了同一個線程內(nèi)多個函數(shù)或組件之間一些公共變量傳遞的復(fù)雜度。
特點
通俗的說ThreadLocal具備三個特性:
- 線程并發(fā): 在多線程并發(fā)的場景下使用
- 傳遞數(shù)據(jù): 我們通過ThreadLocal在同一線程,不同組件中傳遞公共變量
- 線程隔離: 每個線程的變量都是獨立的,不會相互影響
ThreadLocal用于保存每個線程獨享的對象,為每個線程創(chuàng)建一個副本,這樣每個線程都可以修改自己所擁有的副本,而不會影響其他線程的副本,從而確保了線程安全??偠灾琓hreadLocal的核心作用就是將變量在線程中隔離。
ThreadLocal的基本使用
常用方法
我們先看一下它的類圖中的方法:

| 方法申明 | 描述 |
|---|---|
| ThreadLocal() | 創(chuàng)建ThreadLocal對象 |
| public void set(T value) | 設(shè)置當(dāng)前線程綁定的局部變量 |
| public T get() | 獲取當(dāng)前線程綁定的局部變量 |
| public void remove() | 移除當(dāng)前線程綁定的局部變量 |
例子
線程之間的變量非獨立
/**
* Description
* 線程隔離例子
* 在多線程并發(fā)場景下,每個線程中的變量都是相互獨立的
* 線程A:設(shè)置(變量1) 獲?。ㄗ兞?)
* 線程B:設(shè)置(變量2) 獲?。ㄗ兞?)
* Date 2020/6/3 22:03
* Created by kwz
*/
public class ThreadLocalExample1 {
@Getter
@Setter
private String content;
public static void main(String[] args) {
ThreadLocalExample1 example = new ThreadLocalExample1();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(
() -> {
//每個線程存一個變量,過一會兒取這個變量
example.setContent(Thread.currentThread().getName() + "的數(shù)據(jù)");
System.out.println("----------------------------------------");
System.out.println(Thread.currentThread().getName() + "---->" + example.getContent());
}
);
//0->4一共有5個線程
thread.setName("線程" + i);
thread.start();
}
}
}
我們看它的控制臺輸出結(jié)果:
----------------------------------------
----------------------------------------
----------------------------------------
線程2---->線程2的數(shù)據(jù)
線程0---->線程2的數(shù)據(jù)
----------------------------------------
線程4---->線程4的數(shù)據(jù)
----------------------------------------
線程1---->線程2的數(shù)據(jù)
線程3---->線程4的數(shù)據(jù)
我們可以看到一個線程取到了其他線程的變量
線程之間的變量相互獨立
/**
* Description(利用ThreadLocal)
* 線程隔離例子
* 在多線程并發(fā)場景下,每個線程中的變量都是相互獨立的
* 線程A:設(shè)置(變量1) 獲?。ㄗ兞?)
* 線程B:設(shè)置(變量2) 獲?。ㄗ兞?)
*
* ThreadLocal
* 1.set(): 將變量綁定到
* Date 2020/6/3 22:03
* Created by kwz
*/
public class ThreadLocalExample2 {
ThreadLocal<String> t1 = new ThreadLocal<>();
private String content;
private String getContent(){
String s = t1.get();
return s;
}
private void setContent(String content){
//變量content綁定到當(dāng)前線程
t1.set(content);
}
public static void main(String[] args) {
ThreadLocalExample2 example = new ThreadLocalExample2();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(
() -> {
//每個線程存一個變量,過一會兒取這個變量
example.setContent(Thread.currentThread().getName() + "的數(shù)據(jù)");
System.out.println("----------------------------------------");
System.out.println(Thread.currentThread().getName() + "---->" + example.getContent());
}
);
//0->4一共有5個線程
thread.setName("線程" + i);
thread.start();
}
}
}
我們看它的控制臺輸出結(jié)果:
----------------------------------------
----------------------------------------
----------------------------------------
線程4---->線程4的數(shù)據(jù)
----------------------------------------
線程1---->線程1的數(shù)據(jù)
線程2---->線程2的數(shù)據(jù)
----------------------------------------
線程3---->線程3的數(shù)據(jù)
線程0---->線程0的數(shù)據(jù)
通過兩個例子可以看到,通過引入ThreadLocal可以做到不同線程之前訪問變量的相互獨立性。
ThreadLocal和Synchronized關(guān)鍵字
Synchronized的同步方式
線程之間共享變量的相互隔離,我們首先想到的其實是Synchronized,我們通過下面的Synchronized也能夠?qū)崿F(xiàn)
public class ThreadLocalExample3 {
@Getter
@Setter
private String content;
public static void main(String[] args) {
ThreadLocalExample3 example = new ThreadLocalExample3();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(
() -> {
//每個線程存一個變量,過一會兒取這個變量
synchronized (ThreadLocalExample3.class) {
example.setContent(Thread.currentThread().getName() + "的數(shù)據(jù)");
System.out.println("----------------------------------------");
System.out.println(Thread.currentThread().getName() + "---->" + example.getContent());
}
}
);
//0->4一共有5個線程
thread.setName("線程" + i);
thread.start();
}
}
}
上面的這段代碼也能起到線程對于共享變量的隔離效果,但是需要線程挨個排隊去實現(xiàn)業(yè)務(wù),這樣就失去了多線程并發(fā)執(zhí)行的意義了。
ThreadLocal和Synchronized的區(qū)別
雖然ThreadLocal模式和Synchronized關(guān)鍵字都用于多線程并發(fā)訪問變量的問題,不過兩者處理問題的角度和思路的不同的
|Synchronized|ThreadLocal|
---|----|--|--|
原理|同步機(jī)制采用了以時間換空間的方式,只提供了一份變量,讓不同線程排隊訪問|ThreadLocal采用了以空間換時間的方式,為每個線程都提供了一份變量的副本,從而實現(xiàn)同時訪問而互不干擾
側(cè)重點|多個線程之間訪問資源的同步|多個線程中讓每個線程之間的數(shù)據(jù)相互隔離
ThreadLocal和Synchronized都能解決問題,但是使用ThreadLocal更為合適,因為這樣可以讓程序擁有更高的并發(fā)性
ThreadLocal的使用場景與優(yōu)勢
使用場景
- 如在銀行證券相互轉(zhuǎn)賬時,我們手動開啟事務(wù),直接獲取當(dāng)前線程綁定的連接對象,如果連接對象是空的再去連接池中獲取連接,將此連接對象跟當(dāng)前線程進(jìn)行綁定。
優(yōu)勢
- 傳遞數(shù)據(jù): 保存每個線程綁定的數(shù)據(jù),在需要的地方可以直接獲取,避免參數(shù)直接傳遞帶來的代碼耦合性問題
- 線程隔離: 各線程之間的數(shù)據(jù)相互隔離卻又具有并發(fā)性,避免同步方式帶來的性能損失
ThreadLocal的設(shè)計及源碼分析
設(shè)計
JDK1.8中ThreadLocal的設(shè)計原則是:每個Thread維護(hù)一個ThreadLocalMap,這個Map的key是ThreadLocal實例本身,value才是真正要存儲的值object,比如存儲的user對象等,如下圖所示。

過程如下:
- 每個Thread線程內(nèi)部都有一個Map(ThreadLocalMap)
- Map里面存儲ThreadLocal對象(key) 和線程的變量副本(value)
- Thread內(nèi)部的Map是由ThreadLocal維護(hù)的,由ThreadLocal負(fù)責(zé)向map獲取和設(shè)置線程的變量值
- 對于不同的線程,每次獲取副本值時,別的線程并不能獲取到當(dāng)前線程的副本值,形成副本的隔離,互不干擾
源碼分析
get 方法
public T get() {
//獲取到當(dāng)前線程
Thread t = Thread.currentThread();
//每個線程內(nèi)都有一個ThreadLocalMap對象,獲取到當(dāng)前線程內(nèi)的 ThreadLocalMap 對象,
ThreadLocalMap map = getMap(t);
if (map != null) {
//獲取 ThreadLocalMap 中的 Entry 對象并拿到 Value值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果這個線程之前沒創(chuàng)建過 ThreadLocalMap,就初始化一個ThreadLocalMap
return setInitialValue();
}
getMap方法
//上面?zhèn)魅胍粋€Thread.currentThread(),這個方法就是獲取當(dāng)前線程內(nèi)的ThreadLocalMap對象
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
threadLocals
//這個對象的名字叫threadLocals
ThreadLocal.ThreadLocalMap threadLocals = null;
set方法
//獲取當(dāng)前線程的引用,把值給set進(jìn)去
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//這個線程之前沒創(chuàng)建過 ThreadLocalMap就創(chuàng)建一個
createMap(t, value);
}
ThreadLocal使用注意事項
一個線程池,中間有個線程1號工作了一段時間,它的ThreadLocalMap里面裝了許多東西,裝完了之后如果沒有被清理掉,然后直接還到線程池里面,
任務(wù)不停的過來,然后都沒清理就會產(chǎn)生很大問題。因此在讀線程池源碼的時候,發(fā)現(xiàn)任務(wù)執(zhí)行完之后首先清理ThreadLocals,首先清理Map。
項目中使用遇到的問題
思考
- ThreadLocal是不是用來解決共享資源的多線程訪問問題的?
不是,ThreadLocal雖然可以用于解決多線程情況下的線程安全問題,但其資源不是共享的,而是每個線程獨享的。它解決并發(fā)資源的思路是在initialValue中new出自己線程獨享的資源,而多個線程之間,它們所訪問的對象本身是不共享的,自然就不存在任何并發(fā)問題。 - ThreadLocal什么情況下會發(fā)生內(nèi)存泄漏?如何避免的?
ThreadLocal內(nèi)存泄漏主要體現(xiàn)在key和value的內(nèi)存泄漏,ThreadLocal的消亡是伴隨著線程的,單純的將ThreadLocal置為空它底層的ThreadLocalMap
的key會存在一個引用而不能釋放,因此會發(fā)生內(nèi)存泄漏。
解決方法:
1.key采用弱引用(ThreadLocal內(nèi)部解決)
2.通過.remove的方式避免了value值的內(nèi)存泄漏
小結(jié)
本文主要介紹ThreadLocal的一些基本用法,以及它的設(shè)計和源碼分享,又介紹了它可能產(chǎn)生內(nèi)存泄漏體現(xiàn)的兩個方面,
因此每次在使用完之后都要做.remove操作。
本文由mdnice多平臺發(fā)布