使用ThreadLocal差点让我怀疑自己见鬼了

开发 前端
使用ThreadLocal来存储数据库连接对象Connection,从而每次操作数据库表都是使用同一个对象保障了事务。

[[443312]]

前言

最近使用ThreadLocal出现了一个生产问题

一大清早就接到业务人员的电话,说系统登录进去后总是莫名其妙的报错,而且有点随机...昏沉的脑袋瞬间清醒了,我问具体是哪个模块报错,是不是操作了哪些特定的功能才报错,得到的回答是否定的,任何功能操作都随机报错??,也就是有时候报错,有时候不报错。

一时间有点懵逼了,脑海里不断回忆这段时间是不是上了什么新版本,不对啊,最近也没有什么大版本啊,都是一些小改,不可能会影响到所有业务模块啊。

赶忙起床去公司~

到公司后赶忙去机房,查看后台日志,发现报的是空指针异常,接着继续定位代码,发现是这段代码是从链路日志模块报出来的,仔细看了下代码,发现报错是从链路日志那块报出来的,这块代码看起来也没啥问题,而且这个模块都投产好几个月了,从来都没有发生过类似的报错,跟了下代码,是从ThreadLocal中取值,第一反应是链路日志又问题,先不管了,业务催的紧,先把应用重启了。

说来也奇怪,重启后应用竟然没有再出现报错了,真的绝了,这下我更加好奇了,在开发环境进行debug,那块代码逻辑的伪代码如下

  1. // 伪代码 
  2. 1、ThreadLocal的初始化 
  3.  
  4. 2、ThreadLocal threadlocal = new ThreadLocal(); 
  5.  
  6. 3、if(threadlocal.get() == null) threadlocal.set(XX) 
  7.  
  8. 4、....相关业务代码 
  9.  
  10. 5、threadlocal.get() 获取链路日志相关信息进行相关的处理 
  11.  
  12. 6、threadlocal.remove() 

咋一看,没啥问题,然而由于异常的信息导致第4步出现了异常,catch住了但是没有在finally里操作threadlocal.remove(),又因为第3步的判空对该线程无效了(这个线程已经被设置值了),从而该线程被污染了,

也就是每次用到这个被污染的线程就会报错,生产的随机报错就是这么来的,话不多说修bug。至此问题也解决了。

吸取教训:使用ThreadLocal时一定要记得考虑清楚场景,把各种情况都考虑全。

下面是对ThreadLocal的一些操作

没有进行remove操作

  1. static ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); 
  2.     // 没有进行remove操作的ThreadLocal的表现 
  3.     public static void main(String[] args) throws InterruptedException { 
  4.         // 创建一个线程池 
  5.         ExecutorService pool = Executors.newFixedThreadPool(2); 
  6.         for (int i = 0; i <= 5; i++) { 
  7.             final int count = i; 
  8.             pool.execute(()->{ 
  9.                 Integer integer = threadLocal.get(); 
  10.                 System.out.println("******************线程" + Thread.currentThread().getName().substring(7) + "设置threadlocal前的值是: " + integer); 
  11.      
  12.                 if (StringUtils.isEmpty(threadLocal.get())) { 
  13.                     threadLocal.set(count); 
  14.                 } 
  15.                 System.out.println("******************线程" + Thread.currentThread().getName().substring(7) + "里面的值是: " + threadLocal.get()); 
  16.             }); 
  17.             Thread.sleep(100); 
  18.         } 
  19.     } 

控制台打印效果如下,得到错误答案

进行了remove操作

  1. static ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); 
  2.     // 进行remove操作的ThreadLocal的表现 
  3.     public static void main(String[] args) throws InterruptedException { 
  4.         // 创建一个线程池 
  5.         ExecutorService pool = Executors.newFixedThreadPool(2); 
  6.         for (int i = 0; i <= 5; i++) { 
  7.             final int count = i; 
  8.             pool.execute(()->{ 
  9.                 Integer integer = threadLocal.get(); 
  10.                 System.out.println("******************线程" + Thread.currentThread().getName().substring(7) + "设置threadlocal前的值是: " + integer); 
  11.  
  12.                 if (StringUtils.isEmpty(threadLocal.get())) { 
  13.                     threadLocal.set(count); 
  14.                 } 
  15.                 System.out.println("******************线程" + Thread.currentThread().getName().substring(7) + "里面的值是: " + threadLocal.get()); 
  16.                 threadLocal.remove(); 
  17.             }); 
  18.             Thread.sleep(100); 
  19.         } 
  20.     } 

控制台打印效果如下,得到正确答案

