ThreadLocal使用與原理
在處理多線程并發(fā)安全的方法中,最常用的方法,就是使用鎖,通過(guò)鎖來(lái)控制多個(gè)不同線程對(duì)臨界區(qū)的訪問(wèn)。
但是,無(wú)論是什么樣的鎖,樂(lè)觀鎖或者悲觀鎖,都會(huì)在并發(fā)沖突的時(shí)候?qū)π阅墚a(chǎn)生一定的影響。
那有沒(méi)有一種方法,可以徹底避免競(jìng)爭(zhēng)呢?
答案是肯定的,這就是ThreadLocal。
從字面意思上看,ThreadLocal可以解釋成線程的局部變量,也就是說(shuō)一個(gè)ThreadLocal的變量只有當(dāng)前自身線程可以訪問(wèn),別的線程都訪問(wèn)不了,那么自然就避免了線程競(jìng)爭(zhēng)。
因此,ThreadLocal提供了一種與眾不同的線程安全方式,它不是在發(fā)生線程沖突時(shí)想辦法解決沖突,而是徹底的避免了沖突的發(fā)生。
ThreadLocal的基本使用
創(chuàng)建一個(gè)ThreadLocal對(duì)象:
- private ThreadLocal<Integer> localInt = new ThreadLocal<>();
上述代碼創(chuàng)建一個(gè)localInt變量,由于ThreadLocal是一個(gè)泛型類(lèi),這里指定了localInt的類(lèi)型為整數(shù)。
下面展示了如果設(shè)置和獲取這個(gè)變量的值:
- public int setAndGet(){
- localInt.set(8);
- return localInt.get();
- }
上述代碼設(shè)置變量的值為8,接著取得這個(gè)值。
由于ThreadLocal里設(shè)置的值,只有當(dāng)前線程自己看得見(jiàn),這意味著你不可能通過(guò)其他線程為它初始化值。為了彌補(bǔ)這一點(diǎn),ThreadLocal提供了一個(gè)withInitial()方法統(tǒng)一初始化所有線程的ThreadLocal的值:
- private ThreadLocal<Integer> localInt = ThreadLocal.withInitial(() -> 6);
上述代碼將ThreadLocal的初始值設(shè)置為6,這對(duì)全體線程都是可見(jiàn)的。
ThreadLocal的實(shí)現(xiàn)原理
ThreadLocal變量只在單個(gè)線程內(nèi)可見(jiàn),那它是如何做到的呢?我們先從最基本的get()方法說(shuō)起:
- public T get() {
- //獲得當(dāng)前線程
- Thread t = Thread.currentThread();
- //每個(gè)線程 都有一個(gè)自己的ThreadLocalMap,
- //ThreadLocalMap里就保存著所有的ThreadLocal變量
- ThreadLocalMap map = getMap(t);
- if (map != null) {
- //ThreadLocalMap的key就是當(dāng)前ThreadLocal對(duì)象實(shí)例,
- //多個(gè)ThreadLocal變量都是放在這個(gè)map中的
- ThreadLocalMap.Entry e = map.getEntry(this);
- if (e != null) {
- @SuppressWarnings("unchecked")
- //從map里取出來(lái)的值就是我們需要的這個(gè)ThreadLocal變量
- T result = (T)e.value;
- return result;
- }
- }
- // 如果map沒(méi)有初始化,那么在這里初始化一下
- return setInitialValue();
- }
可以看到,所謂的ThreadLocal變量就是保存在每個(gè)線程的map中的。這個(gè)map就是Thread對(duì)象中的threadLocals字段。如下:
- ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap是一個(gè)比較特殊的Map,它的每個(gè)Entry的key都是一個(gè)弱引用:
- static class Entry extends WeakReference<ThreadLocal<?>> {
- /** The value associated with this ThreadLocal. */
- Object value;
- //key就是一個(gè)弱引用
- Entry(ThreadLocal<?> k, Object v) {
- super(k);
- value = v;
- }
- }
這樣設(shè)計(jì)的好處是,如果這個(gè)變量不再被其他對(duì)象使用時(shí),可以自動(dòng)回收這個(gè)ThreadLocal對(duì)象,避免可能的內(nèi)存泄露(注意,Entry中的value,依然是強(qiáng)引用,如何回收,見(jiàn)下文分解)。
理解ThreadLocal中的內(nèi)存泄漏問(wèn)題
雖然ThreadLocalMap中的key是弱引用,當(dāng)不存在外部強(qiáng)引用的時(shí)候,就會(huì)自動(dòng)被回收,但是Entry中的value依然是強(qiáng)引用。這個(gè)value的引用鏈條如下:
可以看到,只有當(dāng)Thread被回收時(shí),這個(gè)value才有被回收的機(jī)會(huì),否則,只要線程不退出,value總是會(huì)存在一個(gè)強(qiáng)引用。但是,要求每個(gè)Thread都會(huì)退出,是一個(gè)極其苛刻的要求,對(duì)于線程池來(lái)說(shuō),大部分線程會(huì)一直存在在系統(tǒng)的整個(gè)生命周期內(nèi),那樣的話,就會(huì)造成value對(duì)象出現(xiàn)泄漏的可能。處理的方法是,在ThreadLocalMap進(jìn)行set(),get(),remove()的時(shí)候,都會(huì)進(jìn)行清理:
以getEntry()為例:
- private Entry getEntry(ThreadLocal<?> key) {
- int i = key.threadLocalHashCode & (table.length - 1);
- Entry e = table[i];
- if (e != null && e.get() == key)
- //如果找到key,直接返回
- return e;
- else
- //如果找不到,就會(huì)嘗試清理,如果你總是訪問(wèn)存在的key,那么這個(gè)清理永遠(yuǎn)不會(huì)進(jìn)來(lái)
- return getEntryAfterMiss(key, i, e);
- }
下面是getEntryAfterMiss()的實(shí)現(xiàn):
- private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
- Entry[] tab = table;
- int len = tab.length;
- while (e != null) {
- // 整個(gè)e是entry ,也就是一個(gè)弱引用
- ThreadLocal<?> k = e.get();
- //如果找到了,就返回
- if (k == key)
- return e;
- if (k == null)
- //如果key為null,說(shuō)明弱引用已經(jīng)被回收了
- //那么就要在這里回收里面的value了
- expungeStaleEntry(i);
- else
- //如果key不是要找的那個(gè),那說(shuō)明有hash沖突,這里是處理沖突,找下一個(gè)entry
- i = nextIndex(i, len);
- e = tab[i];
- }
- return null;
- }
真正用來(lái)回收value的是expungeStaleEntry()方法,在remove()和set()方法中,都會(huì)直接或者間接調(diào)用到這個(gè)方法進(jìn)行value的清理:
從這里可以看到,ThreadLocal為了避免內(nèi)存泄露,也算是花了一番大心思。不僅使用了弱引用維護(hù)key,還會(huì)在每個(gè)操作上檢查key是否被回收,進(jìn)而再回收value。
但是從中也可以看到,ThreadLocal并不能100%保證不發(fā)生內(nèi)存泄漏。
比如,很不幸的,你的get()方法總是訪問(wèn)固定幾個(gè)一直存在的ThreadLocal,那么清理動(dòng)作就不會(huì)執(zhí)行,如果你沒(méi)有機(jī)會(huì)調(diào)用set()和remove(),那么這個(gè)內(nèi)存泄漏依然會(huì)發(fā)生。
因此,一個(gè)良好的習(xí)慣依然是:當(dāng)你不需要這個(gè)ThreadLocal變量時(shí),主動(dòng)調(diào)用remove(),這樣對(duì)整個(gè)系統(tǒng)是有好處的。
ThreadLocalMap中的Hash沖突處理
ThreadLocalMap作為一個(gè)HashMap和java.util.HashMap的實(shí)現(xiàn)是不同的。對(duì)于java.util.HashMap使用的是鏈表法來(lái)處理沖突:
但是,對(duì)于ThreadLocalMap,它使用的是簡(jiǎn)單的線性探測(cè)法,如果發(fā)生了元素沖突,那么就使用下一個(gè)槽位存放:
具體來(lái)說(shuō),整個(gè)set()的過(guò)程如下:
可以被繼承的ThreadLocal——InheritableThreadLocal
在實(shí)際開(kāi)發(fā)過(guò)程中,我們可能會(huì)遇到這么一種場(chǎng)景。主線程開(kāi)了一個(gè)子線程,但是我們希望在子線程中可以訪問(wèn)主線程中的ThreadLocal對(duì)象,也就是說(shuō)有些數(shù)據(jù)需要進(jìn)行父子線程間的傳遞。比如像這樣:
- public static void main(String[] args) {
- ThreadLocal threadLocal = new ThreadLocal();
- IntStream.range(0,10).forEach(i -> {
- //每個(gè)線程的序列號(hào),希望在子線程中能夠拿到
- threadLocal.set(i);
- //這里來(lái)了一個(gè)子線程,我們希望可以訪問(wèn)上面的threadLocal
- new Thread(() -> {
- System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
- }).start();
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- });
- }
執(zhí)行上述代碼,你會(huì)看到:
- Thread-0:null
- Thread-1:null
- Thread-2:null
- Thread-3:null
因?yàn)樵谧泳€程中,是沒(méi)有threadLocal的。如果我們希望子線可以看到父線程的ThreadLocal,那么就可以使用InheritableThreadLocal。顧名思義,這就是一個(gè)支持線程間父子繼承的ThreadLocal,將上述代碼中的threadLocal使用InheritableThreadLocal:
- InheritableThreadLocal threadLocal = new InheritableThreadLocal();
再執(zhí)行,就能看到:
- Thread-0:0
- Thread-1:1
- Thread-2:2
- Thread-3:3
- Thread-4:4
可以看到,每個(gè)線程都可以訪問(wèn)到從父進(jìn)程傳遞過(guò)來(lái)的一個(gè)數(shù)據(jù)。雖然InheritableThreadLocal看起來(lái)挺方便的,但是依然要注意以下幾點(diǎn):
變量的傳遞是發(fā)生在線程創(chuàng)建的時(shí)候,如果不是新建線程,而是用了線程池里的線程,就不靈了
變量的賦值就是從主線程的map復(fù)制到子線程,它們的value是同一個(gè)對(duì)象,如果這個(gè)對(duì)象本身不是線程安全的,那么就會(huì)有線程安全問(wèn)題
寫(xiě)在最后的話
今天,我們介紹了ThreadLocal,ThreadLocal在Java的多線程開(kāi)發(fā)中有著十分重要的作用。
在這里,我們介紹了ThreadLocal的基本使用和實(shí)現(xiàn)原理,尤其重點(diǎn)介紹了基于當(dāng)前實(shí)現(xiàn)原理下可能存在的內(nèi)存泄漏問(wèn)題。
最后,還介紹了一個(gè)用于在父子線程間傳遞數(shù)據(jù)的特殊的ThreadLocal實(shí)現(xiàn),希望對(duì)大家有所幫助。