Java中ThreadLocal使用原理及Synchronized區(qū)別
一、ThreadLocal簡(jiǎn)介
ThreadLocal叫做線程變量,意思是ThreadLocal中填充的變量屬于當(dāng)前線程,該變量對(duì)其他線程而言是隔離的,也就是說(shuō)該變量是當(dāng)前線程獨(dú)有的變量。ThreadLocal為變量在每個(gè)線程中都創(chuàng)建了一個(gè)副本,那么每個(gè)線程可以訪問(wèn)自己內(nèi)部的副本變量。
ThreadLoal 變量,線程局部變量,同一個(gè) ThreadLocal 所包含的對(duì)象,在不同的 Thread 中有不同的副本。這里有幾點(diǎn)需要注意:
- 因?yàn)槊總€(gè) Thread 內(nèi)有自己的實(shí)例副本,且該副本只能由當(dāng)前 Thread 使用。這是也是 ThreadLocal 命名的由來(lái)。
- 既然每個(gè) Thread 有自己的實(shí)例副本,且其它 Thread 不可訪問(wèn),那就不存在多線程間共享的問(wèn)題。
ThreadLocal 提供了線程本地的實(shí)例。它與普通變量的區(qū)別在于,每個(gè)使用該變量的線程都會(huì)初始化一個(gè)完全獨(dú)立的實(shí)例副本。ThreadLocal 變量通常被private static修飾。當(dāng)一個(gè)線程結(jié)束時(shí),它所使用的所有 ThreadLocal 相對(duì)的實(shí)例副本都可被回收。
總的來(lái)說(shuō),ThreadLocal 適用于每個(gè)線程需要自己獨(dú)立的實(shí)例且該實(shí)例需要在多個(gè)方法中被使用,也即變量在線程間隔離而在方法或類間共享的場(chǎng)景
下圖可以增強(qiáng)理解:

