threadlocal使用_ThreadLocal使用不当竟然会引起服务宕机,看完这篇文章你就懂了...

本篇文章主要介绍了ThreadLocal基本使用、实现原理以及从原理层面分析使用上的注意事项(入坑和脱坑指南),让大家能够在使用ThreadLocal的时候减少犯错可能。

什么是ThreadLocal

ThreadLocal是一个用于为每个线程提供独立变量副本的工具类。注意ThreadLocal仅仅是个工具类,而真正存储独立变量副本是Thread类。ThreadLocal只是提供了存储独立变量副本的数据结构ThreadLocalMap和操作当前线程独立变量副本的方法(get/set/remove)。

ThreadLocal的基本使用

//模拟动态切换数据源public class DynamicDataSourceEntity {    public final static String DEFAULT_SOURCE = "DB_001";    private static final  ThreadLocal local = new ThreadLocal<>();    private DynamicDataSourceEntity(){}    public static String get(){         //获取线程独立变量副本        return local.get();    }    //DB_2019    public static void set(String source){        //设置线程独立变量副本        local.set(source);    }    public static void set(int year){ local.set("DB_"+year); }    public static void  restore(){ local.set(DEFAULT_SOURCE); }        public static void remove(){         //移除线程独立变量副本        local.remove();     }    public static void main(String[] args) {        for (int i=0; i<10; i++){            new Thread(()->{                LocalDate localDate = LocalDate.now();                DynamicDataSourceEntity.set(localDate.getYear());                System.out.println("select data");                DynamicDataSourceEntity.remove();            }).start();        }    }}

上述代码模拟了动态切换数据源的场景,ThreadLocal的使用方式还是很简单的,核心方法如下:

  • set:用于设置线程独立变量副本。没有set操作的ThreadLocal,容易引起脏数据。
  • get:用于获取线程独立变量副本。没有get操作的ThreadLocal对象没有意义。
  • remove:用于移除线程独立变量副本。没有remove操作,容易引起内存泄漏。

如果我们想为每个线程都先初始化一个默认值,可以用如下方式实现:

