您好,登錄后才能下訂單哦!
老套路,先列舉下關(guān)于ThreadLocal常見(jiàn)的疑問(wèn),希望可以通過(guò)這篇學(xué)習(xí)筆記來(lái)解決這幾個(gè)問(wèn)題:
ThreadLocal是線(xiàn)程局部變量,和普通變量的不同在于:每個(gè)線(xiàn)程持有這個(gè)變量的一個(gè)副本,可以獨(dú)立修改(set方法)和訪(fǎng)問(wèn)(get方法)這個(gè)變量,并且線(xiàn)程之間不會(huì)發(fā)生沖突。
類(lèi)中定義的ThreadLocal實(shí)例一般會(huì)被private static
修飾,這樣可以讓ThreadLocal實(shí)例的狀態(tài)和Thread綁定在一起,業(yè)務(wù)上,一般用ThreadLocal包裝一些業(yè)務(wù)ID(user ID或事務(wù)ID)——不同的線(xiàn)程使用的ID是不相同的。
從某個(gè)角度來(lái)看,ThreadLocal為Java并發(fā)編程提供了額外的思路——避免并發(fā),如果某個(gè)對(duì)象本身是非線(xiàn)程安全的,但是你想實(shí)現(xiàn)多線(xiàn)程同步訪(fǎng)問(wèn)的效果,例如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);
}
}
注意,這里針對(duì)每個(gè)線(xiàn)程只需要初始化一次SimpleDateFormat對(duì)象,其實(shí)跟在自定義線(xiàn)程中定義一個(gè)SimpleDateFormat成員變量,并在線(xiàn)程初始化的時(shí)候new這個(gè)對(duì)象,效果是一樣的,只是這樣看起來(lái)代碼更規(guī)整。
之前在yunos做酷盤(pán)項(xiàng)目的數(shù)據(jù)遷移時(shí),我們需要按照用戶(hù)維度去加鎖,每個(gè)線(xiàn)程在處理遷移之前,都需要先獲取當(dāng)前用戶(hù)的鎖,每個(gè)鎖的key是帶著用戶(hù)信息的,因此也可以使用ThreadLocal變量實(shí)現(xiàn):
下面這個(gè)例子,我們定義了一個(gè)MyRunnable對(duì)象,這個(gè)MyRunnable對(duì)象會(huì)被線(xiàn)程1和線(xiàn)程2使用,但是通過(guò)內(nèi)部的ThreadLocal變量,每個(gè)線(xiàn)程訪(fǎng)問(wèn)到的整數(shù)都是自己?jiǎn)为?dú)的一份。
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是如何被線(xiàn)程使用的?原理如下圖所示:Thread引用和ThreadLocal引用都在棧上,Thread引用會(huì)引用一個(gè)ThreadLocalMap對(duì)象,這個(gè)map中的key是ThreadLocal對(duì)象(使用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)前線(xiàn)程綁定的ThreadLocal變量;inheritableThreadLocals的作用相同,里面也是存放的ThreadLocal變量,但是存放的是從當(dāng)前線(xiàn)程的父線(xiàn)程繼承過(guò)來(lái)的ThreadLocal變量。
在看java.lang.ThreadLocal
類(lèi),主要的成員和接口如下:
withInitial方法,Java 8以后用于初始化ThreadLocal的一種方法,在外部調(diào)用get()方法的時(shí)候,會(huì)通過(guò)Supplier確定變量的初始值;
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
get方法,獲取當(dāng)前線(xiàn)程的變量副本,如果當(dāng)前線(xiàn)程還沒(méi)有創(chuàng)建該變量的副本,則需要通過(guò)調(diào)用initialValue
方法來(lái)設(shè)置初始值;get方法的源代碼如下,首先通過(guò)當(dāng)前線(xiàn)程獲取當(dāng)前線(xiàn)程對(duì)應(yīng)的map,如果map不為空,則從map中取出對(duì)應(yīng)的Entry,然后取出對(duì)應(yīng)的值;如果map為空,則調(diào)用setInitialValue設(shè)置初始值;如果map不為空,當(dāng)前ThreadLocal實(shí)例對(duì)應(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)前線(xiàn)程對(duì)應(yīng)的map,如果map為空,則調(diào)用createMap創(chuàng)建map,否則將變量的值放入map——key為當(dāng)前這個(gè)ThreadLocal對(duì)象,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)前線(xiàn)程綁定的這個(gè)副本
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主要用于子線(xiàn)程創(chuàng)建時(shí),需要自動(dòng)繼承父線(xiàn)程的ThreadLocal變量,實(shí)現(xiàn)子線(xiàn)程訪(fǎng)問(wèn)父線(xiàn)程的threadlocal變量。InheritableThreadLocal繼承了ThreadLocal,并重寫(xiě)了childValue、getMap、createMap三個(gè)方法。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* 創(chuàng)建線(xiàn)程的時(shí)候,如果需要繼承且父線(xiàn)程中Thread-Local變量,則需要將父線(xiàn)程中的ThreadLocal變量一次拷貝過(guò)來(lái)。
*/
protected T childValue(T parentValue) {
return parentValue;
}
/**
* 由于重寫(xiě)了getMap,所以在操作InheritableThreadLocal變量的時(shí)候,將只操作Thread類(lèi)中的inheritableThreadLocals變量,與threadLocals變量沒(méi)有關(guān)系
**/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* 跟getMap類(lèi)似,set或getInheritableThreadLocal變量的時(shí)候,將只操作Thread類(lèi)中的inheritableThreadLocals變量
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
關(guān)于childValue多說(shuō)兩句,拷貝是如何發(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方法,最終會(huì)調(diào)用到newThreadLocalMap方法,這里InheritableThreadLocal對(duì)childValue做了重寫(xiě),可以看出,這里確實(shí)是將父線(xiàn)程關(guān)聯(lián)的ThreadLocalMap中的內(nèi)容依次拷貝到子線(xiàn)程的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對(duì)象,然后ThreadLocal對(duì)象時(shí)被WeakReference包裝的,這樣當(dāng)沒(méi)有強(qiáng)引用指向該ThreadLocal對(duì)象之后,或者說(shuō)Map中的ThreadLocal對(duì)象被判定為弱引用可達(dá)時(shí),就會(huì)在垃圾收集中被回收掉??聪翬ntry的定義:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal對(duì)象的生命周期跟線(xiàn)程的生命周期一樣長(zhǎng),那么如果將ThreadLocal對(duì)象和線(xiàn)程池一起使用,就可能會(huì)遇到這種情況:一個(gè)線(xiàn)程的ThreadLocal對(duì)象會(huì)和其他線(xiàn)程的ThreadLocal對(duì)象串掉,一般不建議將兩者一起使用。
我從Dubbo中找到了ThreadLocal的例子,它主要是用在請(qǐng)求緩存的場(chǎng)景,具體代碼如下:
@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)的鏈路上,會(huì)先使用請(qǐng)求參數(shù)判斷當(dāng)前線(xiàn)程是否剛剛發(fā)起過(guò)同樣參數(shù)的調(diào)用——這個(gè)調(diào)用會(huì)使用ThreadLocalCache保存起來(lái)。具體的看,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 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的身影,它是用在消息發(fā)送的場(chǎng)景,MQClientAPIImpl是RMQ中負(fù)責(zé)將消息發(fā)送到服務(wù)端的實(shí)現(xiàn),其中有一個(gè)步驟需要選擇一個(gè)具體的隊(duì)列,選擇具體的隊(duì)列的時(shí)候,不同的線(xiàn)程有自己負(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<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() +
'}';
}
}
這篇文章主要是解決了關(guān)于ThreadLocal的幾個(gè)問(wèn)題:(1)具體的概念是啥?(2)在Java開(kāi)發(fā)中的什么場(chǎng)景下使用?(3)ThreadLocal的實(shí)現(xiàn)原理是怎樣的?(4)開(kāi)源項(xiàng)目中有哪些案例可以參考?不知道你是否對(duì)這幾個(gè)問(wèn)題有了一定的了解呢?如果還有疑問(wèn),歡迎交流。
本號(hào)專(zhuān)注于后端技術(shù)、JVM問(wèn)題排查和優(yōu)化、Java面試題、個(gè)人成長(zhǎng)和自我管理等主題,為讀者提供一線(xiàn)開(kāi)發(fā)者的工作和成長(zhǎng)經(jīng)驗(yàn),期待你能在這里有所收獲。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。