为了帮助大家在项目中更好使用ThreadLocal,本文向大家介绍ThreadLocal原理和常见问题,具体内容如下:
ThreadLocal是什么
ThreadLocal的应用场景
ThreadLocal的内部原理
ThreadLocal内存泄露问题
父子线程如何共享数据
ThreadLocal是什么
java.lang.ThreadLocal是JDK中提供的一个类,在并发编程中,为解决线程安全问题提供了一种用空间换时间的新思路。
在一些高并发的场景中,如果需要对公共资源进行操作,我们第一时间就会想到使用synchronized或Lock,给访问公共资源的代码上锁,来保证了代码的原子性。但是多个线程同时竞争同一把锁的时候,可能会造成大量的锁等待,可能会浪费很多时间,让系统的响应时间变慢。这个时候我们就可以考虑是否可以使用ThreadLocal。
将类变量放到ThreadLocal类型的对象中,就可以使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。
ThreadLocal的应用场景
1、全局存储用户信息
当用户登陆后,会将用户信息存入token中并返回给前端,当用户调用需要授权的接口时,前端会把token放到header中去请求后端接口,后端在拦截器中解析token,获取用户信息,然后存放到某个工具类的ThreadLocal变量中,后续执行代码过程中,就不需要关注如何获取用户信息,只需要使用工具类get方法就可以获取。
2、SimpleDateFormat与ThreadLocal结合使用
大家都知道SimpleDateFormat是不安全类,但是做一些日期处理的时候又经常会用到这个类,这个时候我们可以与ThreadLocal结合进行使用,从而避免了线程安全问题。
// 这样来申明全局变量
public final static ThreadLocal<SimpleDateFormat> DF = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
3、org.slf4j.MDC处理链路追踪ID
MDC类本身包含了一个MDCAdapter对象,此对象里又包含了一个ThreadLocal的全局变量,可以用来处理日志相关的信息。例如常见的我们处理traceId的时候,先在过滤器里生成traceId并放入MDC,然后在日志文件里配置traceId,这样我们打印日志的时候就可以看到traceId了。
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
// 此处生成traceId...
// 将traceId放入MDC
MDC.put("traceId", traceId);
try {
filterChain.doFilter(servletRequest, servletResponse);
} catch (Exception e) {
MDC.remove("traceId");
}
<!-- 控制台日志 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %level %C.%M[%F:%L] traceId[%X{traceId:-default}] - %msg%n
</Pattern>
</encoder>
</appender>
ThreadLocal的内部原理
想要搞清楚ThreadLocal的底层实现原理,我们不得不扒一下jdk源码。先看一下ThreadLocal类,发现有set、get、remove三个关键的方法:
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的成员变量ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//如果map不为空
if (map != null)
//将值设置到map中,key是this,即threadLocal对象,value是传入的value值
map.set(this, value);
else
//如果map为空,则需要创建新的map对象
createMap(t, value);
}
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的成员变量ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//根据threadLocal对象从map中获取Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//获取保存的数据
T result = (T)e.value;
return result;
}
}
//初始化数据
return setInitialValue();
}
public void remove() {
//获取当前线程的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//如果ThreadLocalMap对象不为空,则删除,key是this,即threadLocal对象
m.remove(this);
}
从以上源码中,可以看到set、get、remove方法实际上都是在操作ThreadLocalMap对象中的数据,而ThreadLocalMap对象又是从当前线程中获取到的,所以不同的线程之间,ThreadLocalMap对象中的数据是隔离开的。
ThreadLocal内存泄露问题
首先一句话简单概括什么是内存泄露:由于某种原因,导致程序中的某些资源(内存)无法得到释放,因此造成了系统内存泄露。
在真正解释内存泄露之前我们还得简单回顾一下JAVA中的四种引用类型的区别:
1、强引用:发生了GC,对象也不会被回收
2、软引用(SoftReference):发生了GC,如果内存足够,则对象不会被回收;反之,会被回收
3、弱引用(WeakReference):发生了GC,无论内存是否足够,对象都会被回收
4、虚引用(PhantomReference):所指向的对象获取不到、拿出来是null,因此也叫幽灵对象
先来看下ThreadLocalMap的结构
static class ThreadLocalMap {
//此处省略...
//可以看出Entry继承了弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//此处省略...
}
那么问题来了,ThreadLocalMap的Entry为什么使用了弱引用,跟内存泄露又有什么关系呢?
我们分别来看假设key是强引用和原本的弱引用情况下怎么样会发生内存泄漏
从上面的对比,我们可以看到,ThreadLocal 内存泄漏的根源是:由于ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除(remove()方法)对应 key 就会导致内存泄漏。要避免内存泄漏有两种方式:
1、使用完ThreadLocal,调用其remove方法删除对应的Entry
2、使用完ThreadLocal,当前Thread也随之运行结束
第一种方式很好理解。但是第二种方式显然就不好控制,特别是使用线程池的时候,线程执行代码结束后是不会销毁的。也就是说,只要记得在使用完ThreadLocal后及时的调用 remove,无论 key 是强引用还是弱引用都不会有问题。
那么为什么key要用弱引用呢?事实上,在ThreadLocalMap中,只要调用了它的get、set或remove三个方法中的任何一个方法,都会自动触发清理机制,将key为null的value值清空,如果key和value都是null,那么Entry对象会被GC回收。如果所有的Entry对象都被回收了,ThreadLocalMap也会被回收了,这样就能最大程度(就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收.对应value在下一次ThreadLocaIMap调用 set/get/remove中的任一方法的时候会被清除,从而避免内存泄漏)的解决内存泄露问题。
// 这里是清除过期的entry的核心方法
private int expungeStaleEntry(int staleSlot) {
//此处省略...
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
// 当k为null时,把value也改为null
e.value = null;
tab[i] = null;
size--;
} else {
//此处省略...
}
}
return i;
}
父子线程如何共享数据
前面介绍的ThreadLocal都是在一个线程中保存和获取数据的。但在实际工作中,有可能是在父子线程中共享数据的。即在父线程中往ThreadLocal设置了值,在子线程中能够获取到。
public class ThreadLocalTest {
public static void main(String[] args) {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
threadLocal.set(6);
System.out.println("父线程获取数据:" + threadLocal.get());
new Thread(() -> {
System.out.println("子线程获取数据:" + threadLocal.get());
}).start();
}
}
执行结果:
父线程获取数据:6
子线程获取数据:null
你会发现,在这种情况下使用ThreadLocal是行不通的。main方法是在主线程中执行的,相当于父线程。在main方法中开启了另外一个线程,相当于子线程。显然通过ThreadLocal,无法在父子线程中共享数据。那么这个时候可以使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类。
修改代码之后:
public class ThreadLocalTest {
public static void main(String[] args) {
InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
threadLocal.set(6);
System.out.println("父线程获取数据:" + threadLocal.get());
new Thread(() -> {
System.out.println("子线程获取数据:" + threadLocal.get());
}).start();
}
}
执行结果:
父线程获取数据:6
子线程获取数据:6