ThreadLocal 简单介绍

发布时间 2023-04-15 17:23:54作者: 吴川华仔博客

一、什么是ThreadLocal?

​ 从名字我们就可以看到 ThreadLocal 叫做本地线程变量,意思是说,ThreadLocal 中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal 为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。

二、ThreadLocal如何使用?

ThreadLocal的变量通常用private static修饰:

package com.linhuaming.test;

import java.util.HashMap;
import java.util.Map;

/**
 * 本地线程 测试
 */
public class ThreadLocalTest {

    private static ThreadLocal<Map<String,Object>> LOCAL = new ThreadLocal<>();

    //打印出本线程内的localVar值
    public static void printLoca(){
        Map<String, Object> map = LOCAL.get();
        System.out.println(map);

        //清除本地内存中的本地变量
        ThreadLocalTest.LOCAL.remove();
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            Map<String,Object> map = new HashMap<>();
            map.put("username","lhm");
            map.put("password","123456");
            map.put("age",28);
            ThreadLocalTest.LOCAL.set(map);
            printLoca();
        }, "t1");
        t1.start();

        Thread.sleep(2000);

        Thread t2 = new Thread(() -> {
            Map<String,Object> map = new HashMap<>();
            map.put("username","wja");
            map.put("password","123456");
            map.put("age",29);
            ThreadLocalTest.LOCAL.set(map);
            printLoca();
        }, "t2");
        t2.start();
    }

}

注意:

  • ThreadLocal的泛型是Object的,可以定义成map、set、list等等
  • 一个类中可以定义多个ThreadLocal属性

运行结果:

三、ThreadLocal的实现原理是什么?

1、set()方法

实现过程:

  • 首先获取当前线程对象,并获取当前线程的ThreadLocalMap。
  • 如果这个map为null,就调用createMap()方法初始化map,初始化需要传入泛型和要存储的值。
  • 如果map不为null,就调用set的重载方法,它会将当前线程对象作为键,存储值。

如下图所示:

2、ThreadLocalMap

createMap()方法

其实本地线程对象的threadLocals属性,就是用来存储ThreadLocalMap的。

ThreadLocalMap是ThreadLocal的静态内部类。它用Entry保存数据,而且继承了弱引用,

Entry内部使用ThreadLocal类型的变量作为键,保存传入的值。

3、get()方法

实现过程:

  • 获取当前线程对象。
  • 如果map不为null,就通过ThreadLocal对象,取出对应的Entry。
  • 如果entry不为空,就获取Entry中的Value,返回。
  • 如果前一步中map为空,就调用setInitialValue()方法。

如下图所示:

setInitialValue()方法

这个方法是给ThreadLocal设置初始值,如下图所示:

4、remove()方法

将ThreadLocal的值,从当前线程的ThreadLocalMap中删除,如下图所示:

5、总结

​ 如果读懂了上述 get() 和 set() 方法,会发现 Map 存储在线程之上,在 ThreadLocal 中通过Thread.currentThread()来得到此线程,然后通过这个线程来获取这个线程对应的 Map ,而Map上的数据存储方式采用的是 <key,value> ,其中 key 的值是 ThreadLocal 自己,而value的值是我们想要获取的值,如下图所示:

img

四、ThreadLocal 数据共享

既然 ThreadLocal 设计的初衷是解决线程间信息隔离的,那 ThreadLocal 能不能实现线程间信息共享呢?
答案是肯定的,只需要使用 ThreadLocal 的子类 InheritableThreadLocal 就可以轻松实现,来看具体实现代码:

public static void main(String[] args) {
    ThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
    inheritableThreadLocal.set("测试");
    new Thread(() -> System.out.println(inheritableThreadLocal.get())).start();
}

五、ThreadLocal在Java中的应用场景有哪些?

1、Spring实现事务隔离级别

private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
 
private static final ThreadLocal<Map<Object, Object>> resources =
    new NamedThreadLocal<>("Transactional resources");
 
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
    new NamedThreadLocal<>("Transaction synchronizations");
 
private static final ThreadLocal<String> currentTransactionName =
    new NamedThreadLocal<>("Current transaction name");

2、解决日期的线程安全

项目中有部分用户的时间出错,发现是多个线程共享一个SimpleDataFormat的问题。

使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add()。

如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。

但是每个线程内部都new一个SimpleDataFormat对象也不太好,所以使用ThreadLocal包装SimpleDataFormat,解决了线程安全的问题。

3、多个方法调用

一个线程经常需要横跨多个方法调用,那么它的参数就必须层层传递,给每个方法都加上相同的参数不太优雅。

而且,如果中间遇到第三方类库,参数就无法传递了。可以使用ThreadLocal,开始时把参数存进去,需要时直接get取出即可。

4、JDBC的数据库连接

从数据库连接池里获取的连接 Connection,在 JDBC 规范里并没有要求这个 Connection 必须是线程安全的。

数据库连接池通过线程封闭技术,保证一个 Connection 一旦被一个线程获取之后,在这个线程关闭 Connection 之前的这段时间里,不会再分配给其他线程,从而保证了 Connection 不会有并发问题。

六、常见问题

1、Entry的key为什么设计成弱引用?

前面说过,Entry的key,传入的是ThreadLocal对象,使用了WeakReference对象,即被设计成了弱引用。

那么,为什么要这样设计呢?

假如key对ThreadLocal对象的弱引用,改为强引用。

img

我们都知道ThreadLocal变量对ThreadLocal对象是有强引用存在的。

即使ThreadLocal变量生命周期完了,设置成null了,但由于key对ThreadLocal还是强引用。

此时,如果执行该代码的线程使用了线程池,一直长期存在,不会被销毁。

就会存在这样的强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。