圖1-1 ThreadLocal在使用過(guò)程中狀態(tài)
二、ThreadLocal與Synchronized的區(qū)別
ThreadLocal<T>其實(shí)是與線程綁定的一個(gè)變量。ThreadLocal和Synchonized都用于解決多線程并發(fā)訪問(wèn)。
但是ThreadLocal與synchronized有本質(zhì)的區(qū)別:
1、Synchronized用于線程間的數(shù)據(jù)共享,而ThreadLocal則用于線程間的數(shù)據(jù)隔離。
2、Synchronized是利用鎖的機(jī)制,使變量或代碼塊在某一時(shí)該只能被一個(gè)線程訪問(wèn)。而ThreadLocal為每一個(gè)線程都提供了變量的副本,使得每個(gè)線程在某一時(shí)間訪問(wèn)到的并不是同一個(gè)對(duì)象,這樣就隔離了多個(gè)線程對(duì)數(shù)據(jù)的數(shù)據(jù)共享。
而Synchronized卻正好相反,它用于在多個(gè)線程間通信時(shí)能夠獲得數(shù)據(jù)共享。
一句話理解ThreadLocal,threadlocl是作為當(dāng)前線程中屬性ThreadLocalMap集合中的某一個(gè)Entry的key值Entry(threadlocl,value),雖然不同的線程之間threadlocal這個(gè)key值是一樣,但是不同的線程所擁有的ThreadLocalMap是獨(dú)一無(wú)二的,也就是不同的線程間同一個(gè)ThreadLocal(key)對(duì)應(yīng)存儲(chǔ)的值(value)不一樣,從而到達(dá)了線程間變量隔離的目的,但是在同一個(gè)線程中這個(gè)value變量地址是一樣的。
三、ThreadLocal的簡(jiǎn)單使用
直接上代碼:
public class ThreadLocaDemo {
private static ThreadLocal<String> localVar = new ThreadLocal<String>();
static void print(String str) {
//打印當(dāng)前線程中本地內(nèi)存中本地變量的值
System.out.println(str + " :" + localVar.get());
//清除本地內(nèi)存中的本地變量
localVar.remove();
}
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
public void run() {
ThreadLocaDemo.localVar.set("local_A");
print("A");
//打印本地變量
System.out.println("after remove : " + localVar.get());
}
},"A").start();
Thread.sleep(1000);
new Thread(new Runnable() {
public void run() {
ThreadLocaDemo.localVar.set("local_B");
print("B");
System.out.println("after remove : " + localVar.get());
}
},"B").start();
}
}
A :local_A
after remove : null
B :local_B
after remove : null從這個(gè)示例中我們可以看到,兩個(gè)線程分表獲取了自己線程存放的變量,他們之間變量的獲取并不會(huì)錯(cuò)亂。這個(gè)的理解也可以結(jié)合圖1-1,相信會(huì)有一個(gè)更深刻的理解。
四、ThreadLocal的原理
要看原理那么就得從源碼看起。
4.1 ThreadLocal的set()方法:
public void set(T value) {
//1、獲取當(dāng)前線程
Thread t = Thread.currentThread();
//2、獲取線程中的屬性 threadLocalMap ,如果threadLocalMap 不為空,
//則直接更新要保存的變量值,否則創(chuàng)建threadLocalMap,并賦值
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 初始化thradLocalMap 并賦值
createMap(t, value);
}從上面的代碼可以看出,ThreadLocal set賦值的時(shí)候首先會(huì)獲取當(dāng)前線程thread,并獲取thread線程中的ThreadLocalMap屬性。如果map屬性不為空,則直接更新value值,如果map為空,則實(shí)例化threadLocalMap,并將value值初始化。
那么ThreadLocalMap又是什么呢,還有createMap又是怎么做的,我們繼續(xù)往下看。大家最后自己再idea上跟下源碼,會(huì)有更深的認(rèn)識(shí)。
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;
}
}
}可看出ThreadLocalMap是ThreadLocal的內(nèi)部靜態(tài)類,而它的構(gòu)成主要是用Entry來(lái)保存數(shù)據(jù) ,而且還是繼承的弱引用。在Entry內(nèi)部使用ThreadLocal作為key,使用我們?cè)O(shè)置的value作為value。詳細(xì)內(nèi)容要大家自己去跟。
//這個(gè)是threadlocal 的內(nèi)部方法
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//ThreadLocalMap 構(gòu)造方法
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);
}4.2 ThreadLocal的get方法
public T get() {
//1、獲取當(dāng)前線程
Thread t = Thread.currentThread();
//2、獲取當(dāng)前線程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//3、如果map數(shù)據(jù)不為空,
if (map != null) {
//3.1、獲取threalLocalMap中存儲(chǔ)的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果是數(shù)據(jù)為null,則初始化,初始化的結(jié)果,TheralLocalMap中存放key值為threadLocal,值為null
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;
}4.3 ThreadLocal的remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}remove方法,直接將ThrealLocal 對(duì)應(yīng)的值從當(dāng)前相差Thread中的ThreadLocalMap中刪除。為什么要?jiǎng)h除,這涉及到內(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。
ThreadLocal其實(shí)是與線程綁定的一個(gè)變量,如此就會(huì)出現(xiàn)一個(gè)問(wèn)題:如果沒(méi)有將ThreadLocal內(nèi)的變量刪除(remove)或替換,它的生命周期將會(huì)與線程共存。通常線程池中對(duì)線程管理都是采用線程復(fù)用的方法,在線程池中線程很難結(jié)束甚至于永遠(yuǎn)不會(huì)結(jié)束,這將意味著線程持續(xù)的時(shí)間將不可預(yù)測(cè),甚至與JVM的生命周期一致。舉個(gè)例字,如果ThreadLocal中直接或間接包裝了集合類或復(fù)雜對(duì)象,每次在同一個(gè)ThreadLocal中取出對(duì)象后,再對(duì)內(nèi)容做操作,那么內(nèi)部的集合類和復(fù)雜對(duì)象所占用的空間可能會(huì)開始持續(xù)膨脹。
4.4、ThreadLocal與Thread,ThreadLocalMap之間的關(guān)系


圖4-1 Thread、THreadLocal、ThreadLocalMap之間啊的數(shù)據(jù)關(guān)系圖
從這個(gè)圖中我們可以非常直觀的看出,ThreadLocalMap其實(shí)是Thread線程的一個(gè)屬性值,而ThreadLocal是維護(hù)ThreadLocalMap
這個(gè)屬性指的一個(gè)工具類。Thread線程可以擁有多個(gè)ThreadLocal維護(hù)的自己線程獨(dú)享的共享變量(這個(gè)共享變量只是針對(duì)自己線程里面共享)
五、ThreadLocal 常見使用場(chǎng)景
如上文所述,ThreadLocal 適用于如下兩種場(chǎng)景
1、每個(gè)線程需要有自己?jiǎn)为?dú)的實(shí)例
2、實(shí)例需要在多個(gè)方法中共享,但不希望被多線程共享
對(duì)于第一點(diǎn),每個(gè)線程擁有自己實(shí)例,實(shí)現(xiàn)它的方式很多。例如可以在線程內(nèi)部構(gòu)建一個(gè)單獨(dú)的實(shí)例。ThreadLoca 可以以非常方便的形式滿足該需求。
對(duì)于第二點(diǎn),可以在滿足第一點(diǎn)(每個(gè)線程有自己的實(shí)例)的條件下,通過(guò)方法間引用傳遞的形式實(shí)現(xiàn)。ThreadLocal 使得代碼耦合度更低,且實(shí)現(xiàn)更優(yōu)雅。
場(chǎng)景
1)存儲(chǔ)用戶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;
}場(chǎng)景二、數(shù)據(jù)庫(kù)連接,處理數(shù)據(jù)庫(kù)事務(wù)
場(chǎng)景三、數(shù)據(jù)跨層傳遞(controller,service, dao)
每個(gè)線程內(nèi)需要保存類似于全局變量的信息(例如在攔截器中獲取的用戶信息),可以讓不同方法直接使用,避免參數(shù)傳遞的麻煩卻不想被多線程共享(因?yàn)椴煌€程獲取到的用戶信息不一樣)。
例如,用 ThreadLocal 保存一些業(yè)務(wù)內(nèi)容(用戶權(quán)限信息、從用戶系統(tǒng)獲取到的用戶名、用戶ID 等),這些信息在同一個(gè)線程內(nèi)相同,但是不同的線程使用的業(yè)務(wù)內(nèi)容是不相同的。
在線程生命周期內(nèi),都通過(guò)這個(gè)靜態(tài) ThreadLocal 實(shí)例的 get() 方法取得自己 set 過(guò)的那個(gè)對(duì)象,避免了將這個(gè)對(duì)象(如 user 對(duì)象)作為參數(shù)傳遞的麻煩。
比如說(shuō)我們是一個(gè)用戶系統(tǒng),那么當(dāng)一個(gè)請(qǐng)求進(jìn)來(lái)的時(shí)候,一個(gè)線程會(huì)負(fù)責(zé)執(zhí)行這個(gè)請(qǐng)求,然后這個(gè)請(qǐng)求就會(huì)依次調(diào)用service-1()、service-2()、service-3()、service-4(),這4個(gè)方法可能是分布在不同的類中的。這個(gè)例子和存儲(chǔ)session有些像。
package com.kong.threadlocal;
public class ThreadLocalDemo05 {
public static void main(String[] args) {
User user = new User("jack");
new Service1().service1(user);
}
}
class Service1 {
public void service1(User user){
//給ThreadLocal賦值,后續(xù)的服務(wù)直接通過(guò)ThreadLocal獲取就行了。
UserContextHolder.holder.set(user);
new Service2().service2();
}
}
class Service2 {
public void service2(){
User user = UserContextHolder.holder.get();
System.out.println("service2拿到的用戶:"+user.name);
new Service3().service3();
}
}
class Service3 {
public void service3(){
User user = UserContextHolder.holder.get();
System.out.println("service3拿到的用戶:"+user.name);
//在整個(gè)流程執(zhí)行完畢后,一定要執(zhí)行remove
UserContextHolder.holder.remove();
}
}
class UserContextHolder {
//創(chuàng)建ThreadLocal保存User對(duì)象
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name){
this.name = name;
}
}
執(zhí)行的結(jié)果:
service2拿到的用戶:jack
service3拿到的用戶:jack場(chǎng)景四、Spring使用ThreadLocal解決線程安全問(wèn)題
我們知道在一般情況下,只有無(wú)狀態(tài)的Bean才可以在多線程環(huán)境下共享,在Spring中,絕大部分Bean都可以聲明為singleton作用域。就是因?yàn)镾pring對(duì)一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全的“狀態(tài)性對(duì)象”采用ThreadLocal進(jìn)行封裝,讓它們也成為線程安全的“狀態(tài)性對(duì)象”,因此有狀態(tài)的Bean就能夠以singleton的方式在多線程中正常工作了。
一般的Web應(yīng)用劃分為展現(xiàn)層、服務(wù)層和持久層三個(gè)層次,在不同的層中編寫對(duì)應(yīng)的邏輯,下層通過(guò)接口向上層開放功能調(diào)用。在一般情況下,從接收請(qǐng)求到返回響應(yīng)所經(jīng)過(guò)的所有程序調(diào)用都同屬于一個(gè)線程,如圖9-2所示。

