深入淺出解析Java ThreadLocal原理
分享一下最近看的ThreadLocal的源碼的一些體會。
1.了解ThreadLocal
簡介
- ThreadLocal是JDK中java.lang包下提供的類。
- ThreadLocal是線程安全的,并且沒有使用到鎖。
- 常用來存放線程獨有變量,解決參數(shù)傳遞問題。
- 當我們創(chuàng)建一個ThreadLocal包裝的變量后,每個訪問這個變量的線程會在自己的線程空間創(chuàng)建這個變量的一個副本,在每次操作這個變量的時候,都是在自己的線程空間內(nèi)操作,解決了線程安全問題。
使用
- (是線程安全的) 在這個demo中,localStr是共享的,隨后在每個線程中給localStr設(shè)置值為自己線程的名字,然后再將當前線程的日志輸出。
- sleep5毫秒是為了體現(xià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(); } } }
而對于普通變量來說,很明顯是存在線程安全問題的:
/** * @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.源碼解析 – 探究實現(xiàn)思路
threadLocals變量與ThreadLocalMap
- 每個線程的本地變量并不存放于ThreadLocal對象中,而是存在調(diào)用線程的threadLocals變量中。因為是線程對象的成員變量,所以生命周期等同于線程的生命周期。
- 而threadLocals是ThreadLocalMap類的實例。
- ThreadLocalMap實際上是一個類似HashMap的實現(xiàn),是ThreadLocal的靜態(tài)內(nèi)部類。
- 看下Doug Lea寫的注釋: ThreadLocalMap是一個定制的hash map,僅適用于維護線程本地值。在ThreadLocal類之外沒有暴露任何的操作。這個類是私有的,允許在類線程中聲明字段。為了處理非常大并長期存在(對象)的用法,哈希表的entries使用weakReference作為鍵。但是,由于沒有使用引用隊列,因此只有當表開始耗盡空間時,才能保證刪除過時的entries。
- 暫不探究ThreadLocalMap的內(nèi)部實現(xiàn)細節(jié),暫時只需要知道實現(xiàn)了一個hash map,并且Entry的key是弱引用即可,具體的set() get() remove() 方法在下文中會有。
set(T value) 方法
- 進入set(T value) 方法后,先嘗試獲取map,如果獲取到了map,直接設(shè)置值,否則新建一個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() 方法
- 進入get()方法后,首先獲取當前線程,然后進入getMap(Thread t)中獲取ThreadLocalMap對象,直接返回t.threadLocals。
- 如果map不為空,直接返回map中當前ThreadLocal作為鍵對應的值。
- 如果map為空,需要先進行初始化。調(diào)用setInitialValue()方法進行初始化。
- setInitialValue()中先獲取一個初始值,默認為null。
- 如果map存在當前線程中,直接設(shè)置初始值。
- 如果map不存在當前線程中,需要先創(chuàng)建一個map。
- createMap(Thread t, T firstValue)中就是new了一個ThreadLocalMap對象,并且初始化了一個entry對。
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); }
實現(xiàn)思路總結(jié)
- ThreadLocal搭配線程的threadLocals變量實現(xiàn),當調(diào)用set(T value) 和 get() 方法時,如果線程中的threadLocals仍然為null,會為其初始化。
- ThreadLocal對象往threadLocals存儲具體變量時,key是ThreadLocal對象的自身引用,value是真正的變量,且key是弱引用。
3.InheritableThreadLocal與繼承性
InheritableThreadLocal英語翻譯一下就是可繼承的ThreadLocal,讓我們看下它和ThreadLocal的繼承性體現(xiàn)在哪。
這里的繼承性指的是:子線程是否能訪問父線程的變量。
ThreadLocal的不可繼承性
threadLocals是當前線程的成員變量,在子線程中不可見
/** * @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實現(xiàn)繼承性的源碼剖析
看一下InheritableThreadLocal的源碼:
源碼非常簡短,下面簡單分析一下:
- InheritableThreadLocal類繼承自ThreadLocal類,重寫了childValue(T parentValue)、getMap()、createMap(Thread t, T firstValue) 三個方法。
- createMap(Thread t, T firstValue)會在初始化的時候調(diào)用,重寫createMap(Thread t, T firstValue) 意味著,InheritableThreadLocal的實例使用的是線程對象中的inheritableThreadLocals,而不再是原來的threadLocals。
- getMap() 方法也是確保使用的是inheritableThreadLocals。
- childValue(T parentValue) 方法中,直接返回了parentValue,這個方法會在ThreadLocal的構(gòu)造方法中被調(diào)用,為了弄清這個意圖,我們有必要看看Thread類初始化方法的源碼。
從Thread的構(gòu)造方法看,發(fā)現(xiàn)所有的構(gòu)造方法都會調(diào)用init()方法進行初始化,init()方法有兩個重載形式。
我們進入?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)建出來,當前線程就是即將要創(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(); }
我們重點看一下和inheritThreadLocals相關(guā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òu)造方法:
- 發(fā)現(xiàn)主要是用parentMap的所有entry初始化當前的map。
- 在注釋處,調(diào)用了inheritThreadLocals重寫的childValue方法,而重寫后,直接返回的是parentValue,也就是將父線程的inheritThreadLocal里面的entry完整的復制到了子線程中。
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++; } } } }
如何理解這個繼承性
通過上面的源碼分析,可以發(fā)現(xiàn),InheritableThreadLocal的繼承性主要體現(xiàn)在:創(chuàng)建子線程時,會將父線程的inheritThreadLocals里面所有entry拷貝一份給子進程。
那么當子進程被創(chuàng)建出來之后,父進程又修改了inheritThreadLocals里面的值,這個操作是否對子線程可見,通過上面的源碼可知,這個操作明顯是不可見的,下面有個demo可以證實。
- sleep操作是為了控制兩個線程的執(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)建出來后,對父線程中inheritThreadLocals的修改操作,對子線程不可見。
總結(jié)
- ThreadLocal不可繼承,threadLocals是當前線程的成員變量,在子線程中不可見。
- InheritableThreadLocal可繼承,原理是:在新建子線程的時候,將父線程中inheritThreadLocals所有的entry拷貝給了子線程。
- 子線程創(chuàng)建出來后,對父線程中inheritThreadLocals的修改操作,對子線程不可見。
4.存在的內(nèi)存泄露問題
要充分理解ThreadLocal中存在的內(nèi)存泄露問題,需要有以下JVM對內(nèi)存管理的前置知識(這里篇幅問題就不補充了):
- 什么是內(nèi)存泄露?
- 什么是強引用?
- 什么是弱引用?
- 何時GC?
- 強引用和弱引用GC時的區(qū)別?
在分析上述ThreadLocalMap源碼的時候,注意到有一個小細節(jié),ThreadLocalMap的Entry繼承了WeakReference<ThreadLocal<?>>,也就是說Entry的key是一個對ThreadLocal<?>的弱引用。問題來了,為什么這里要使用弱引用呢?
使用強引用會如何?
現(xiàn)在假設(shè)Entry的key是一個對ThreadLocal的強引用,當ThreadLocal對象使用完后,外部的強引用不存在,但是因為當前線程對象中的threadLocals還持有ThreadLocal的強引用,而threadLocals的生命周期是和線程一致的,這個時候,如果沒有手動刪除,整個Entry就發(fā)生了內(nèi)存泄露。
使用弱引用會如何?
現(xiàn)在假設(shè)Entry的key是一個對ThreadLocal的弱引用,當ThreadLocal對象使用完后,外部的強引用不存在,此時ThreadLocal對象只存在Entry中key對它的弱引用,在下次GC的時候,這個ThreadLocal對象就會被回收,導致key為null,此時value的強引用還存在,但是value已經(jīng)不會被使用了,如果沒有手動刪除,那么這個Entry中的key就會發(fā)生內(nèi)存泄露。
使用弱引用還有一些好處,那就是,當key為null時, ThreadLocalMap中最多存在一個key為null,并且當調(diào)用set(),get(),remove()這些方法的時候,是會清除掉key為null的entry的。
set()、get()、remove() 方法中相關(guān)實現(xiàn)
- 從下可以發(fā)現(xiàn),set方法首先會進入一個循環(huán)。
- 在這個循環(huán)中,會遍歷整個Entry數(shù)組。直到遇到一個空的entry,退出循環(huán)。
- 當遇到已存在的key'時,會直接替換value,然后返回。
- 當遇到key為空的entry的時候,會直接將當前的entry存在這個過時的entry中,然后返回。
通過這個方法的源碼可以看出,key為null的那個entry實際上遲早會被替換成新的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; // 替換過時的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)]) { // 清除過時的key if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }
總結(jié)
- ThreadLocal如果對ThreadLocalMap的key使用強引用,那么會存在整個entry發(fā)生內(nèi)存泄露的問題,如果不手動清除,那么這個不被使用的entry會一直存在。
- ThreadLocal如果對ThreadLocalMap的key使用弱引用,那么可能會存在一個entry的value發(fā)生內(nèi)存泄露,但是在調(diào)用set(),get(),remove() 方法時,key為null的entry會被清除掉。
- 發(fā)生內(nèi)存泄露最根本的原因是:threadLocals的生命周期是和線程一致的。
- 每次使用完ThreadLocal對象后,必須調(diào)用它的remove()方法清除數(shù)據(jù)。
5.ThreadLocal應用
ThreadLocal把數(shù)據(jù)存放到線程本地,解決了線程安全問題,沒有使用鎖,直接訪問線程本地變量,效率較高(空間換時間。)
同時threadLocals的生命周期是和線程一致的,可以解決很多參數(shù)傳遞問題。
- Session管理(Mabaties使用ThreadLocal存儲session),數(shù)據(jù)庫連接。
- 如果需要跟蹤請求的整個流程,可以使用ThreadLocal來傳遞參數(shù)。
ATFWUS 2021-11-11
以上就是深入淺出解析Java ThreadLocal原理的詳細內(nèi)容,更多關(guān)于Java ThreadLocal原理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot整合MongoDB實現(xiàn)文件上傳下載刪除
這篇文章主要介紹了SpringBoot整合MongoDB實現(xiàn)文件上傳下載刪除的方法,幫助大家更好的理解和學習使用SpringBoot框架,感興趣的朋友可以了解下2021-05-05SpringBoot如何手寫一個starter并使用這個starter詳解
starter是SpringBoot中的一個新發(fā)明,它有效的降低了項目開發(fā)過程的復雜程度,對于簡化開發(fā)操作有著非常好的效果,下面這篇文章主要給大家介紹了關(guān)于SpringBoot如何手寫一個starter并使用這個starter的相關(guān)資料,需要的朋友可以參考下2022-12-12