原创

Spring Boot中ThreadLocal踩坑

温馨提示:
本文最后更新于 2024年02月07日,已超过 19 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

ThreadLocal作用

ThreadLocal的作用:用来存当前线程的局部变量,不同线程间互不干扰。拿完数据记得需要移除数据,不然JVM不会将ThreadLocal回收(可能还会被引用),多了就会出现内存泄漏的情况。

解析ThreadLocal类

ThreadLocal包含几个方法:

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }

T get()

get()方法是用来获取ThreadLocal在当前线程中保存的变量副本

set(T value)

set()用来设置当前线程中变量的副本

void remove()

remove()用来移除当前线程中变量的副本

T initialValue()

initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法。

ThreadLocal变量污染

ThreadLocal会在每个线程存储一个副本,但是如果我们使用的是比如Tomcat,Tomcat自身会维护一个线程池,线程结束后,并不会马上销毁,而是会重新进入线程池,下次有请求时,有可能会复用当前线程,如果我们每次使用ThreadLocal之前,没有进行Set(T value),那么就有可能导致不同线程之间变量污染,比如下面的代码

@RestController
@RequestMapping(value = "book")
@Slf4j
public class BookController {

    private static final ThreadLocal<String> THREAD_LOCAL_TEST = new ThreadLocal<>();

    @Resource
    private IBookService bookService;

    /**
     * 构造函数
     */
    public BookController() {
        log.info("构造函数,此时bookService :" + bookService);
    }

    @PostConstruct
    public void init() {
        log.info("PostConstruct,此时bookService :" + bookService);
    }

    @PreDestroy
    public void destroy() {
        log.info("PreDestroy");
    }

    @GetMapping(value = "set")
    public void set() {
        if (StringUtils.isEmpty(THREAD_LOCAL_TEST.get())) {
            THREAD_LOCAL_TEST.set(UUID.randomUUID().toString());
        }
    }

    @GetMapping(value = "search")
    public List<Book> search() {
        log.error(THREAD_LOCAL_TEST.get());
        return bookService.search();
    }

}

使用jmeter进行请求,可以看到同一线程,输出的内容永远不会发生改变。

可以在内次使用之前进行set(T value),但是set(T value)可能会导致内存无法释放。

ThreadLocal可能导致的内存泄露

ThreadLocal为了避免内存泄露,不仅使用了弱引用维护key,还会在每个操作上检查key是否被回收,进而再回收value
但是从中也可以看到,ThreadLocal并不能100%保证不发生内存泄漏。比如,很不幸的,你的get()方法总是访问固定几个一直存在的ThreadLocal,那么清理动作就不会执行,如果你没有机会调用set()remove(),那么这个内存泄漏依然会发生。
一个良好的习惯依然是:当你不需要这个ThreadLocal变量时,主动调用remove(),这样对整个系统是有好处的。

@RestController
@RequestMapping(value = "book")
@Slf4j
public class BookController {

    private static final ThreadLocal<String> THREAD_LOCAL_TEST = new ThreadLocal<>();

    @Resource
    private IBookService bookService;

    /**
     * 构造函数
     */
    public BookController() {
        log.info("构造函数,此时bookService :" + bookService);
    }

    @PostConstruct
    public void init() {
        log.info("PostConstruct,此时bookService :" + bookService);
    }

    @PreDestroy
    public void destroy() {
        log.info("PreDestroy");
    }

    @GetMapping(value = "set")
    public void set() {
        THREAD_LOCAL_TEST.set(UUID.randomUUID().toString());
    }

    @GetMapping(value = "search")
    public List<Book> search() {
        log.error(THREAD_LOCAL_TEST.get());
        THREAD_LOCAL_TEST.remove();
        return bookService.search();
    }

}

ThreadLocal与局部变量

局部变量和ThreadLocal起到的作用是一样的,保证了并发环境下数据的安全性。那就是说,完全可以用局部变量来代替ThreadLocal咯?

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

翻译过来

ThreadLocal提供的是一种线程局部变量。这些变量不同于其它变量的点在于每个线程在获取变量的时候,都拥有它自己相对独立的变量初始化拷贝。ThreadLocal的实例一般是私有静态的,可以做到与一个线程绑定某一种状态。

所以就这段话而言,我们知道ThreadLocal不是为了满足多线程安全而开发出来的,因为局部变量已经足够安全。ThreadLocal是为了方便线程处理自己的某种状态。

可以看到ThreadLocal实例化所处的位置,是一个线程共有区域。好比一个银行和个人,我们可以把钱存在银行,也可以把钱存在家。
存在家里的钱是局部变量,仅供个人使用;存在银行里的钱也不是说可以让别人随便使用,只有我们以个人身份去获取才能得到。
所以说ThreadLocal封装的变量我们是在外面某个区域保存了处于我们个人的一个状态,只允许我们自己去访问和修改的状态。

正文到此结束
本文目录