那么,ThreadLocal对象和ThreadLocalMap都将不会被GC回收,于是产生了内存泄露问题。

为了解决这个问题,JDK的开发者们把Entry的key设计成了弱引用

弱引用的对象,在GC做垃圾清理的时候,就会被自动回收了。

如果key是弱引用,当ThreadLocal变量指向null之后,在GC做垃圾清理的时候,key会被自动回收,其值也被设置成null。

如下图所示:

img

接下来,最关键的地方来了。

由于当前的ThreadLocal变量已经被指向null了,但如果直接调用它的getsetremove方法,很显然会出现空指针异常。因为它的生命已经结束了,再调用它的方法也没啥意义。

此时,如果系统中还定义了另外一个ThreadLocal变量b,调用了它的getsetremove,三个方法中的任何一个方法,都会自动触发清理机制,将key为null的value值清空。

如果key和value都是null,那么Entry对象会被GC回收。如果所有的Entry对象都被回收了,ThreadLocalMap也会被回收了。

这样就能最大程度的解决内存泄露问题。

需要特别注意的地方是:

  1. key为null的条件是,ThreadLocal变量指向null,并且key是弱引用。如果ThreadLocal变量没有断开对ThreadLocal的强引用,即ThreadLocal变量没有指向null,GC就贸然的把弱引用的key回收了,不就会影响正常用户的使用?
  2. 如果当前ThreadLocal变量指向null了,并且key也为null了,但如果没有其他ThreadLocal变量触发getsetremove方法,也会造成内存泄露。

下面看看弱引用的例子:

public static void main(String[] args) {
    WeakReference<Object> weakReference0 = new WeakReference<>(new Object());
    System.out.println(weakReference0.get());
    System.gc();
    System.out.println(weakReference0.get());
}

打印结果:

java.lang.Object@1ef7fe8e
null

传入WeakReference构造方法的是直接new处理的对象,没有其他引用,在调用gc方法后,弱引用对象会被自动回收。

但如果出现下面这种情况:

public static void main(String[] args) {
    Object object = new Object();
    WeakReference<Object> weakReference1 = new WeakReference<>(object);
    System.out.println(weakReference1.get());
    System.gc();
    System.out.println(weakReference1.get());
}

执行结果:

java.lang.Object@1ef7fe8e
java.lang.Object@1ef7fe8e

先定义了一个强引用object对象,在WeakReference构造方法中将object对象的引用作为参数传入。这时,调用gc后,弱引用对象不会被自动回收。

我们的Entry对象中的key不就是第二种情况吗?在Entry构造方法中传入的是ThreadLocal对象的引用。

如果将object强引用设置为null:

public static void main(String[] args) {
    Object object = new Object();
    WeakReference<Object> weakReference1 = new WeakReference<>(object);
    System.out.println(weakReference1.get());
    System.gc();
    System.out.println(weakReference1.get());
    object=null;
    System.gc();
    System.out.println(weakReference1.get());
}

执行结果:

java.lang.Object@6f496d9f
java.lang.Object@6f496d9f
null

第二次gc之后,弱引用能够被正常回收。

由此可见,如果强引用和弱引用同时关联一个对象,那么这个对象是不会被GC回收。也就是说这种情况下Entry的key,一直都不会为null,除非强引用主动断开关联。

此外,你可能还会问这样一个问题:Entry的value为什么不设计成弱引用?

答:Entry的value假如只是被Entry引用,有可能没被业务系统中的其他地方引用。如果将value改成了弱引用,被GC贸然回收了(数据突然没了),可能会导致业务系统出现异常。

而相比之下,Entry的key,管理的地方就非常明确了。

这就是Entry的key被设计成弱引用,而value没被设计成弱引用的原因。

2、ThreadLocal真的会导致内存泄露?

通过上面的Entry对象中的key设置成弱引用,并且使用getsetremove方法清理key为null的value值,就能彻底解决内存泄露问题?答案是否定的。

如下图所示:

img

假如ThreadLocalMap中存在很多key为null的Entry,但后面的程序,一直都没有调用过有效的ThreadLocal的getsetremove方法。

那么,Entry的value值一直都没被清空。

所以会存在这样一条强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> value -> Object。

其结果就是:Entry和ThreadLocalMap将会长期存在下去,会导致内存泄露

3、如何解决内存泄露问题?

前面说过的ThreadLocal还是会导致内存泄露的问题,我们有没有解决办法呢?

答:有办法,调用ThreadLocal对象的remove方法。

不是在一开始就调用remove方法,而是在使用完ThreadLocal对象之后。列如:

先创建一个CurrentUser类,其中包含了ThreadLocal的逻辑。

public class CurrentUser {
    
    private static final ThreadLocal<UserInfo> THREA_LOCAL = new ThreadLocal();
    
    public static void set(UserInfo userInfo) {
        THREA_LOCAL.set(userInfo);
    }
    
    public static UserInfo get() {
       THREA_LOCAL.get();
    }

    public static void remove() {
       THREA_LOCAL.remove();
    }
}

然后在业务代码中调用相关方法:

public void doSamething(UserDto userDto) {
   UserInfo userInfo = convert(userDto);

   try{
     CurrentUser.set(userInfo);
     ...

     //业务代码
     UserInfo userInfo = CurrentUser.get();
     ...

   } finally {
      CurrentUser.remove();
   }
}

需要我们特别注意的地方是:一定要在finally代码块中,调用remove方法清理没用的数据。如果业务代码出现异常,也能及时清理没用的数据。

remove方法中会把Entry中的key和value都设置成null,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。

参考:

https://blog.csdn.net/m0_62609939/article/details/129493213
https://blog.csdn.net/weixin_45560850/article/details/124868500