PageHelper中遇到的ThreadLocal小坑

本文探讨了ThreadLocal在特定情况下出现线程不安全的问题,包括共享外部对象和线程池复用时的数据残留,以及在mybatis-plus使用中因未正确清理分页配置而导致的意外分页行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近写代码刚好碰到ThreadLocal的小坑,顺便学习了一波ThreadLocal,拿出来分享一下

ThreadLocal什么时候会出现线程不安全的情况呢?

我总结了两种情况

1.记录在 ThreadLocal 中的是一个线程共享的外部对象

https://www.cnblogs.com/qilong853/p/5982878.html

这边文章讲的很好,我就不复制黏贴了,ThreadLocal中保存的是Object对象的一个引用,这样的话,当有其他线程对这个引用指向的对象做修改时,当前线程Thread对象中保存的值也会发生变化

	public class ThreadLocalTest implements Runnable {
	    private Integer count = 0;
		// 一个普通的对象
	    private NumberClass numberClass = new NumberClass();
	    private ThreadLocal<NumberClass> threadLocal = new ThreadLocal();
	    @Override
	    public void run() {
	        numberClass.setNum(++count);
	        threadLocal.set(numberClass);
	        try {
	            Thread.sleep(1000);
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        }
	        System.out.println("[Thread-" + Thread.currentThread().getId() + "]"+threadLocal.get().getNum());
	
	    }
	}

	 public static void main(String[] args){
	        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
	       ThreadLocalTest thread = new ThreadLocalTest();
	        for (int i =0;i<5;i++ ){
	            fixedThreadPool.execute(thread);
	        }

	//                [Thread-12]5
	//                [Thread-15]5
	//                [Thread-14]5
	//                [Thread-11]5
	//                [Thread-13]5

	// 执行完5个线程中的threadLocal中的值相同
	
	    }

2.引入线程池,线程复用

如果在一个线程被使用完准备回放到线程池中之前,我们没有对记录在数据库中的数据执行清理,那么这部分数据就会被下一个复用该线程的业务看到

	public class ThreadLocalTest2 implements Runnable {
	//    private Integer count = 0;
	    private ThreadLocal<List> threadLocal = new ThreadLocal();
	    @Override
	    public void run() {
	//        Integer count = 0;
	        List<Long> list = threadLocal.get();
	        if (list == null){
	            list = new ArrayList<>();
	            System.out.println("[Thread-" + Thread.currentThread().getId() + "]  init threadLocal");
	        }
	        list.add((long) (1+Math.random()*(10)));
	        threadLocal.set(list);
	        List list2 = threadLocal.get();
	        System.out.println("[Thread-" + Thread.currentThread().getId() + "]"+list);
	
	    }
	}

	  public static void main(String[] args){
	        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
	        ThreadLocalTest2 thread = new ThreadLocalTest2();
	        for (int i =0;i<5;i++ ){
	            fixedThreadPool.execute(thread);
	        }
	
	// 执行结果
	//                [Thread-11]  init threadLocal
	//                [Thread-12]  init threadLocal
	//                [Thread-11][9]
	//                [Thread-12][5]
	//                [Thread-11][9, 1]
	//                [Thread-12][5, 9]
	//                [Thread-11][9, 1, 4]
	
	    }

3.自己踩到的坑

首先碰到的坑是这样的,首先这个selectList是mybatisPlus的方法,执行到这个方法的时无缘无故调用了分页的相关sql,而这个方法里并没有进行分页(pageHelper)的配置。当时我们就想到是线程串了,当前线程使用了其他线程的分页配置。但是我们通过观察PageHelper的源码发现,

    protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

配置是放在ThreadLocal里面的,而且分页查询执行完毕,会调用销毁ThreadLocal的方法,不会出现第二种问题,

    public static void clearLocalPage() {
            LOCAL_PAGE.remove();
    }

并且存的Page对象是new的变量,也不会出现第一种问题。
正常情况不会出现线程不安全的情况。

	public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
	      Page<E> page = new Page<E>(pageNum, pageSize, count);
        	 ...
    }

坑在这里
图
写代码的时候一开始设置了pageHelper,后面进行了if语句,没有执行下面的sql,导致page信息没有被销毁,当这个线程被复用的时候,就会以为要分页了

在使用 `ThreadLocal` 的过程中,开发者可能会遇到多个常见问题和陷阱,尤其是在多线程环境下,这些问题可能导致内存泄漏、数据污染或性能下降。 ### 1. **内存泄漏问题** 由于 `ThreadLocal` 对象作为键存储在每个线程的 `ThreadLocalMap` 中,并且这些键是弱引用(WeakReference),当外部不再持有对 `ThreadLocal` 实例的强引用时,它会被垃圾回收器回收。然而,如果线程本身仍然存活,那么对应的 `ThreadLocalMap` 中的值可能不会被及时清理,从而导致内存泄漏 [^3]。这种情况在使用线程池时尤为明显,因为线程的生命周期较长,而 `ThreadLocal` 可能已经被回收,但其值仍驻留在线程中 [^2]。 为了避免此类问题,建议在使用完 `ThreadLocal` 后显式调用 `remove()` 方法来清除当前线程中的值: ```java threadLocal.remove(); ``` ### 2. **线程池与 ThreadLocal 的结合问题** 在线程池环境中使用 `ThreadLocal` 需要特别小心,因为线程池中的线程是复用的。如果某个线程执行完任务后未清理 `ThreadLocal` 值,则下一个任务可能会访问到前一个任务遗留的数据,造成数据污染 [^4]。为避免这种风险,可以在任务执行完毕后手动调用 `remove()` 或确保 `ThreadLocal` 在每次任务开始时都被正确初始化 [^2]。 ### 3. **初始值设置不当** 如果没有正确设置 `ThreadLocal` 的初始值,可能导致获取到 `null` 值,进而引发空指针异常。可以通过重写 `initialValue()` 方法或者使用 `set()` 提供默认值来规避此问题: ```java private static final ThreadLocal<String> threadLocal = new ThreadLocal<>() { @Override protected String initialValue() { return "default"; } }; ``` ### 4. **并发修改问题** 虽然 `ThreadLocal` 保证了线程间的数据隔离性,但如果多个线程共享同一个对象并通过 `ThreadLocal` 存储该对象的引用,则需要额外同步机制来保护这个对象的状态,以防止并发修改带来的不一致问题。 ### 5. **过度使用导致资源浪费** 不必要的频繁创建 `ThreadLocal` 实例不仅增加了内存开销,还可能影响程序性能。因此,应合理规划 `ThreadLocal` 的使用范围,尽量复用已有的 `ThreadLocal` 实例。 ### 6. **调试困难** 由于 `ThreadLocal` 的特性使得每个线程都有自己的独立副本,这给调试带来了挑战,特别是在分布式系统或多线程复杂交互场景下,追踪特定线程的状态变得更为复杂。 综上所述,在设计和实现过程中,应当充分考虑到以上提到的各种潜在问题,并采取相应的预防措施,比如适时清理不再使用的 `ThreadLocal` 数据、谨慎处理线程池环境下的 `ThreadLocal` 使用方式等,以此确保应用程序能够稳定可靠地运行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值