摘要:方法,刪除當(dāng)前線程綁定的這個(gè)副本數(shù)字,這個(gè)值是的值,普通的是使用鏈表來處理沖突的,但是是使用線性探測法來處理沖突的,就是每次增加的步長,根據(jù)參考資料所說,選擇這個(gè)數(shù)字是為了讓沖突概率最小。
老套路,先列舉下關(guān)于ThreadLocal常見的疑問,希望可以通過這篇學(xué)習(xí)筆記來解決這幾個(gè)問題:
ThreadLocal是用來解決什么問題的?
如何使用ThreadLocal?
ThreadLocal的實(shí)現(xiàn)原理是什么?
可否舉幾個(gè)實(shí)際項(xiàng)目中使用ThreadLocal的案例?
基礎(chǔ)知識ThreadLocal是線程局部變量,和普通變量的不同在于:每個(gè)線程持有這個(gè)變量的一個(gè)副本,可以獨(dú)立修改(set方法)和訪問(get方法)這個(gè)變量,并且線程之間不會發(fā)生沖突。
類中定義的ThreadLocal實(shí)例一般會被private static修飾,這樣可以讓ThreadLocal實(shí)例的狀態(tài)和Thread綁定在一起,業(yè)務(wù)上,一般用ThreadLocal包裝一些業(yè)務(wù)ID(user ID或事務(wù)ID)——不同的線程使用的ID是不相同的。
如何使用 case1從某個(gè)角度來看,ThreadLocal為Java并發(fā)編程提供了額外的思路——避免并發(fā),如果某個(gè)對象本身是非線程安全的,但是你想實(shí)現(xiàn)多線程同步訪問的效果,例如SimpleDateFormat,你可以使用ThreadLocal變量。
public class Foo { // SimpleDateFormat is not thread-safe, so give one to each thread private static final ThreadLocalformatter = new ThreadLocal (){ @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyyMMdd HHmm"); } }; public String formatIt(Date date) { return formatter.get().format(date); } }
注意,這里針對每個(gè)線程只需要初始化一次SimpleDateFormat對象,其實(shí)跟在自定義線程中定義一個(gè)SimpleDateFormat成員變量,并在線程初始化的時(shí)候new這個(gè)對象,效果是一樣的,只是這樣看起來代碼更規(guī)整。
case2之前在yunos做酷盤項(xiàng)目的數(shù)據(jù)遷移時(shí),我們需要按照用戶維度去加鎖,每個(gè)線程在處理遷移之前,都需要先獲取當(dāng)前用戶的鎖,每個(gè)鎖的key是帶著用戶信息的,因此也可以使用ThreadLocal變量實(shí)現(xiàn):
case3下面這個(gè)例子,我們定義了一個(gè)MyRunnable對象,這個(gè)MyRunnable對象會被線程1和線程2使用,但是通過內(nèi)部的ThreadLocal變量,每個(gè)線程訪問到的整數(shù)都是自己多帶帶的一份。
package org.java.learn.concurrent.threadlocal; /** * @author duqi * @createTime 2018-12-29 23:25 **/ public class ThreadLocalExample { public static class MyRunnable implements Runnable { private ThreadLocalThreadLocal關(guān)鍵知識點(diǎn) 源碼分析threadLocal = new ThreadLocal (); @Override public void run() { threadLocal.set((int) (Math.random() * 100D)); try { Thread.sleep(2000); } catch (InterruptedException e) { } System.out.println(threadLocal.get()); } } public static void main(String[] args) throws InterruptedException { MyRunnable sharedRunnableInstance = new MyRunnable(); Thread thread1 = new Thread(sharedRunnableInstance); Thread thread2 = new Thread(sharedRunnableInstance); thread1.start(); thread2.start(); thread1.join(); //wait for thread 1 to terminate thread2.join(); //wait for thread 2 to terminate } }
ThreadLocal是如何被線程使用的?原理如下圖所示:Thread引用和ThreadLocal引用都在棧上,Thread引用會引用一個(gè)ThreadLocalMap對象,這個(gè)map中的key是ThreadLocal對象(使用WeakReference包裝),value是業(yè)務(wù)上變量的值。
首先看java.lang.Thread中的代碼:
public class Thread implements Runnable { //......其他源碼 /* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is maintained by the InheritableThreadLocal class. */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; //......其他源碼
Thread中的threadLocals變量指向的是一個(gè)map,這個(gè)map就是ThreadLocal.ThreadLocalMap,里面存放的是跟當(dāng)前線程綁定的ThreadLocal變量;inheritableThreadLocals的作用相同,里面也是存放的ThreadLocal變量,但是存放的是從當(dāng)前線程的父線程繼承過來的ThreadLocal變量。
在看java.lang.ThreadLocal類,主要的成員和接口如下:
withInitial方法,Java 8以后用于初始化ThreadLocal的一種方法,在外部調(diào)用get()方法的時(shí)候,會通過Supplier確定變量的初始值;
public staticThreadLocalwithInitial(Supplier extends S> supplier) { return new SuppliedThreadLocal<>(supplier); }
get方法,獲取當(dāng)前線程的變量副本,如果當(dāng)前線程還沒有創(chuàng)建該變量的副本,則需要通過調(diào)用initialValue方法來設(shè)置初始值;get方法的源代碼如下,首先通過當(dāng)前線程獲取當(dāng)前線程對應(yīng)的map,如果map不為空,則從map中取出對應(yīng)的Entry,然后取出對應(yīng)的值;如果map為空,則調(diào)用setInitialValue設(shè)置初始值;如果map不為空,當(dāng)前ThreadLocal實(shí)例對應(yīng)的Entry為空,則也需要設(shè)置初始值。
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(); }
set方法,跟get方法一樣,先獲取當(dāng)前線程對應(yīng)的map,如果map為空,則調(diào)用createMap創(chuàng)建map,否則將變量的值放入map——key為當(dāng)前這個(gè)ThreadLocal對象,value為變量的值。
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
remove方法,刪除當(dāng)前線程綁定的這個(gè)副本
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
數(shù)字0x61c88647,這個(gè)值是HASH_INCREMENT的值,普通的hashmap是使用鏈表來處理沖突的,但是ThreadLocalMap是使用線性探測法來處理沖突的,HASH_INCREMENT就是每次增加的步長,根據(jù)參考資料1所說,選擇這個(gè)數(shù)字是為了讓沖突概率最小。
/** * The difference between successively generated hash codes - turns * implicit sequential thread-local IDs into near-optimally spread * multiplicative hash values for power-of-two-sized tables. */ private static final int HASH_INCREMENT = 0x61c88647;父子進(jìn)程數(shù)據(jù)共享
InheritableThreadLocal主要用于子線程創(chuàng)建時(shí),需要自動繼承父線程的ThreadLocal變量,實(shí)現(xiàn)子線程訪問父線程的threadlocal變量。InheritableThreadLocal繼承了ThreadLocal,并重寫了childValue、getMap、createMap三個(gè)方法。
public class InheritableThreadLocalextends ThreadLocal { /** * 創(chuàng)建線程的時(shí)候,如果需要繼承且父線程中Thread-Local變量,則需要將父線程中的ThreadLocal變量一次拷貝過來。 */ protected T childValue(T parentValue) { return parentValue; } /** * 由于重寫了getMap,所以在操作InheritableThreadLocal變量的時(shí)候,將只操作Thread類中的inheritableThreadLocals變量,與threadLocals變量沒有關(guān)系 **/ ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } /** * 跟getMap類似,set或getInheritableThreadLocal變量的時(shí)候,將只操作Thread類中的inheritableThreadLocals變量 */ void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } }
關(guān)于childValue多說兩句,拷貝是如何發(fā)生的?
首先看Thread.init方法,
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { //其他源碼 if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */ this.stackSize = stackSize; /* Set thread ID */ tid = nextThreadID(); }
然后看ThreadLocal.createInheritedMap方法,最終會調(diào)用到newThreadLocalMap方法,這里InheritableThreadLocal對childValue做了重寫,可以看出,這里確實(shí)是將父線程關(guān)聯(lián)的ThreadLocalMap中的內(nèi)容依次拷貝到子線程的ThreadLocalMap中了。
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") ThreadLocalThreadLocal對象何時(shí)被回收?
ThreadLocalMap中的key是ThreadLocal對象,然后ThreadLocal對象時(shí)被WeakReference包裝的,這樣當(dāng)沒有強(qiáng)引用指向該ThreadLocal對象之后,或者說Map中的ThreadLocal對象被判定為弱引用可達(dá)時(shí),就會在垃圾收集中被回收掉。看下Entry的定義:
static class Entry extends WeakReferenceThreadLocal和線程池一起使用?> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal> k, Object v) { super(k); value = v; } }
ThreadLocal對象的生命周期跟線程的生命周期一樣長,那么如果將ThreadLocal對象和線程池一起使用,就可能會遇到這種情況:一個(gè)線程的ThreadLocal對象會和其他線程的ThreadLocal對象串掉,一般不建議將兩者一起使用。
案例學(xué)習(xí) Dubbo中對ThreadLocal的使用我從Dubbo中找到了ThreadLocal的例子,它主要是用在請求緩存的場景,具體代碼如下:
@Activate(group = {Constants.CONSUMER, Constants.PROVIDER}, value = Constants.CACHE_KEY) public class CacheFilter implements Filter { private CacheFactory cacheFactory; public void setCacheFactory(CacheFactory cacheFactory) { this.cacheFactory = cacheFactory; } @Override public Result invoke(Invoker> invoker, Invocation invocation) throws RpcException { if (cacheFactory != null && ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.CACHE_KEY))) { Cache cache = cacheFactory.getCache(invoker.getUrl(), invocation); if (cache != null) { String key = StringUtils.toArgumentString(invocation.getArguments()); Object value = cache.get(key); if (value != null) { if (value instanceof ValueWrapper) { return new RpcResult(((ValueWrapper)value).get()); } else { return new RpcResult(value); } } Result result = invoker.invoke(invocation); if (!result.hasException()) { cache.put(key, new ValueWrapper(result.getValue())); } return result; } } return invoker.invoke(invocation); }
可以看出,在RPC調(diào)用(invoke)的鏈路上,會先使用請求參數(shù)判斷當(dāng)前線程是否剛剛發(fā)起過同樣參數(shù)的調(diào)用——這個(gè)調(diào)用會使用ThreadLocalCache保存起來。具體的看,ThreadLocalCache的實(shí)現(xiàn)如下:
package org.apache.dubbo.cache.support.threadlocal; import org.apache.dubbo.cache.Cache; import org.apache.dubbo.common.URL; import java.util.HashMap; import java.util.Map; /** * ThreadLocalCache */ public class ThreadLocalCache implements Cache { //ThreadLocal里存放的是參數(shù)到結(jié)果的映射 private final ThreadLocalRocketMQ
在RocketMQ中,我也找到了ThreadLocal的身影,它是用在消息發(fā)送的場景,MQClientAPIImpl是RMQ中負(fù)責(zé)將消息發(fā)送到服務(wù)端的實(shí)現(xiàn),其中有一個(gè)步驟需要選擇一個(gè)具體的隊(duì)列,選擇具體的隊(duì)列的時(shí)候,不同的線程有自己負(fù)責(zé)的index值,這里使用了ThreadLocal的機(jī)制,可以看下ThreadLocalIndex的實(shí)現(xiàn):
package org.apache.rocketmq.client.common; import java.util.Random; public class ThreadLocalIndex { private final ThreadLocal總結(jié)threadLocalIndex = new ThreadLocal (); private final Random random = new Random(); public int getAndIncrement() { Integer index = this.threadLocalIndex.get(); if (null == index) { index = Math.abs(random.nextInt()); if (index < 0) index = 0; this.threadLocalIndex.set(index); } index = Math.abs(index + 1); if (index < 0) index = 0; this.threadLocalIndex.set(index); return index; } @Override public String toString() { return "ThreadLocalIndex{" + "threadLocalIndex=" + threadLocalIndex.get() + "}"; } }
這篇文章主要是解決了關(guān)于ThreadLocal的幾個(gè)問題:(1)具體的概念是啥?(2)在Java開發(fā)中的什么場景下使用?(3)ThreadLocal的實(shí)現(xiàn)原理是怎樣的?(4)開源項(xiàng)目中有哪些案例可以參考?不知道你是否對這幾個(gè)問題有了一定的了解呢?如果還有疑問,歡迎交流。
參考資料Why 0x61c88647?
Java ThreadLocal
When and how should I use a ThreadLocal variable?
技術(shù)小黑屋:理解Java中的ThreadLocal
深入分析ThreadLocal的內(nèi)存泄漏問題
《Java并發(fā)編程實(shí)戰(zhàn)》
InheritableThreadLocal詳解
ThreadLocal詳解
ThreadLocal的使用場景
數(shù)據(jù)結(jié)構(gòu):哈希表
本號專注于后端技術(shù)、JVM問題排查和優(yōu)化、Java面試題、個(gè)人成長和自我管理等主題,為讀者提供一線開發(fā)者的工作和成長經(jīng)驗(yàn),期待你能在這里有所收獲。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/75524.html
摘要:如何在線程池中提交線程內(nèi)存模型相關(guān)問題什么是的內(nèi)存模型,中各個(gè)線程是怎么彼此看到對方的變量的請談?wù)動惺裁刺攸c(diǎn),為什么它能保證變量對所有線程的可見性既然能夠保證線程間的變量可見性,是不是就意味著基于變量的運(yùn)算就是并發(fā)安全的請對比下對比的異同。 并發(fā)編程高級面試面試題 showImg(https://upload-images.jianshu.io/upload_images/133416...
摘要:什么是線程餓死,什么是活鎖多線程中的忙循環(huán)是什么變量是什么變量和變量有什么不同類型變量提供什么保證能使得一個(gè)非原子操作變成原子操作嗎 JVM專題 showImg(https://segmentfault.com/img/remote/1460000019943435); (面試題+答案領(lǐng)取方式見個(gè)人主頁) Java 類加載過程? 描述一下 JVM 加載 Class 文件的原理機(jī)制? ...
摘要:作為面試官,我是如何甄別應(yīng)聘者的包裝程度語言和等其他語言的對比分析和主從復(fù)制的原理詳解和持久化的原理是什么面試中經(jīng)常被問到的持久化與恢復(fù)實(shí)現(xiàn)故障恢復(fù)自動化詳解哨兵技術(shù)查漏補(bǔ)缺最易錯過的技術(shù)要點(diǎn)大掃盲意外宕機(jī)不難解決,但你真的懂?dāng)?shù)據(jù)恢復(fù)嗎每秒 作為面試官,我是如何甄別應(yīng)聘者的包裝程度Go語言和Java、python等其他語言的對比分析 Redis和MySQL Redis:主從復(fù)制的原理詳...
閱讀 2510·2021-11-15 11:38
閱讀 1959·2021-11-05 09:37
閱讀 2281·2021-10-08 10:12
閱讀 2818·2019-08-30 15:55
閱讀 2120·2019-08-30 15:52
閱讀 1230·2019-08-29 13:24
閱讀 471·2019-08-26 18:27
閱讀 1483·2019-08-26 18:27