三个重要方法:
- set()
如果没有set操作的ThreadLocal
, 很容易引起脏数据问题 - get()
始终没有get操作的ThreadLocal
对象是没有意义的 - remove()
如果没有remove操作,则容易引起内存泄漏 - 如果ThreadLocal是非静态的,属于某个线程实例,那就失去了线程间共享的本质属性;
那么ThreadLocal到底有什么作用呢?
我们知道,局部变量在方法内各个代码块间进行传递,而类变量在类内方法间进行传递;
复杂的线程方法可能需要调用很多方法来实现某个功能,这时候用什么来传递线程内变量呢?
即ThreadLocal,它通常用于同一个线程内,跨类、跨方法传递数据;
如果没有ThreadLocal,那么相互之间的信息传递,势必要靠返回值和参数,这样无形之中,有些类甚至有些框架会互相耦合;
通过将Thread构造方法的最后一个参数设置为true,可以把当前线程的变量继续往下传递给它创建的子线程
public Thread (ThreadGroup group, Runnable target, String name,long stackSize, boolean inheritThreadLocals) [ this (group, target, name, stackSize, null, inheritThreadLocals) ; }
parent为其父线程
if (inheritThreadLocals && parent. inheritableThreadLocals != null) this. inheritableThreadLocals = ThreadLocal. createInheritedMap (parent. inheritableThreadLocals) ;
createlnheritedMap()
其实就是调用ThreadLocalMap
的私有构造方法来产生一个实例对象,把父线程中不为null
的线程变量都拷贝过来
private ThreadLocalMap (ThreadLocalMap parentMap) { // table就是存储 Entry[] parentTable = parentMap. table; int len = parentTable. length; setThreshold(len) ; table = new Entry[len]; for (Entry e : parentTable) { if (e != null) { 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来透传全局上下文的;
比如用ThreadLocal来存储监控系统的某个标记位,暂且命名为traceld.
某次请求下所有的traceld都是一致的,以获得可以统一解析的日志文件;
但在实际开发过程中,发现子线程里的traceld为null,跟主线程的traceld并不一致,所以这就需要刚才说到的InheritableThreadLocal来解决父子线程之间共享线程变量的问题,使整个连接过程中的traceld一致。
示例代码如下
import org.apache.commons.lang3.StringUtils; /** * @author sss * @date 2019/1/17 */ public class RequestProcessTrace { private static final InheritableThreadLocal<FullLinkContext> FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL = new InheritableThreadLocal<FullLinkContext>(); public static FullLinkContext getContext() { FullLinkContext fullLinkContext = FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.get(); if (fullLinkContext == null) { FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.set(new FullLinkContext()); fullLinkContext = FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.get(); } return fullLinkContext; } private static class FullLinkContext { private String traceId; public String getTraceId() { if (StringUtils.isEmpty(traceId)) { FrameWork.startTrace(null, "JavaEdge"); traceId = FrameWork.getTraceId(); } return traceId; } public void setTraceId(String traceId) { this.traceId = traceId; } } }
ThreadLocal的副作用
为了使线程安全地共享某个变量,JDK给出了ThreadLocal.
但ThreadLocal的主要问题是会产生脏数据和内存泄漏;
这两个问题通常是在线程池的线程中使用ThreadLocal引发的,因为线程池有线程复用和内存常驻两是在线程池的线程中使用ThreadLocal 引发的,因为线程池有线程复用和内存常驻两个特点
脏数据
线程复用会产生脏数据。
由于线程池会重用 Thread 对象,与 Thread 绑定的静态属性 ThreadLocal 变量也会被重用。
如果在实现的线程run()方法中不显式调用remove()清理与线程相关的ThreadLocal信息,那么若下一个线程不调用set(),就可能get() 到重用的线程信息。包括ThreadLocal所关联的线程对象的value值。
脏读案例
比如,用户A下单后没有看到订单记录,而用户B却看到了用户A的订单记录。通过排查发现是由于 session 优化引发。
在原来的请求过程中,用户每次请求Server,都需要通过 sessionId 去缓存里查询用户的session信息,这样无疑增加了一次调用。
因此工程师决定采用某框架来缓存每个用户对应的SecurityContext,它封装了session 相关信息。优化后虽然会为每个用户新建一个 session 相关的上下文,但由于Threadlocal没有在线程处理结束时及时remove()。在高并发场景下,线程池中的线程可能会读取到上一个线程缓存的用户信息。
示例代码
输出结果
重用错误案例
生产环境中,有时获取到的用户信息是别人的。查看代码后,发现是使用了ThreadLocal
缓存获取到的用户信息。
ThreadLocal
适用于变量在线程间隔离,而在方法或类间共享的场景。
若用户信息的获取比较昂贵(比如从DB查询),则在ThreadLocal
中缓存比较合适。
问题来了,为什么有时会出现用户信息错乱?
1.1 案例
使用Spring Boot创建一个Web应用程序,使用ThreadLocal存放一个Integer值,代表需要在线程中保存的用户信息,这个值初始是null。在业务逻辑中,我先从ThreadLocal获取一次值,然后把外部传入的参数设置到ThreadLocal中,来模拟从当前上下文获取到用户信息的逻辑,随后再获取一次值,最后输出两次获得的值和线程名称。
固定思维认为,在设置用户信息前第一次获取的值始终是null,但要清楚程序运行在Tomcat,执行程序的线程是Tomcat的工作线程,其基于线程池。
而线程池会重用固定线程,一旦线程重用,那么很可能首次从ThreadLocal获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal中的用户信息就是其他用户的信息。
1.2 bug 重现
在配置文件设置Tomcat参数-工作线程池最大线程数设为1,这样始终是同一线程在处理请求:
server.tomcat.max-threads=1
先让用户1请求接口,第一、第二次获取到用户ID分别是null和1,符合预期
用户2请求接口,bug复现!第一、第二次获取到用户ID分别是1和2,显然第一次获取到了用户1的信息,因为Tomcat线程池重用了线程。两次请求线程都是同一线程:http-nio-45678-exec-1
。
写业务代码时,首先要理解代码会跑在什么线程上:
- Tomcat服务器下跑的业务代码,本就运行在一个多线程环境(否则接口也不可能支持这么高的并发),并不能认为没有显式开启多线程就不会有线程安全问题
- 线程创建较昂贵,所以Web服务器会使用线程池处理请求,线程会被重用。使用类似ThreadLocal工具存放数据时,需注意在代码运行完后,显式清空设置的数据。