PageHelper中遇到的ThreadLocal小坑

Posted by zhangtao on Thursday, June 27, 2019

最近写代码刚好碰到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);
        	 ...
    }

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