关于ThreadLocal的使用
本文参考微信公众号文章链接:https://mp.weixin.qq.com/s/UNKVgvlWb3RRCHFEZlGO5w
项目实战为开发中的例子
ThreadLocal是什么?
ThreadLocal,即线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是在操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了并发场景下的线程安全问题。
为什么要使用ThreadLocal
并发场景下,会存在多个线程同时修改一个共享变量的场景。这就可能会出现线性安全问题。为了解决线性安全问题,可以用加锁的方式,比如使用synchronized 或者Lock。但是加锁的方式,可能会导致系统变慢。
还有另外一种方案,就是使用空间换时间的方式,即使用ThreadLocal。使用ThreadLocal类访问共享变量时,会在每个线程的本地,都保存一份共享变量的拷贝副本。多线程对共享变量修改时,实际上操作的是这个变量副本,从而保证线性安全。
ThreadLocal的内存结构图

从内存结构图,我们可以看到:
Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。- 并发多线程场景下,每个线程
Thread,在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而可以实现了线程隔离。
了解完这几个核心方法后,有些小伙伴可能会有疑惑,ThreadLocalMap为什么要用ThreadLocal作为key呢?直接用线程Id不一样嘛?
因为一个线程下有多个ThreadLocal的变量时,使用线程ID区分不出来了。
ThreadLocal为什么会导致内存泄露?
先来看看TreadLocal的引用示意图:

