老套路,先列舉下關於ThreadLocal常見的疑問,但願能夠經過這篇學習筆記來解決這幾個問題:html
ThreadLocal是線程局部變量,和普通變量的不一樣在於:每一個線程持有這個變量的一個副本,能夠獨立修改(set方法)和訪問(get方法)這個變量,而且線程之間不會發生衝突。java
類中定義的ThreadLocal實例通常會被private static
修飾,這樣可讓ThreadLocal實例的狀態和Thread綁定在一塊兒,業務上,通常用ThreadLocal包裝一些業務ID(user ID或事務ID)——不一樣的線程使用的ID是不相同的。面試
從某個角度來看,ThreadLocal爲Java併發編程提供了額外的思路——避免併發,若是某個對象自己是非線程安全的,可是你想實現多線程同步訪問的效果,例如SimpleDateFormat,你可使用ThreadLocal變量。apache
public class Foo { // SimpleDateFormat is not thread-safe, so give one to each thread private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){ @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyyMMdd HHmm"); } }; public String formatIt(Date date) { return formatter.get().format(date); } }
注意,這裏針對每一個線程只須要初始化一次SimpleDateFormat對象,其實跟在自定義線程中定義一個SimpleDateFormat成員變量,並在線程初始化的時候new這個對象,效果是同樣的,只是這樣看起來代碼更規整。編程
以前在yunos作酷盤項目的數據遷移時,咱們須要按照用戶維度去加鎖,每一個線程在處理遷移以前,都須要先獲取當前用戶的鎖,每一個鎖的key是帶着用戶信息的,所以也可使用ThreadLocal變量實現:後端
下面這個例子,咱們定義了一個MyRunnable對象,這個MyRunnable對象會被線程1和線程2使用,可是經過內部的ThreadLocal變量,每一個線程訪問到的整數都是本身單獨的一份。緩存
package org.java.learn.concurrent.threadlocal; /** * @author duqi * @createTime 2018-12-29 23:25 **/ public class ThreadLocalExample { public static class MyRunnable implements Runnable { private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(); @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引用會引用一個ThreadLocalMap對象,這個map中的key是ThreadLocal對象(使用WeakReference包裝),value是業務上變量的值。安全
首先看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變量指向的是一個map,這個map就是ThreadLocal.ThreadLocalMap,裏面存放的是跟當前線程綁定的ThreadLocal變量;inheritableThreadLocals的做用相同,裏面也是存放的ThreadLocal變量,可是存放的是從當前線程的父線程繼承過來的ThreadLocal變量。多線程
在看java.lang.ThreadLocal
類,主要的成員和接口以下:
withInitial方法,Java 8之後用於初始化ThreadLocal的一種方法,在外部調用get()方法的時候,會經過Supplier肯定變量的初始值;
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) { return new SuppliedThreadLocal<>(supplier); }
get方法,獲取當前線程的變量副本,若是當前線程尚未建立該變量的副本,則須要經過調用initialValue
方法來設置初始值;get方法的源代碼以下,首先經過當前線程獲取當前線程對應的map,若是map不爲空,則從map中取出對應的Entry,而後取出對應的值;若是map爲空,則調用setInitialValue設置初始值;若是map不爲空,當前ThreadLocal實例對應的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(); }
set方法,跟get方法同樣,先獲取當前線程對應的map,若是map爲空,則調用createMap建立map,不然將變量的值放入map——key爲當前這個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方法,刪除當前線程綁定的這個副本
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
/** * 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;
InheritableThreadLocal主要用於子線程建立時,須要自動繼承父線程的ThreadLocal變量,實現子線程訪問父線程的threadlocal變量。InheritableThreadLocal繼承了ThreadLocal,並重寫了childValue、getMap、createMap三個方法。
public class InheritableThreadLocal<T> extends ThreadLocal<T> { /** * 建立線程的時候,若是須要繼承且父線程中Thread-Local變量,則須要將父線程中的ThreadLocal變量一次拷貝過來。 */ protected T childValue(T parentValue) { return parentValue; } /** * 因爲重寫了getMap,因此在操做InheritableThreadLocal變量的時候,將只操做Thread類中的inheritableThreadLocals變量,與threadLocals變量沒有關係 **/ ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } /** * 跟getMap相似,set或getInheritableThreadLocal變量的時候,將只操做Thread類中的inheritableThreadLocals變量 */ void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } }
關於childValue多說兩句,拷貝是如何發生的?
首先看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方法,最終會調用到newThreadLocalMap方法,這裏InheritableThreadLocal對childValue作了重寫,能夠看出,這裏確實是將父線程關聯的ThreadLocalMap中的內容依次拷貝到子線程的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") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { 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++; } } } }
ThreadLocalMap中的key是ThreadLocal對象,而後ThreadLocal對象時被WeakReference包裝的,這樣當沒有強引用指向該ThreadLocal對象以後,或者說Map中的ThreadLocal對象被斷定爲弱引用可達時,就會在垃圾收集中被回收掉。看下Entry的定義:
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
ThreadLocal對象的生命週期跟線程的生命週期同樣長,那麼若是將ThreadLocal對象和線程池一塊兒使用,就可能會遇到這種狀況:一個線程的ThreadLocal對象會和其餘線程的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調用(invoke)的鏈路上,會先使用請求參數判斷當前線程是否剛剛發起過一樣參數的調用——這個調用會使用ThreadLocalCache保存起來。具體的看,ThreadLocalCache的實現以下:
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裏存放的是參數到結果的映射 private final ThreadLocal<Map<Object, Object>> store; public ThreadLocalCache(URL url) { this.store = new ThreadLocal<Map<Object, Object>>() { @Override protected Map<Object, Object> initialValue() { return new HashMap<Object, Object>(); } }; } @Override public void put(Object key, Object value) { store.get().put(key, value); } @Override public Object get(Object key) { return store.get().get(key); } }
在RocketMQ中,我也找到了ThreadLocal的身影,它是用在消息發送的場景,MQClientAPIImpl是RMQ中負責將消息發送到服務端的實現,其中有一個步驟須要選擇一個具體的隊列,選擇具體的隊列的時候,不一樣的線程有本身負責的index值,這裏使用了ThreadLocal的機制,能夠看下ThreadLocalIndex的實現:
package org.apache.rocketmq.client.common; import java.util.Random; public class ThreadLocalIndex { private final ThreadLocal<Integer> threadLocalIndex = new ThreadLocal<Integer>(); 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() + '}'; } }
這篇文章主要是解決了關於ThreadLocal的幾個問題:(1)具體的概念是啥?(2)在Java開發中的什麼場景下使用?(3)ThreadLocal的實現原理是怎樣的?(4)開源項目中有哪些案例能夠參考?不知道你是否對這幾個問題有了必定的瞭解呢?若是還有疑問,歡迎交流。
本號專一於後端技術、JVM問題排查和優化、Java面試題、我的成長和自我管理等主題,爲讀者提供一線開發者的工做和成長經驗,期待你能在這裏有所收穫。