老套路, 先列举下关于 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 详解 https://www.jianshu.com/p/94ba4a918ff5
ThreadLocal 详解 https://www.jianshu.com/p/3bb70ae81828
ThreadLocal 的使用场景 https://mp.weixin.qq.com/s/l2wwMWuR1oTYS8wWgFaLXg
数据结构: 哈希表
本号专注于后端技术, JVM 问题排查和优化, Java 面试题, 个人成长和自我管理等主题, 为读者提供一线开发者的工作和成长经验, 期待你能在这里有所收获.
来源: https://www.cnblogs.com/javaadu/p/11222891.html