前言
最近使用ThreadLocal出现了一个生产问题
一大清早就接到业务人员的电话,说系统登录进去后总是莫名其妙的报错,而且有点随机...昏沉的脑袋瞬间清醒了,我问具体是哪个模块报错,是不是操作了哪些特定的功能才报错,得到的回答是否定的,任何功能操作都随机报错??,也就是有时候报错,有时候不报错。
一时间有点懵逼了,脑海里不断回忆这段时间是不是上了什么新版本,不对啊,最近也没有什么大版本啊,都是一些小改,不可能会影响到所有业务模块啊。
赶忙起床去公司~
到公司后赶忙去机房,查看后台日志,发现报的是空指针异常,接着继续定位代码,发现是这段代码是从链路日志模块报出来的,仔细看了下代码,发现报错是从链路日志那块报出来的,这块代码看起来也没啥问题,而且这个模块都投产好几个月了,从来都没有发生过类似的报错,跟了下代码,是从ThreadLocal中取值,第一反应是链路日志又问题,先不管了,业务催的紧,先把应用重启了。
说来也奇怪,重启后应用竟然没有再出现报错了,真的绝了,这下我更加好奇了,在开发环境进行debug,那块代码逻辑的伪代码如下
- // 伪代码
- 1、ThreadLocal的初始化
- 2、ThreadLocal threadlocal = new ThreadLocal();
- 3、if(threadlocal.get() == null) threadlocal.set(XX)
- 4、....相关业务代码
- 5、threadlocal.get() 获取链路日志相关信息进行相关的处理
- 6、threadlocal.remove()
咋一看,没啥问题,然而由于异常的信息导致第4步出现了异常,catch住了但是没有在finally里操作threadlocal.remove(),又因为第3步的判空对该线程无效了(这个线程已经被设置值了),从而该线程被污染了,
也就是每次用到这个被污染的线程就会报错,生产的随机报错就是这么来的,话不多说修bug。至此问题也解决了。
吸取教训:使用ThreadLocal时一定要记得考虑清楚场景,把各种情况都考虑全。
下面是对ThreadLocal的一些操作
没有进行remove操作
- static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
- // 没有进行remove操作的ThreadLocal的表现
- public static void main(String[] args) throws InterruptedException {
- // 创建一个线程池
- ExecutorService pool = Executors.newFixedThreadPool(2);
- for (int i = 0; i <= 5; i++) {
- final int count = i;
- pool.execute(()->{
- Integer integer = threadLocal.get();
- System.out.println("******************线程" + Thread.currentThread().getName().substring(7) + "设置threadlocal前的值是: " + integer);
- if (StringUtils.isEmpty(threadLocal.get())) {
- threadLocal.set(count);
- }
- System.out.println("******************线程" + Thread.currentThread().getName().substring(7) + "里面的值是: " + threadLocal.get());
- });
- Thread.sleep(100);
- }
- }
控制台打印效果如下,得到错误答案
进行了remove操作
- static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
- // 进行remove操作的ThreadLocal的表现
- public static void main(String[] args) throws InterruptedException {
- // 创建一个线程池
- ExecutorService pool = Executors.newFixedThreadPool(2);
- for (int i = 0; i <= 5; i++) {
- final int count = i;
- pool.execute(()->{
- Integer integer = threadLocal.get();
- System.out.println("******************线程" + Thread.currentThread().getName().substring(7) + "设置threadlocal前的值是: " + integer);
- if (StringUtils.isEmpty(threadLocal.get())) {
- threadLocal.set(count);
- }
- System.out.println("******************线程" + Thread.currentThread().getName().substring(7) + "里面的值是: " + threadLocal.get());
- threadLocal.remove();
- });
- Thread.sleep(100);
- }
- }
控制台打印效果如下,得到正确答案
remove操作报错了
- static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
- // 没有进行remove操作的ThreadLocal的表现
- public static void main(String[] args) throws InterruptedException {
- // 创建一个线程池
- ExecutorService pool = Executors.newFixedThreadPool(2);
- for (int i = 0; i <= 5; i++) {
- final int count = i;
- pool.execute(()-> {
- try {
- Integer integer = threadLocal.get();
- System.out.println("******************线程" + Thread.currentThread().getName().substring(7) + "设置threadlocal前的值是: " + integer);
- if (StringUtils.isEmpty(threadLocal.get())) {
- threadLocal.set(count);
- }
- if (Thread.currentThread().getName().contains("thread-1")) {
- throw new RuntimeException();
- }
- System.out.println("******************线程" + Thread.currentThread().getName().substring(7) + "里面的值是: " + threadLocal.get());
- threadLocal.remove();
- } catch (Exception e) {}
- });
- Thread.sleep(100);
- }
- }
控制台打印效果如下,虽然进行了catch但是没有在finally里进行remove操作,得到错误答案
再修改得到最终代码
- static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
- // 没有进行remove操作的ThreadLocal的表现
- public static void main(String[] args) throws InterruptedException {
- // 创建一个线程池
- ExecutorService pool = Executors.newFixedThreadPool(2);
- for (int i = 0; i <= 5; i++) {
- final int count = i;
- pool.execute(()-> {
- try {
- Integer integer = threadLocal.get();
- System.out.println("******************线程" + Thread.currentThread().getName().substring(7) + "设置threadlocal前的值是: " + integer);
- if (StringUtils.isEmpty(threadLocal.get())) {
- threadLocal.set(count);
- }
- if (Thread.currentThread().getName().contains("thread-1")) {
- throw new RuntimeException();
- }
- System.out.println("******************线程" + Thread.currentThread().getName().substring(7) + "里面的值是: " + threadLocal.get());
- } catch (Exception e) {.....}
- finally {
- threadLocal.remove();
- }
- });
- Thread.sleep(100);
- }
- }
ThreadLocal用于线程间的数据隔离,一说到线程间的数据隔离,我们还能想到synchronized或者其他的锁来实现线程间的安全问题。
ThreadLocal适合什么样的业务场景
1、使用threadlocal存储数据库连接,如果说一次线程请求,需要同时更新Goods表和Goods_Detail表,要是直接new出2个数据库连接,那么事务就没法进行保障了,数据库连接池
使用ThreadLocal来存储数据库连接对象Connection,从而每次操作数据库表都是使用同一个对象保障了事务。
2、解决SimpleDataFormat的线程安全问题
3、基于hreadlocal的数据源的动态切换
4、使用ThreadLocal来存储Cookie对象,在这次Http请求中,任何时候都可以通过简单的方式获取到Cookie。
当ThreadLocal被设置后绑定了当前线程,如果线程希望当前线程的子线程也能获取到该值,这就是InheritableThreadLocal的用武之地了
如何传递给子线程呢?InheritableThreadLocal的具体使用如下:
- // 创建InheritableThreadLocal
- static ThreadLocal<Integer> threadLocaltest = new InheritableThreadLocal<>();
- public static void main(String[] args) {
- // 主线程设置值
- threadLocaltest.set(100);
- new Thread(()-> {
- // 子线程获取值
- Integer num = threadLocaltest.get();
- // 子线程获取到值并打印出来
- System.out.println(Thread.currentThread().getName() + "子类获取到的值" + num); // 输出:Thread-0子类获取到的值100
- }).start();
- }