remove操作报错了

  1. static ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); 
  2.     // 没有进行remove操作的ThreadLocal的表现 
  3.     public static void main(String[] args) throws InterruptedException { 
  4.         // 创建一个线程池 
  5.         ExecutorService pool = Executors.newFixedThreadPool(2); 
  6.         for (int i = 0; i <= 5; i++) { 
  7.  
  8.             final int count = i; 
  9.             pool.execute(()-> { 
  10.                 try { 
  11.                 Integer integer = threadLocal.get(); 
  12.                 System.out.println("******************线程" + Thread.currentThread().getName().substring(7) + "设置threadlocal前的值是: " + integer); 
  13.  
  14.                 if (StringUtils.isEmpty(threadLocal.get())) { 
  15.                     threadLocal.set(count); 
  16.                 } 
  17.                 if (Thread.currentThread().getName().contains("thread-1")) { 
  18.                     throw new RuntimeException(); 
  19.                 } 
  20.                 System.out.println("******************线程" + Thread.currentThread().getName().substring(7) + "里面的值是: " + threadLocal.get()); 
  21.                 threadLocal.remove(); 
  22.             } catch (Exception e) {} 
  23.             }); 
  24.             Thread.sleep(100); 
  25.         } 
  26.     } 

控制台打印效果如下,虽然进行了catch但是没有在finally里进行remove操作,得到错误答案

再修改得到最终代码

  1. static ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); 
  2.     // 没有进行remove操作的ThreadLocal的表现 
  3.     public static void main(String[] args) throws InterruptedException { 
  4.         // 创建一个线程池 
  5.         ExecutorService pool = Executors.newFixedThreadPool(2); 
  6.         for (int i = 0; i <= 5; i++) { 
  7.  
  8.             final int count = i; 
  9.             pool.execute(()-> { 
  10.                 try { 
  11.                 Integer integer = threadLocal.get(); 
  12.                 System.out.println("******************线程" + Thread.currentThread().getName().substring(7) + "设置threadlocal前的值是: " + integer); 
  13.  
  14.                 if (StringUtils.isEmpty(threadLocal.get())) { 
  15.                     threadLocal.set(count); 
  16.                 } 
  17.                 if (Thread.currentThread().getName().contains("thread-1")) { 
  18.                     throw new RuntimeException(); 
  19.                 } 
  20.                 System.out.println("******************线程" + Thread.currentThread().getName().substring(7) + "里面的值是: " + threadLocal.get()); 
  21.                  
  22.             } catch (Exception e) {.....}  
  23.              finally { 
  24.                threadLocal.remove();  
  25.              } 
  26.             }); 
  27.             Thread.sleep(100); 
  28.         } 
  29.     } 

ThreadLocal用于线程间的数据隔离,一说到线程间的数据隔离,我们还能想到synchronized或者其他的锁来实现线程间的安全问题。

ThreadLocal适合什么样的业务场景

1、使用threadlocal存储数据库连接,如果说一次线程请求,需要同时更新Goods表和Goods_Detail表,要是直接new出2个数据库连接,那么事务就没法进行保障了,数据库连接池

使用ThreadLocal来存储数据库连接对象Connection,从而每次操作数据库表都是使用同一个对象保障了事务。

2、解决SimpleDataFormat的线程安全问题

3、基于hreadlocal的数据源的动态切换

4、使用ThreadLocal来存储Cookie对象,在这次Http请求中,任何时候都可以通过简单的方式获取到Cookie。

当ThreadLocal被设置后绑定了当前线程,如果线程希望当前线程的子线程也能获取到该值,这就是InheritableThreadLocal的用武之地了

如何传递给子线程呢?InheritableThreadLocal的具体使用如下:

  1. // 创建InheritableThreadLocal 
  2.   static ThreadLocal<Integer> threadLocaltest = new InheritableThreadLocal<>(); 
  3.   public static void main(String[] args) { 
  4. // 主线程设置值 
  5.       threadLocaltest.set(100); 
  6.       new Thread(()-> { 
  7.     // 子线程获取值 
  8.           Integer num = threadLocaltest.get(); 
  9.  // 子线程获取到值并打印出来 
  10.           System.out.println(Thread.currentThread().getName() + "子类获取到的值" + num); // 输出:Thread-0子类获取到的值100 
  11.       }).start(); 
  12.   } 

 

责任编辑:武晓燕 来源: 程序员巴士
相关推荐

2019-06-21 15:23:08

Python面试题代码

2020-04-17 10:23:43

TDD测试驱动

2021-10-22 05:56:31

数据库锁表锁定机制

2012-07-25 09:56:52

编程程序员

2020-05-25 09:45:47

开发技能代码

2022-02-21 12:29:01

for循环前端

2020-08-04 08:44:08

HashCode

2023-11-02 08:27:29

2009-11-16 17:38:32

博科资讯ERP

2017-09-06 15:40:36

大数据动向

2023-05-14 22:25:33

内存CPU

2023-03-27 07:39:07

内存溢出优化

2019-07-09 05:29:56

木马病毒网络安全

2011-06-27 08:35:28

2020-05-29 08:14:49

代码Try-Catch程序员

2013-05-13 11:51:29

2018-06-07 09:32:07

2018-05-23 11:43:59

数据库

2020-08-13 07:56:48

JDK枚举类安全

2017-10-27 18:20:59

程序员
点赞
收藏

51CTO技术栈公众号