共计 9018 个字符,预计需要花费 23 分钟才能阅读完成。
老套路,先列举下关于 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 面试题、个人成长和自我管理等主题,为读者提供一线开发者的工作和成长经验,期待你能在这里有所收获。