老套路,先列舉下關於ThreadLocal常見的疑問,希望可以通過這篇學習筆記來解決這幾個問題: 1. ThreadLocal是用來解決什麼問題的? 2. 如何使用ThreadLocal? 3. ThreadLocal的實現原理是什麼? 4. 可否舉幾個實際項目中使用ThreadLocal的案例? ...
老套路,先列舉下關於ThreadLocal常見的疑問,希望可以通過這篇學習筆記來解決這幾個問題:
- ThreadLocal是用來解決什麼問題的?
- 如何使用ThreadLocal?
- ThreadLocal的實現原理是什麼?
- 可否舉幾個實際項目中使用ThreadLocal的案例?
基礎知識
ThreadLocal是線程局部變數,和普通變數的不同在於:每個線程持有這個變數的一個副本,可以獨立修改(set方法)和訪問(get方法)這個變數,並且線程之間不會發生衝突。
類中定義的ThreadLocal實例一般會被private static
修飾,這樣可以讓ThreadLocal實例的狀態和Thread綁定在一起,業務上,一般用ThreadLocal包裝一些業務ID(user ID或事務ID)——不同的線程使用的ID是不相同的。
如何使用
case1
從某個角度來看,ThreadLocal為Java併發編程提供了額外的思路——避免併發,如果某個對象本身是非線程安全的,但是你想實現多線程同步訪問的效果,例如SimpleDateFormat,你可以使用ThreadLocal變數。
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這個對象,效果是一樣的,只是這樣看起來代碼更規整。
case2
之前在yunos做酷盤項目的數據遷移時,我們需要按照用戶維度去加鎖,每個線程在處理遷移之前,都需要先獲取當前用戶的鎖,每個鎖的key是帶著用戶信息的,因此也可以使用ThreadLocal變數實現:
case3
下麵這個例子,我們定義了一個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關鍵知識點
源碼分析
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); }
數字0x61c88647,這個值是HASH_INCREMENT的值,普通的hashmap是使用鏈表來處理衝突的,但是ThreadLocalMap是使用線性探測法來處理衝突的,HASH_INCREMENT就是每次增加的步長,根據參考資料1所說,選擇這個數字是為了讓衝突概率最小。
/**
* 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++;
}
}
}
}
ThreadLocal對象何時被回收?
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對象會和其他線程的ThreadLocal對象串掉,一般不建議將兩者一起使用。
案例學習
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調用(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
在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)開源項目中有哪些案例可以參考?不知道你是否對這幾個問題有了一定的瞭解呢?如果還有疑問,歡迎交流。
參考資料
- Why 0x61c88647?
- Java ThreadLocal
- When and how should I use a ThreadLocal variable?
- 技術小黑屋:理解Java中的ThreadLocal
- 深入分析ThreadLocal的記憶體泄漏問題
- 《Java併發編程實戰》
- InheritableThreadLocal詳解
- ThreadLocal詳解
- ThreadLocal的使用場景
- 數據結構:哈希表
本號專註於後端技術、JVM問題排查和優化、Java面試題、個人成長和自我管理等主題,為讀者提供一線開發者的工作和成長經驗,期待你能在這裡有所收穫。