ThreadLocalMap使用ThreadLocal的弱引用作为key,当ThreadLocal变量被手动设置为null,即一个ThreadLocal没有外部强引用来引用它,当系统GC时,ThreadLocal一定会被回收。这样的话,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如线程池的核心线程),这些key为null的Entry的value就会一直存在一条强引用链:Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object 永远无法回收,造成内存泄漏。
实际上,ThreadLocalMap的设计中已经考虑到这种情况。所以也加上了一些防护措施:即在ThreadLocal的get,set,remove方法,都会清除线程ThreadLocalMap里所有key为null的value。
key是弱引用,GC回收会影响ThreadLocal的正常工作嘛?
其实不会的,因为有ThreadLocal变量引用着它,是不会被GC回收的,除非手动把ThreadLocal变量设置为null,我们可以跑个demo来验证一下:
public class WeakReferenceTest {
public static void main(String[] args) {
Object object = new Object();
WeakReference<Object> testWeakReference = new WeakReference<>(object);
System.out.println("GC回收之前,弱引用:"+testWeakReference.get());
//触发系统垃圾回收
System.gc();
System.out.println("GC回收之后,弱引用:"+testWeakReference.get());
//手动设置为object对象为null
object=null;
System.gc();
System.out.println("对象object设置为null,GC回收之后,弱引用:"+testWeakReference.get());
}
}
运行结果:
GC回收之前,弱引用:java.lang.Object@7b23ec81
GC回收之后,弱引用:java.lang.Object@7b23ec81
对象object设置为null,GC回收之后,弱引用:null
hreadLocal内存泄漏的demo
public class ThreadLocalTestDemo {
private static ThreadLocal<TianLuoClass> tianLuoThreadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
for (int i = 0; i < 10; ++i) {
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("创建对象:");
TianLuoClass tianLuoClass = new TianLuoClass();
tianLuoThreadLocal.set(tianLuoClass);
tianLuoClass = null; //将对象设置为 null,表示此对象不在使用了
// tianLuoThreadLocal.remove();
}
});
Thread.sleep(1000);
}
}
static class TianLuoClass {
// 100M
private byte[] bytes = new byte[100 * 1024 * 1024];
}
}
创建对象:
创建对象:
创建对象:
创建对象:
Exception in thread "pool-1-thread-4" java.lang.OutOfMemoryError: Java heap space
at com.example.dto.ThreadLocalTestDemo$TianLuoClass.<init>(ThreadLocalTestDemo.java:33)
at com.example.dto.ThreadLocalTestDemo$1.run(ThreadLocalTestDemo.java:21)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
我们这里没有手动设置tianLuoThreadLocal变量为null,但是还是会内存泄漏。因为我们使用了线程池,线程池有很长的生命周期,因此线程池会一直持有tianLuoClass对象的value值,即使设置tianLuoClass = null;引用还是存在的。这就好像,你把一个个对象object放到一个list列表里,然后再单独把object设置为null的道理是一样的,列表的对象还是存在的。
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
Object object = new Object();
list.add(object);
object = null;
System.out.println(list.size());
}
//运行结果
1
Entry的Key为什么要设计成弱引用呢?
我们先来回忆一下四种引用:
- 强引用:我们平时
new了一个对象就是强引用,例如Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。 - 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
- 弱引用:具有弱引用的对象拥有更短暂的生命周期。如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)。
- 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
下面我们分情况讨论:
- 如果
Key使用强引用:当ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用的话,如果没有手动删除,ThreadLocal就不会被回收,会出现Entry的内存泄漏问题。 - 如果
Key使用弱引用:当ThreadLocal的对象被回收了,因为ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value则在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
因此可以发现,使用弱引用作为Entry的Key,可以多一层保障:弱引用ThreadLocal不会轻易内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
实际上,我们的内存泄漏的根本原因是,不再被使用的Entry,没有从线程的ThreadLocalMap中删除。一般删除不再使用的Entry有这两种方式:
- 一种就是,使用完
ThreadLocal,手动调用remove(),把Entry从ThreadLocalMap中删除 - 另外一种方式就是:
ThreadLocalMap的自动清除机制去清除过期Entry.(ThreadLocalMap的get(),set()时都会触发对过期Entry的清除)
InheritableThreadLocal保证父子线程间的共享数据
我们知道ThreadLocal是线程隔离的,如果我们希望父子线程共享数据,如何做到呢?可以使用InheritableThreadLocal。先来看看demo:
public class InheritableThreadLocalTest {
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
threadLocal.set("关注公众号:捡田螺的小男孩");
inheritableThreadLocal.set("关注公众号:程序员田螺");
Thread thread = new Thread(()->{
System.out.println("ThreadLocal value " + threadLocal.get());
System.out.println("InheritableThreadLocal value " + inheritableThreadLocal.get());
});
thread.start();
}
}
//运行结果
ThreadLocal value null
InheritableThreadLocal value 关注公众号:程序员田螺
可以发现,在子线程中,是可以获取到父线程的 InheritableThreadLocal 类型变量的值,但是不能获取到 ThreadLocal 类型变量的值。
获取不到ThreadLocal 类型的值,很好理解,因为它是线程隔离的嘛。InheritableThreadLocal 是如何做到的呢?原理是什么呢?
在Thread类中,除了成员变量threadLocals之外,还有另一个成员变量:inheritableThreadLocals。它们两类型是一样的:
public class Thread implements Runnable { ThreadLocalMap threadLocals = null; ThreadLocalMap inheritableThreadLocals = null; }
当parent的inheritableThreadLocals不为null时,就会将parent的inheritableThreadLocals,赋值给前线程的inheritableThreadLocals。说白了,就是如果当前线程的inheritableThreadLocals不为null,就从父线程哪里拷贝过来一个过来,类似于另外一个ThreadLocal,数据从父线程那里来的。有兴趣的小伙伴们可以在去研究研究源码~
ThreadLocal的应用场景和使用注意点
ThreadLocal的很重要一个注意点,就是使用完,要手动调用remove()。
而ThreadLocal的应用场景主要有以下这几种:
- 使用日期工具类,当用到
SimpleDateFormat,使用ThreadLocal保证线性安全 - 全局存储用户信息(用户信息存入
ThreadLocal,那么当前线程在任何地方需要时,都可以使用) - 保证同一个线程,获取的数据库连接
Connection是同一个,使用ThreadLocal来解决线程安全的问题 - 使用
MDC保存日志信息。
ThreadLocal在项目中的应用实战如下:
定义上下文
public class ThreadContext<T> { private static final ThreadLocal<ThreadContext<?>> LOCAL = new ThreadLocal<>(); private ThreadContext(){} public static <T> ThreadContext<T> init(){ ThreadContext<T> context = new ThreadContext<>(); LOCAL.set(context); return context; } public static <T> ThreadContext<T> get(){ return (ThreadContext<T>) LOCAL.get(); } public static void fill(UserInfo userInfo){ ThreadContext<UserInfo> context = ThreadContext.get(); context.setAddress(userInfo.getAddress()); context.setUserId(userInfo.getUserId()); context.setUserName(context.getUserName()); } public static ThreadContext<?> set(ThreadContext<?> context){ ThreadContext<?> backup = get(); LOCAL.set(context); return backup; } public static void clear(){ LOCAL.remove(); } private String userName; private Long userId; private String address; public Long getUserId() { return userId; } public String getUserName() { return userName; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public void setUserId(Long userId) { this.userId = userId; } public void setUserName(String userName) { this.userName = userName; }
定义用户信息
public class UserInfo { private String userName; private Long userId; private String address; public Long getUserId() { return userId; } public String getUserName() { return userName; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public void setUserId(Long userId) { this.userId = userId; } public void setUserName(String userName) { this.userName = userName; } }
定义注解用于切面
@Inherited @Retention( RetentionPolicy.RUNTIME ) @Target({ElementType.METHOD}) public @interface ThreadScene { String userName(); String address(); long userId(); }
定义切面
@Aspect @Component public class TreadSceneAspect { @Around("@annotation(scene)") public Object around(ProceedingJoinPoint joinPoint,ThreadScene scene) throws Throwable{ ThreadContext<Object> originContext = ThreadContext.get(); ThreadContext<Object> context = originContext; if(context == null){ context = ThreadContext.init(); } String userName = context.getUserName(); Long userId = context.getUserId(); String address =context.getAddress(); try{ context.setUserName(scene.userName()); context.setUserId(scene.userId()); context.setAddress(scene.address()); return joinPoint.proceed(); }finally { if(originContext == null){ ThreadContext.clear(); }else { context.setAddress(address); context.setUserId(userId); context.setUserName(userName); } } } }

浙公网安备 33010602011771号