這樣用戶就可以根據(jù)需要,將一些非線程安全的變量以ThreadLocal存放,在同一次請(qǐng)求響應(yīng)的調(diào)用線程中,所有對(duì)象所訪問(wèn)的同一ThreadLocal變量都是當(dāng)前線程所綁定的。
下面的實(shí)例能夠體現(xiàn)Spring對(duì)有狀態(tài)Bean的改造思路:
代碼清單9-5 TopicDao:非線程安全
public class TopicDao {
//①一個(gè)非線程安全的變量
private Connection conn;
public void addTopic(){
//②引用非線程安全變量
Statement stat = conn.createStatement();
…
}由于①處的conn是成員變量,因?yàn)閍ddTopic()方法是非線程安全的,必須在使用時(shí)創(chuàng)建一個(gè)新TopicDao實(shí)例(非singleton)。下面使用ThreadLocal對(duì)conn這個(gè)非線程安全的“狀態(tài)”進(jìn)行改造:
代碼清單9-6 TopicDao:線程安全
import java.sql.Connection;
import java.sql.Statement;
public class TopicDao {
//①使用ThreadLocal保存Connection變量
private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
public static Connection getConnection(){
//②如果connThreadLocal沒(méi)有本線程對(duì)應(yīng)的Connection創(chuàng)建一個(gè)新的Connection,
//并將其保存到線程本地變量中。
if (connThreadLocal.get() == null) {
Connection conn = ConnectionManager.getConnection();
connThreadLocal.set(conn);
return conn;
}else{
//③直接返回線程本地變量
return connThreadLocal.get();
}
}
public void addTopic() {
//④從ThreadLocal中獲取線程對(duì)應(yīng)的
Statement stat = getConnection().createStatement();
}不同的線程在使用TopicDao時(shí),先判斷connThreadLocal.get()是否為null,如果為null,則說(shuō)明當(dāng)前線程還沒(méi)有對(duì)應(yīng)的Connection對(duì)象,這時(shí)創(chuàng)建一個(gè)Connection對(duì)象并添加到本地線程變量中;如果不為null,則說(shuō)明當(dāng)前的線程已經(jīng)擁有了Connection對(duì)象,直接使用就可以了。這樣,就保證了不同的線程使用線程相關(guān)的Connection,而不會(huì)使用其他線程的Connection。因此,這個(gè)TopicDao就可以做到singleton共享了。
當(dāng)然,這個(gè)例子本身很粗糙,將Connection的ThreadLocal直接放在Dao只能做到本Dao的多個(gè)方法共享Connection時(shí)不發(fā)生線程安全問(wèn)題,但無(wú)法和其他Dao共用同一個(gè)Connection,要做到同一事務(wù)多Dao共享同一個(gè)Connection,必須在一個(gè)共同的外部類使用ThreadLocal保存Connection。但這個(gè)實(shí)例基本上說(shuō)明了Spring對(duì)有狀態(tài)類線程安全化的解決思路。在本章后面的內(nèi)容中,我們將詳細(xì)說(shuō)明Spring如何通過(guò)ThreadLocal解決事務(wù)管理的問(wèn)題。
參考
ThreadLocal的應(yīng)用場(chǎng)景 - sw_kong - 博客園
ThreadLocal使用場(chǎng)景分析 - 簡(jiǎn)書
ThreadLocal原理分析與使用場(chǎng)景 - 阿凡盧 - 博客園
ThreadLocal原理分析與使用場(chǎng)景 - 阿凡盧 - 博客園
ThreadLocal使用場(chǎng)景分析 - 簡(jiǎn)書
(轉(zhuǎn))spring里面對(duì)ThreadLocal的使用_迷茫之欲的博客-CSDN博客_spring threadlocal
到此這篇關(guān)于Java中ThreadLocal使用原理及Synchronized區(qū)別的文章就介紹到這了,更多相關(guān)Java ThreadLocal Synchronized 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
jdbc實(shí)現(xiàn)寵物商店管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了jdbc實(shí)現(xiàn)寵物商店管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-10-10
Java8中利用stream對(duì)map集合進(jìn)行過(guò)濾的方法
這篇文章主要給大家介紹了關(guān)于Java8中利用stream對(duì)map集合進(jìn)行過(guò)濾的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07
在Java生產(chǎn)環(huán)境下進(jìn)行性能監(jiān)控與調(diào)優(yōu)的詳細(xì)過(guò)程
在Java生產(chǎn)環(huán)境下進(jìn)行性能監(jiān)控與調(diào)優(yōu)是一個(gè)復(fù)雜但重要的過(guò)程,它涉及到多個(gè)方面,包括代碼分析、JVM監(jiān)控、線程管理、垃圾收集優(yōu)化、內(nèi)存管理、數(shù)據(jù)庫(kù)交互等,下面我將提供一個(gè)詳細(xì)的概述和示例代碼,需要的朋友可以參考下2025-02-02
Java中JSON格式反序列化為Map且保證存取順序一致的問(wèn)題
這篇文章主要介紹了Java中JSON格式反序列化為Map且保證存取順序一致的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-06-06
詳解Spring Data操作Redis數(shù)據(jù)庫(kù)
Redis是一種NOSQL數(shù)據(jù)庫(kù),Key-Value形式對(duì)數(shù)據(jù)進(jìn)行存儲(chǔ),其中數(shù)據(jù)可以以內(nèi)存形式存在,也可以持久化到文件系統(tǒng)。Spring data對(duì)Redis進(jìn)行了很好的封裝,用起來(lái)也是十分的得心應(yīng)手,接下來(lái)通過(guò)本文給大家分享Spring Data操作Redis數(shù)據(jù)庫(kù),需要的朋友參考下2017-03-03
Java序列化JSON丟失精度問(wèn)題的解決方法(修復(fù)Long類型太長(zhǎng))
這篇文章主要給大家介紹了關(guān)于Java序列化JSON丟失精度問(wèn)題的解決方法,修復(fù)Long類型太長(zhǎng)的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-03-03
Java實(shí)現(xiàn)利用圖片或視頻生成GIF并發(fā)送微信
這篇文章主要為大家詳細(xì)介紹了Java語(yǔ)言如何利用圖片或視頻實(shí)現(xiàn)生成GIF并發(fā)送微信的功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以嘗試一下2022-11-11
SpringBoot項(xiàng)目中Controller接收兩個(gè)實(shí)體的實(shí)現(xiàn)方法
本文主要介紹了SpringBoot項(xiàng)目中Controller接收兩個(gè)實(shí)體的實(shí)現(xiàn)方法,主要介紹了兩種方法,具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08

