ThreadLocal实战之踩坑笔记

电视资讯 浏览(623)

  简要聊聊ThreadLocal

  ThreadLocal提供线程内部的局部变量,我们可以将项目中的一些变量直接存放在当前线程中,在本线程内随时随地可取,隔离其他线程,获取保存的值时非常方便。

  案例集锦

  案例一:

  public class RequestContextHolderEx {

  ? ? private static ThreadLocal REQUEST_THREAD_LOCAL=new ThreadLocal<>();

  ? ? public static void clear() {

  ? ? ? ? REQUEST_THREAD_LOCAL.remove();

  ? ? }

  ? ? public static HttpServletRequest getRequest() {

  ? ? ? ? HttpServletRequest request=REQUEST_THREAD_LOCAL.get();

  ? ? ? ? if (request !=null)

  ? ? ? ? ? ? return request;

  ? ? ? ? ServletRequestAttributes requestAttributes=getRequestAttributes();

  ? ? ? ? if (requestAttributes !=null)

  ? ? ? ? ? ? request=requestAttributes.getRequest();

  ? ? ? ? REQUEST_THREAD_LOCAL.set(request);

  ? ? ? ? return request;

  ? ? }

  }

  首先这是我们封装的一个处理Request相关的支持类,这个类我们定义了一个REQUEST_THREAD_LOCAL的ThreadLocal对象,专门用来存储HttpServletRequest对象

  我们看看下面的这个getRequest()方法,方法里边,第一行就是直接调用ThreadLocal的get()方法,request对象不为空的话直接返回,为空的话通过getRequestAttributes().getRequest()重新查一遍request,查出来后再调用set()方法把request对象设置进去,等到同一线程下次再调用这个getRequest()方法时,就能够直接get()出request对象返回。

  上面这段代码其实很简单,get()本地线程变量中存的值,存在的话直接返回,不存在的话,重新查一遍,把对象重新set()到本地线程变量中再返回。

  emm…多看了几遍,好像有什么不对劲的地方,我们为啥要用本地线程呢,在这里我不用是不是也可以?答案是:可以。

  确实,这个方法你直接这么写:

  ? ? public static HttpServletRequest getRequest() {

  ? ? ? ? ServletRequestAttributes requestAttributes=getRequestAttributes();

  ? ? ? ? if (requestAttributes !=null)

  ? ? ? ? ? ? request=requestAttributes.getRequest();

  ? ? ? ? return request;

  ? ? }

  说实话,没毛病,还少几行代码,还不会踩我案例二要说到的坑,但是我们引入ThreadLocal是不是真的多此一举了呢?

  其实不然,你仔细想想,假如说整个请求多次调用这个方法,不用ThreadLocal的话,是不是每次都需要先getRequestAttributes(),再requestAttributes.getRequest(),没发现这样很多此一举吗,还很花费时间,如果你感觉这里耗时不多的话,那想过没有假如我们要查的值涉及很多逻辑,甚至是要查数据库,这个开销就大了啊;使用ThreadLocal的话,同一个请求查询多次这个方法,线程只会第一次调用该方法时跑一遍获取逻辑,再把值存储到本地线程中,其他时候直接从本地线程中获取就行了,方便多了,耗时少了,不会再重复的跑相同逻辑的事了,所以引入本地线程是很有必要的事啊。

  那ThreadLocal这么好用,是不是可以到处都用呢?其实也不是啦,很简单嘛,假如你整个请求就调用一次这个方法,那使用ThreadLocal的意义在哪呢,这才是多此一举,所以只有那些一次请求有可能使用到多次的变量才存储到ThreadLocal中,像Request、SessionInfo信息等那些一次请求可能多次访问的数据都可以存储到ThreadLocal。

  会了ThreadLocal使用姿势和使用场景,是不是就可以开始上手了呢,别急,咱们先踩个坑~

  案例二:

  public class SessionContext {

  private static final ThreadLocal SESSION_INFO_THREAD_LOCAL=new ThreadLocal<>();

  ? ? public void clear() {

  ? ? ? ? SESSION_INFO_THREAD_LOCAL.remove();

  ? ? }

  public SessionInfo getSessionInfo() {

  ? ? ? ? SessionInfo si=SESSION_INFO_THREAD_LOCAL.get();

  ? ? ? ? if (null !=si) {

  ? ? ? ? ? ? return si;

  ? ? ? ? }

  ? ? ? ? //balabala...,省略一堆获取SessionInfo的逻辑

  ? ? ? ? SESSION_INFO_THREAD_LOCAL.set(si);

  ? ? ? ? return si;

  ? ? }

  }

  这个代码和案例一的代码基本一样,只不过案例二存储的是用户的登录信息SessionInfo,先说说现象:

  其实就是一个用户getSessionInfo()获取到了其他人的用户登录信息,导致出现了一些偶发的神奇问题,这个问题出现的时候真是头疼,当时自己也不是很懂ThreadLocal,只是定位到应该是SessionInfo获取的有问题。

  先别急着往下看,思考一下,是什么原因会导致获取到了别人的SessionInfo。

  这篇balabala了一堆,其实上面的问题就是和ThreadLocal有关,你们发现没,我两个案例都写了一个一个我没有提到的方法,是的,clear()方法,案例二出现的原因就是因为请求结束没有remove()掉保存在本地线程中的信息。

  我们来看看到底是不是因为没有remove掉原信息

  理论推断:我们知道一个线程使用完之后并不会销毁,而是会回到线程池进行复用,也就是说,如果你不调用remove()的话,保存在当前线程中的变量实例还是绑定在线程上的,当下一个用户使用了其他用户使用过的线程处理请求,直接get()的话,就会把原来在该线程中保存的信息给获取出来,这就直接导致获取到了别人的用户信息,这是非常危险的。

  验证:先来一段代码

  public class ThreadLocalTest {

  ? ?

  ? ? private static ExecutorService executor=Executors.newFixedThreadPool(1);

  ? ?

  ? ? private static ThreadLocal TEST_THREAD_LOCAL=new ThreadLocal<>();

  ? ?