private static final  ThreadLocal local = new ThreadLocal(){    @Override    protected String initialValue() {        //设置默认的数据源        return DEFAULT_SOURCE;    }};

如上述代码所示,通过重写ThrealLocal的initialValue的方法来设置初始值,但是这个初始化过程并非在local对象加载静态变量的时候执行,而是在每个线程执行ThreadLocal.get()方法的时候执行。源代码如下:

public T get() {    /**    *每个线程都有自己的threadLocals变量用于存储独立变量副本(ThreadLocalMap类型),    * threadLocals是一个map集合,key即ThreadLocal,value即独立变量副本    */    Thread t = Thread.currentThread();    //通过当前线程获取当前线程的threadLocals变量    ThreadLocalMap map = getMap(t);    if (map != null) {        //通过ThreadLocal获取Entry,从而拿到独立变量副本即e.value并返回        ThreadLocalMap.Entry e = map.getEntry(this);        if (e != null) {            @SuppressWarnings("unchecked")            T result = (T)e.value;            return result;        }    }  //map == null 或 e==null时执行    return setInitialValue();}private T setInitialValue() {    //初始化默认值    T value = initialValue();    Thread t = Thread.currentThread();    ThreadLocalMap map = getMap(t);    if (map != null)        map.set(this, value);    else        createMap(t, value);    return value;}

每个线程都有自己的 ThreadLocalMap,如源码所示,map==null 或 e==null都会执行setInitialValue()。setInitialValue()中则执行了initialValue()方法并返回默认初始化的值,从而进行初始化操作。

深入剖析ThreadLocal

首先,我们通过ThreadLocal和Thread的类关系图来宏观了解下ThreadLocal和Thread。类关系图如下:

d779b2009cf2563f0c8085a3aa955029.png

ThreadLocal和Thread类关系图

如图所示,ThreadLocalMap是ThreadLocal的静态内部类,Entry是ThreadLocalMap的静态内部类。Thread中的threadLocals变量类型为ThreadLocalMap,是用于存储独立变量副本的。ThreadLocalMap和ThreadLocal有三组对应的方法:get()、set()和remove(),在ThreadLocal中对它们只做了校验和判断,最终的实现会落在ThreadLocalMap上。Entry继承了WeakReference(弱引用),内部只有一个value成员变量,key是ThreadLocal对象。

Thread、ThreadLocal以及ThreadLocalMap三者的堆栈角度分析图如下:

fceb14ae1c0e56ef4c80f2a32d11160f.png

Thread、ThreadLocal以及ThreadLocalMap三者的堆栈图

从堆栈分析图可以看出,Thread中只有一个类型为ThreadLocalMap的threadLocals变量,ThreadLocalMap是一个Map结构的数据类型,key存储的是ThreadLocal对象(弱引用),value存储的是本地线程独占对象,同一个ThreadLocal对象可以被多个线程共享。

前面我们提了两次弱引用,到底什么是弱引用呢?

Java有四种引用类型,引用强度从强到弱依次为:强引用、软引用、弱引用和虚引用。这里我们只简单介绍弱引用,其他引用类型自行了解。

弱引用即WeakReference,表示如果弱引用的指向的对象只存在弱引用这一条线路,则下次YGC时会被回收。

先来看下Entry的源码:

static class Entry extends WeakReference> {    Object value;    Entry(ThreadLocal> k, Object v) {        super(k);        value = v;    }}

Entry继承了WeakReference,key为弱引用,即只要ThreadLocal对象引用被设置为null,Entry的key就会自动在下一次YGC时被回收。同时ThreadLocal使用的set()和get()方法在调用的时候会把key==null的value置为null,使value能够被垃圾回收,避免内存泄漏。但是ThreadLocal是被多个线程共享的,通常作为私有静态变量使用,那么其生命周期不会随着线程结束而结束,所以这个弱引用的设计很鸡肋,没有起到应有的作用,反而增加了开发人员的理解难度。

InheritableThreadLocal详解

ThreadLocal用于同一个线程内,跨类、跨方法传递数据,但是很多情况下需要在线程中创建子线程,如果还是通过ThreadLocal传递数据子线程是获取不到的,这时候InheritableThreadLocal就派上用场了。InheritableThreadLocal继承自ThreadLocal,并且重写了getMap()、createMap()和childValue()方法。源码如下:

public class InheritableThreadLocal extends ThreadLocal {      protected T childValue(T parentValue) {        return parentValue;    }    //ThreadLocal获取的是Thread.threalLocals     //而InheritableThreadLocal获取的是Thread.inheritableThreadLocals    ThreadLocalMap getMap(Thread t) {       return t.inheritableThreadLocals;    }    //ThreadLocal创建的是Thread.threalLocals     //而InheritableThreadLocal创建的是Thread.inheritableThreadLocals    void createMap(Thread t, T firstValue) {        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);    }}

Thread中有两个ThreadLocalMap类型的变量,一个是threalLocals,一个是inheritableThreadLocals。

  • threalLocals:用于存储当前线程的独立变量。子类线程获取不到父类线程的数据。
  • inheritableThreadLocals:用来解决父子线程独立变量共享问题。

通过Thread构造方法来了解InheritableThreadLocal是如何实现父子线程数据共享的,源码如下:

//JDK1.8源码 Thread构造函数最后都是通过调用init(标记2)的 构造的public Thread() {    init(null, null, "Thread-" + nextThreadNum(), 0);}//1private void init(ThreadGroup g, Runnable target, String name,                      long stackSize) {    init(g, target, name, stackSize, null, true);}//2private void init(ThreadGroup g, Runnable target, String name,                      long stackSize, AccessControlContext acc,                      boolean inheritThreadLocals) {    //省去一些无关紧要的代码        setPriority(priority);    //inheritThreadLocals设置为true并且父类线程inheritableThreadLocals有共享数据则    //创建一个父类线程的inheritableThreadLocals副本,然后赋值给本线程的inheritableThreadLocals变量    //来实现父子线程数据共享    if (inheritThreadLocals && parent.inheritableThreadLocals != null)        this.inheritableThreadLocals =        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);    this.stackSize = stackSize;    tid = nextThreadID();}

ThreadLocal入坑与脱坑指南

ThreadLocal让线程可以安全地共享某个变量,但是很多开发者在使用ThreadLocal上存在一些问题,从而导致脏数据和内存泄漏的产生。下面看下ThrealLocal的入坑和脱坑指南:

入坑:

  • 脏数据问题: 线程复用导致产生脏数据。由于线程池会复用Thread对象,进而Thread对象中的threalLocals也会被复用,导致Thread对象在执行其他任务时通过get()方法获取到之前任务设置的数据,从而产生脏数据。
  • 内存泄漏问题: ThreadLocal通常是使用static关键字修饰的。如果开发人员单纯寄希望于ThreadLocal对象失去引用后,触发弱引用机制来回收Entry的Value,那么就会导致内存泄漏,Entry的Value无法被回收。

脱坑:

  • 解决脏数据:线程执行前重新调用set()设置值。线程复用导致产生脏数据,如果复用线程在执行下个任务之前调用set()重新设置值,那么脏数据问题就不会出现了。
  • 解决内存泄漏:线程执行完后调用remove()完成收尾工作。无法依托弱引用机制来回收Entry的Value,那就显式调用ThreadLocal的remove方法显式清除。

最后,Entry的弱引用机制不是导致ThreadLocal内存泄漏的原因,它的存在只是增加了开发人员的理解难度,就算没有弱引用机制,线程执行完不调用remove()清除也会存在内存泄漏问题。

END

笔者是一位热爱互联网、热爱互联网技术、热于分享的年轻人,如果您跟我一样,我愿意成为您的朋友,分享每一个有价值的知识给您。喜欢作者的同学,点赞+转发+关注哦!

点赞+转发+关注,私信作者“读书笔记”即可获得BAT大厂面试资料、高级架构师VIP视频课程等高质量技术资料。

d25d84d812cb1cb62bef9951a277c67f.png

BAT等一线互联网面试资料和VIP高级架构师视频

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在拦截器中使用 `ThreadLocal` 存储数据时,需要注意以下几点: 1. `ThreadLocal` 的作用域是线程级别的,即同一个线程中的所有代码都可以访问该 `ThreadLocal` 对象存储的数据,但不同线程之间是互不干扰的。 2. 在拦截器中,如果使用 `ThreadLocal` 存储数据,需要在请求处理结束后将其清空,否则该线程在下一次请求时可能访问到上一次请求中存储的数据。 3. 在控制器中访问 `ThreadLocal` 存储的数据时,需要确保在同一个线程中,否则出现获取到 `null` 的情况。 以下是一个示例代码: ```java public class MyInterceptor implements HandlerInterceptor { private static final ThreadLocal<String> threadLocal = new ThreadLocal<>(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String value = // 获取需要存储的数据 threadLocal.set(value); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { threadLocal.remove(); // 清空 ThreadLocal 中的数据 } } @RestController public class MyController { @GetMapping("/test") public String test() { String value = MyInterceptor.threadLocal.get(); return "value: " + value; } } ``` 在上面的代码中,`MyInterceptor` 中使用 `ThreadLocal` 存储数据,`MyController` 中通过访问 `MyInterceptor.threadLocal` 获取存储的数据。请注意,`MyInterceptor.threadLocal` 是一个静态变量,因此在不同的线程中访问到的是不同的 `ThreadLocal` 对象。如果在 `MyController` 中访问时获取到的是 `null`,那么很可能是因为在不同的线程中访问了 `ThreadLocal` 对象。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值