关于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代表一个完整的对象,keyThreadLocal本身,valueThreadLocal的泛型值。
  • 并发多线程场景下,每个线程Thread,在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而可以实现了线程隔离

了解完这几个核心方法后,有些小伙伴可能会有疑惑,ThreadLocalMap为什么要用ThreadLocal作为key呢?直接用线程Id不一样嘛?

因为一个线程下有多个ThreadLocal的变量时,使用线程ID区分不出来了。

ThreadLocal为什么会导致内存泄露?

先来看看TreadLocal的引用示意图:

 

 

 ThreadLocalMap使用ThreadLocal弱引用作为key,当ThreadLocal变量被手动设置为null,即一个ThreadLocal没有外部强引用来引用它,当系统GC时,ThreadLocal一定会被回收。这样的话,ThreadLocalMap中就会出现keynullEntry,就没有办法访问这些keynullEntryvalue,如果当前线程再迟迟不结束的话(比如线程池的核心线程),这些keynullEntryvalue就会一直存在一条强引用链:Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object 永远无法回收,造成内存泄漏。

实际上,ThreadLocalMap的设计中已经考虑到这种情况。所以也加上了一些防护措施:即在ThreadLocalget,set,remove方法,都会清除线程ThreadLocalMap里所有keynullvalue

 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的时候会被清除。

因此可以发现,使用弱引用作为EntryKey,可以多一层保障:弱引用ThreadLocal不会轻易内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

实际上,我们的内存泄漏的根本原因是,不再被使用的Entry,没有从线程的ThreadLocalMap中删除。一般删除不再使用的Entry有这两种方式:

  • 一种就是,使用完ThreadLocal,手动调用remove(),把Entry从ThreadLocalMap中删除
  • 另外一种方式就是:ThreadLocalMap的自动清除机制去清除过期Entry.(ThreadLocalMapget(),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时,就会将parentinheritableThreadLocals,赋值给前线程的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);
            }
        }
    }
}

 

posted @ 2022-08-14 10:42  扁豆一号  阅读(1396)  评论(0编辑  收藏  举报