深入淺出解析Java ThreadLocal原理
分享一下最近看的ThreadLocal的源碼的一些體會(huì)。
1.了解ThreadLocal
簡介
- ThreadLocal是JDK中java.lang包下提供的類。
- ThreadLocal是線程安全的,并且沒有使用到鎖。
- 常用來存放線程獨(dú)有變量,解決參數(shù)傳遞問題。
- 當(dāng)我們創(chuàng)建一個(gè)ThreadLocal包裝的變量后,每個(gè)訪問這個(gè)變量的線程會(huì)在自己的線程空間創(chuàng)建這個(gè)變量的一個(gè)副本,在每次操作這個(gè)變量的時(shí)候,都是在自己的線程空間內(nèi)操作,解決了線程安全問題。
使用
- (是線程安全的) 在這個(gè)demo中,localStr是共享的,隨后在每個(gè)線程中給localStr設(shè)置值為自己線程的名字,然后再將當(dāng)前線程的日志輸出。
- sleep5毫秒是為了體現(xiàn)出是否存在線程安全問題。
- 從運(yùn)行結(jié)果可以看到,是不存在線程安全問題的:
/** * @author ATFWUS * @version 1.0 * @date 2021/11/8 21:23 * @description */ @Slf4j public class ThreadLocalTest { static ThreadLocal<String> localStr = new ThreadLocal<>(); public static void main(String[] args) { List<Thread> list = new LinkedList<>(); for(int i = 0; i < 1000; i++){ Thread t = new Thread(new Runnable() { @Override public void run() { localStr.set(Thread.currentThread().getName() + " localStr"); try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } log.debug(localStr.get()); } }, "t" + String.valueOf(i)); list.add(t); } for (Thread t : list) { t.start(); } } }
而對(duì)于普通變量來說,很明顯是存在線程安全問題的:
/** * @author ATFWUS * @version 1.0 * @date 2021/11/8 21:23 * @description */ @Slf4j public class ThreadLocalTest { static ThreadLocal<String> localStr = new ThreadLocal<>(); static String shareStr; public static void main(String[] args) { List<Thread> list = new LinkedList<>(); for(int i = 0; i < 1000; i++){ Thread t = new Thread(new Runnable() { @Override public void run() { shareStr = Thread.currentThread().getName() + " shareStr"; try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } log.debug(shareStr); } }, "t" + String.valueOf(i)); list.add(t); } for (Thread t : list) { t.start(); } } }
2.源碼解析 – 探究實(shí)現(xiàn)思路
threadLocals變量與ThreadLocalMap
- 每個(gè)線程的本地變量并不存放于ThreadLocal對(duì)象中,而是存在調(diào)用線程的threadLocals變量中。因?yàn)槭蔷€程對(duì)象的成員變量,所以生命周期等同于線程的生命周期。
- 而threadLocals是ThreadLocalMap類的實(shí)例。
- ThreadLocalMap實(shí)際上是一個(gè)類似HashMap的實(shí)現(xiàn),是ThreadLocal的靜態(tài)內(nèi)部類。
- 看下Doug Lea寫的注釋: ThreadLocalMap是一個(gè)定制的hash map,僅適用于維護(hù)線程本地值。在ThreadLocal類之外沒有暴露任何的操作。這個(gè)類是私有的,允許在類線程中聲明字段。為了處理非常大并長期存在(對(duì)象)的用法,哈希表的entries使用weakReference作為鍵。但是,由于沒有使用引用隊(duì)列,因此只有當(dāng)表開始耗盡空間時(shí),才能保證刪除過時(shí)的entries。
- 暫不探究ThreadLocalMap的內(nèi)部實(shí)現(xiàn)細(xì)節(jié),暫時(shí)只需要知道實(shí)現(xiàn)了一個(gè)hash map,并且Entry的key是弱引用即可,具體的set() get() remove() 方法在下文中會(huì)有。
set(T value) 方法
- 進(jìn)入set(T value) 方法后,先嘗試獲取map,如果獲取到了map,直接設(shè)置值,否則新建一個(gè)map。
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() 方法
- 進(jìn)入get()方法后,首先獲取當(dāng)前線程,然后進(jìn)入getMap(Thread t)中獲取ThreadLocalMap對(duì)象,直接返回t.threadLocals。
- 如果map不為空,直接返回map中當(dāng)前ThreadLocal作為鍵對(duì)應(yīng)的值。
- 如果map為空,需要先進(jìn)行初始化。調(diào)用setInitialValue()方法進(jìn)行初始化。
- setInitialValue()中先獲取一個(gè)初始值,默認(rèn)為null。
- 如果map存在當(dāng)前線程中,直接設(shè)置初始值。
- 如果map不存在當(dāng)前線程中,需要先創(chuàng)建一個(gè)map。
- createMap(Thread t, T firstValue)中就是new了一個(gè)ThreadLocalMap對(duì)象,并且初始化了一個(gè)entry對(duì)。
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(); }
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
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; }
protected T initialValue() { return null; }
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
remove() 方法
- remove() 方法中,先判斷map是否存在,不存在直接將map中this作為鍵的entry刪掉。
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
實(shí)現(xiàn)思路總結(jié)
- ThreadLocal搭配線程的threadLocals變量實(shí)現(xiàn),當(dāng)調(diào)用set(T value) 和 get() 方法時(shí),如果線程中的threadLocals仍然為null,會(huì)為其初始化。
- ThreadLocal對(duì)象往threadLocals存儲(chǔ)具體變量時(shí),key是ThreadLocal對(duì)象的自身引用,value是真正的變量,且key是弱引用。
3.InheritableThreadLocal與繼承性
InheritableThreadLocal英語翻譯一下就是可繼承的ThreadLocal,讓我們看下它和ThreadLocal的繼承性體現(xiàn)在哪。
這里的繼承性指的是:子線程是否能訪問父線程的變量。
ThreadLocal的不可繼承性
threadLocals是當(dāng)前線程的成員變量,在子線程中不可見
/** * @author ATFWUS * @version 1.0 * @date 2021/11/9 14:29 * @description */ @Slf4j public class InheritableThreadLocalTest { static ThreadLocal<String> localStr = new ThreadLocal<>(); public static void main(String[] args) { localStr.set("main線程為其設(shè)置的值"); new Thread(new Runnable() { @Override public void run() { log.debug("訪問localStr : " + localStr.get()); } }).start(); System.out.println(localStr.get()); } }
InheritableThreadLocal實(shí)現(xiàn)繼承性的源碼剖析
看一下InheritableThreadLocal的源碼:
源碼非常簡短,下面簡單分析一下:
- InheritableThreadLocal類繼承自ThreadLocal類,重寫了childValue(T parentValue)、getMap()、createMap(Thread t, T firstValue) 三個(gè)方法。
- createMap(Thread t, T firstValue)會(huì)在初始化的時(shí)候調(diào)用,重寫createMap(Thread t, T firstValue) 意味著,InheritableThreadLocal的實(shí)例使用的是線程對(duì)象中的inheritableThreadLocals,而不再是原來的threadLocals。
- getMap() 方法也是確保使用的是inheritableThreadLocals。
- childValue(T parentValue) 方法中,直接返回了parentValue,這個(gè)方法會(huì)在ThreadLocal的構(gòu)造方法中被調(diào)用,為了弄清這個(gè)意圖,我們有必要看看Thread類初始化方法的源碼。
從Thread的構(gòu)造方法看,發(fā)現(xiàn)所有的構(gòu)造方法都會(huì)調(diào)用init()方法進(jìn)行初始化,init()方法有兩個(gè)重載形式。
我們進(jìn)入?yún)?shù)較多的init方法查看一下:
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { if (name == null) { throw new NullPointerException("name cannot be null"); } this.name = name; // 新線程還未創(chuàng)建出來,當(dāng)前線程就是即將要?jiǎng)?chuàng)建線程的父線程 Thread parent = currentThread(); SecurityManager security = System.getSecurityManager(); if (g == null) { /* Determine if it's an applet or not */ /* If there is a security manager, ask the security manager what to do. */ if (security != null) { g = security.getThreadGroup(); } /* If the security doesn't have a strong opinion of the matter use the parent thread group. */ if (g == null) { g = parent.getThreadGroup(); } } /* checkAccess regardless of whether or not threadgroup is explicitly passed in. */ g.checkAccess(); /* * Do we have the required permissions? */ if (security != null) { if (isCCLOverridden(getClass())) { security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); } } g.addUnstarted(); this.group = g; this.daemon = parent.isDaemon(); this.priority = parent.getPriority(); if (security == null || isCCLOverridden(parent.getClass())) this.contextClassLoader = parent.getContextClassLoader(); else this.contextClassLoader = parent.contextClassLoader; this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext(); this.target = target; setPriority(priority); // 如果父線程的inheritThreadLocals 不為空 if (inheritThreadLocals && parent.inheritableThreadLocals != null) // 設(shè)置子線程中的inheritableThreadLocals設(shè)置為父線程的inheritableThreadLocals this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */ this.stackSize = stackSize; /* Set thread ID */ tid = nextThreadID(); }
我們重點(diǎn)看一下和inheritThreadLocals相關(guān)的地方(含注釋的地方)
- 在進(jìn)入init方法后,先獲取了父線程,然后再下面判斷了父線程的inheritThreadLocals 是否為空,不為空就調(diào)用ThreadLocal.createInheritedMap方法,參數(shù)就是父線程的inheritThreadLocals 。
再看下ThreadLocal.createInheritedMap方法:
- 調(diào)用了自身的構(gòu)造方法,將parentMap傳入。
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); }
看下這個(gè)構(gòu)造方法:
- 發(fā)現(xiàn)主要是用parentMap的所有entry初始化當(dāng)前的map。
- 在注釋處,調(diào)用了inheritThreadLocals重寫的childValue方法,而重寫后,直接返回的是parentValue,也就是將父線程的inheritThreadLocal里面的entry完整的復(fù)制到了子線程中。
private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { // 調(diào)用inheritThreadLocals重寫的childValue方法 Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } }
如何理解這個(gè)繼承性
通過上面的源碼分析,可以發(fā)現(xiàn),InheritableThreadLocal的繼承性主要體現(xiàn)在:創(chuàng)建子線程時(shí),會(huì)將父線程的inheritThreadLocals里面所有entry拷貝一份給子進(jìn)程。
那么當(dāng)子進(jìn)程被創(chuàng)建出來之后,父進(jìn)程又修改了inheritThreadLocals里面的值,這個(gè)操作是否對(duì)子線程可見,通過上面的源碼可知,這個(gè)操作明顯是不可見的,下面有個(gè)demo可以證實(shí)。
- sleep操作是為了控制兩個(gè)線程的執(zhí)行流程。
/** * @author ATFWUS * @version 1.0 * @date 2021/11/9 14:29 * @description */ @Slf4j public class InheritableThreadLocalTest { static ThreadLocal<String> localStr = new ThreadLocal<>(); static InheritableThreadLocal<String> inheritableLocalStr = new InheritableThreadLocal<>(); public static void main(String[] args) throws InterruptedException { inheritableLocalStr.set("main線程第一次為inheritableLocalStr設(shè)置的值"); new Thread(new Runnable() { @Override public void run() { log.debug("子線程第一次訪問inheritableLocalStr : " + inheritableLocalStr.get()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("子線程第二次訪問inheritableLocalStr : " + inheritableLocalStr.get()); } }).start(); Thread.sleep(500); inheritableLocalStr.set("main線程第二次為inheritableLocalStr設(shè)置的值"); log.debug("main線程第二次為inheritableLocalStr賦值"); Thread.sleep(1000); } }
看下輸出:
可以發(fā)現(xiàn),子線程創(chuàng)建出來后,對(duì)父線程中inheritThreadLocals的修改操作,對(duì)子線程不可見。
總結(jié)
- ThreadLocal不可繼承,threadLocals是當(dāng)前線程的成員變量,在子線程中不可見。
- InheritableThreadLocal可繼承,原理是:在新建子線程的時(shí)候,將父線程中inheritThreadLocals所有的entry拷貝給了子線程。
- 子線程創(chuàng)建出來后,對(duì)父線程中inheritThreadLocals的修改操作,對(duì)子線程不可見。
4.存在的內(nèi)存泄露問題
要充分理解ThreadLocal中存在的內(nèi)存泄露問題,需要有以下JVM對(duì)內(nèi)存管理的前置知識(shí)(這里篇幅問題就不補(bǔ)充了):
- 什么是內(nèi)存泄露?
- 什么是強(qiáng)引用?
- 什么是弱引用?
- 何時(shí)GC?
- 強(qiáng)引用和弱引用GC時(shí)的區(qū)別?
在分析上述ThreadLocalMap源碼的時(shí)候,注意到有一個(gè)小細(xì)節(jié),ThreadLocalMap的Entry繼承了WeakReference<ThreadLocal<?>>,也就是說Entry的key是一個(gè)對(duì)ThreadLocal<?>的弱引用。問題來了,為什么這里要使用弱引用呢?
使用強(qiáng)引用會(huì)如何?
現(xiàn)在假設(shè)Entry的key是一個(gè)對(duì)ThreadLocal的強(qiáng)引用,當(dāng)ThreadLocal對(duì)象使用完后,外部的強(qiáng)引用不存在,但是因?yàn)楫?dāng)前線程對(duì)象中的threadLocals還持有ThreadLocal的強(qiáng)引用,而threadLocals的生命周期是和線程一致的,這個(gè)時(shí)候,如果沒有手動(dòng)刪除,整個(gè)Entry就發(fā)生了內(nèi)存泄露。
使用弱引用會(huì)如何?
現(xiàn)在假設(shè)Entry的key是一個(gè)對(duì)ThreadLocal的弱引用,當(dāng)ThreadLocal對(duì)象使用完后,外部的強(qiáng)引用不存在,此時(shí)ThreadLocal對(duì)象只存在Entry中key對(duì)它的弱引用,在下次GC的時(shí)候,這個(gè)ThreadLocal對(duì)象就會(huì)被回收,導(dǎo)致key為null,此時(shí)value的強(qiáng)引用還存在,但是value已經(jīng)不會(huì)被使用了,如果沒有手動(dòng)刪除,那么這個(gè)Entry中的key就會(huì)發(fā)生內(nèi)存泄露。
使用弱引用還有一些好處,那就是,當(dāng)key為null時(shí), ThreadLocalMap中最多存在一個(gè)key為null,并且當(dāng)調(diào)用set(),get(),remove()這些方法的時(shí)候,是會(huì)清除掉key為null的entry的。
set()、get()、remove() 方法中相關(guān)實(shí)現(xiàn)
- 從下可以發(fā)現(xiàn),set方法首先會(huì)進(jìn)入一個(gè)循環(huán)。
- 在這個(gè)循環(huán)中,會(huì)遍歷整個(gè)Entry數(shù)組。直到遇到一個(gè)空的entry,退出循環(huán)。
- 當(dāng)遇到已存在的key'時(shí),會(huì)直接替換value,然后返回。
- 當(dāng)遇到key為空的entry的時(shí)候,會(huì)直接將當(dāng)前的entry存在這個(gè)過時(shí)的entry中,然后返回。
通過這個(gè)方法的源碼可以看出,key為null的那個(gè)entry實(shí)際上遲早會(huì)被替換成新的entry。
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); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } // 發(fā)現(xiàn)key為空 if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }
同理,可以看到在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); } private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; // 替換過時(shí)的entry if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
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)]) { // 清除過時(shí)的key if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }
總結(jié)
- ThreadLocal如果對(duì)ThreadLocalMap的key使用強(qiáng)引用,那么會(huì)存在整個(gè)entry發(fā)生內(nèi)存泄露的問題,如果不手動(dòng)清除,那么這個(gè)不被使用的entry會(huì)一直存在。
- ThreadLocal如果對(duì)ThreadLocalMap的key使用弱引用,那么可能會(huì)存在一個(gè)entry的value發(fā)生內(nèi)存泄露,但是在調(diào)用set(),get(),remove() 方法時(shí),key為null的entry會(huì)被清除掉。
- 發(fā)生內(nèi)存泄露最根本的原因是:threadLocals的生命周期是和線程一致的。
- 每次使用完ThreadLocal對(duì)象后,必須調(diào)用它的remove()方法清除數(shù)據(jù)。
5.ThreadLocal應(yīng)用
ThreadLocal把數(shù)據(jù)存放到線程本地,解決了線程安全問題,沒有使用鎖,直接訪問線程本地變量,效率較高(空間換時(shí)間。)
同時(shí)threadLocals的生命周期是和線程一致的,可以解決很多參數(shù)傳遞問題。
- Session管理(Mabaties使用ThreadLocal存儲(chǔ)session),數(shù)據(jù)庫連接。
- 如果需要跟蹤請(qǐng)求的整個(gè)流程,可以使用ThreadLocal來傳遞參數(shù)。
ATFWUS 2021-11-11
以上就是深入淺出解析Java ThreadLocal原理的詳細(xì)內(nèi)容,更多關(guān)于Java ThreadLocal原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot整合MongoDB實(shí)現(xiàn)文件上傳下載刪除
這篇文章主要介紹了SpringBoot整合MongoDB實(shí)現(xiàn)文件上傳下載刪除的方法,幫助大家更好的理解和學(xué)習(xí)使用SpringBoot框架,感興趣的朋友可以了解下2021-05-05SpringBoot如何手寫一個(gè)starter并使用這個(gè)starter詳解
starter是SpringBoot中的一個(gè)新發(fā)明,它有效的降低了項(xiàng)目開發(fā)過程的復(fù)雜程度,對(duì)于簡化開發(fā)操作有著非常好的效果,下面這篇文章主要給大家介紹了關(guān)于SpringBoot如何手寫一個(gè)starter并使用這個(gè)starter的相關(guān)資料,需要的朋友可以參考下2022-12-12Java 中實(shí)現(xiàn)隨機(jī)無重復(fù)數(shù)字的方法
為了更好地理解這個(gè)題意,我們先來看下具體內(nèi)容:生成一個(gè)1-100 的隨機(jī)數(shù)組,但數(shù)組中的數(shù)字不能重復(fù),即位置是隨機(jī)的,但數(shù)組元素不能重復(fù)2013-03-03