优雅的使用 ThreadLocal

来源:码匠笔记

前言

在我们日常 Web 开发中难免遇到需要把一个参数层层的传递到最内层,然后中间层根本不需要使用这个参数,或者是仅仅在特定的工具类中使用,这样我们完全没有必要在每一个方法里面都传递这样一个 通用的参数。如果有一个办法能够在任何一个类里面想用的时候直接拿来使用就太好了。JavaWeb项目大部分都是基于 Tomcat,每次访问都是一个新的线程,这样让我们联想到了 ThreadLocal,每一个线程都独享一个 ThreadLocal,在接收请求的时候 set特定内容,在需要的时候 get这个值。下面我们就进入主题。

ThreadLocal

维持线程封闭性的一种更规范的方法就是使用 ThreadLocal,这个类能使线程中的某个值与保存的值的对象关联起来。ThreadLocal提供 getset等接口或方法,这些方法为每一个使用这个变量的线程都存有一份独立的副本,因此 get总是返回由当前线程在调用 set时设置的最新值。ThreadLocal有如下方法

 
 
  1. public T get() { }

  2. public void set(T value) { }

  3. public void remove() { }

  4. protected T initialValue() { }

get()方法是用来获取 ThreadLocal在当前线程中保存的变量副本
set()用来设置当前线程中变量的副本
remove()用来移除当前线程中变量的副本
initialValue()是一个 protected方法,一般是用来在使用时进行重写的,如果在没有set的时候就调用 get,会调用 initialValue方法初始化内容。为了使用的更放心,我们简单的看一下具体的实现:

set方法

 
 
  1. public void set(T value) {

  2.        Thread t = Thread.currentThread();

  3.        ThreadLocalMap map = getMap(t);

  4.        if (map != null)

  5.            map.set(this, value);

  6.        else

  7.            createMap(t, value);

  8.    }

set方法会获取当前的线程,通过当前线程获取 ThreadLocalMap对象。然后把需要存储的值放到这个 map里面。如果没有就调用 createMap创建对象。

getMap方法

 
 
  1. ThreadLocalMap getMap(Thread t) {

  2.        return t.threadLocals;

  3.    }

getMap方法直接返回当前 ThreadthreadLocals变量,这样说明了之所以说 ThreadLocal线程局部变量就是因为它只是通过 ThreadLocal变量存在了 Thread本身而已。

createMap方法

 
 
  1. void createMap(Thread t, T firstValue) {

  2.        t.threadLocals = new ThreadLocalMap(this, firstValue);

  3.    }

set的时候如果不存在 threadLocals,直接创建对象。由上看出,放入 mapkey是当前的 ThreadLocalvalue是需要存放的内容,所以我们设置属性的时候需要注意存放和获取的是一个 ThreadLocal

get方法

 
 
  1. public T get() {

  2.        Thread t = Thread.currentThread();

  3.        ThreadLocalMap map = getMap(t);

  4.        if (map != null) {

  5.            ThreadLocalMap.Entry e = map.getEntry(this);

  6.            if (e != null)

  7.                return (T)e.value;

  8.        }

  9.        return setInitialValue();

  10.    }

get方法就比较简单,获取当前线程,尝试获取当前线程里面的 threadLocals,如果没有获取到就调用 setInitialValue方法, setInitialValue基本和 set是一样的,就不累累述了。

场景

本文应用 ThreadLocal的场景:在调用API接口的时候传递了一些公共参数,这些公共参数携带了一些设备信息,服务端接口根据不同的信息组装不同的格式数据返回给客户端。假定服务器端需要通过设备类型(device)来下发下载地址,当然接口也有同样的其他逻辑,我们只要在返回数据的时候判断好是什么类型的客户端就好了。如下:

场景一

请求

 
 
  1. GET api/users?device=android

返回

 
 
  1.    {

  2.        user : {        

  3.        },

  4.        link : "https://play.google.com/store/apps/details?id=***"

  5.    }

场景二

请求

 
 
  1. GET api/users?device=ios

返回

 
 
  1.    {

  2.        user : {    

  3.        },

  4.        link : "https://itunes.apple.com/us/app/**"

  5.    }

实现

首先准备一个 BaseSigntureRequest类用来存放公共参数

 
 
  1. public class BaseSignatureRequest {

  2.    private String device;

  3.    public String getDevice() {

  4.        return device;

  5.    }

  6.    public void setDevice(String device) {

  7.        this.device = device;

  8.    }

  9. }

然后准备一个 staticThreadLocal类用来存放 ThreadLocal,以便存储和获取时候的 ThreadLocal一致。

 
 
  1. public class ThreadLocalCache {

  2.    public static ThreadLocal<BaseSignatureRequest>

  3.        baseSignatureRequestThreadLocal = new ThreadLocal<>();

  4. }

然后编写一个 Interceptor,在请求的时候获取 device参数,存入当前线程的 ThreadLocal中。这里需要注意的是,重写了 afterCompletion方法,当请求结束的时候把 ThreadLocal remove,移除不必须要键值对。

 
 
  1. public class ParameterInterceptor implements HandlerInterceptor {

  2.    @Override

  3.    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,

  4.                             Object handler) throws Exception {

  5.        String device = request.getParameter("device");

  6.        BaseSignatureRequest baseSignatureRequest = new BaseSignatureRequest();

  7.        baseSignatureRequest.setDevice(device);

  8.        ThreadLocalCache.baseSignatureRequestThreadLocal.set(baseSignatureRequest);

  9.        return true;

  10.    }

  11.    @Override

  12.    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,

  13.                                Object handler, Exception ex) throws Exception {

  14.        ThreadLocalCache.baseSignatureRequestThreadLocal.remove();

  15.    }

  16.    @Override

  17.    public void postHandle(HttpServletRequest httpServletRequest,

  18.                           HttpServletResponse httpServletResponse,

  19.                           Object o, ModelAndView modelAndView) throws Exception {

  20.    }

  21. }

当然需要在 spring里面配置 interceptor

 
 
  1.    <mvc:interceptors>

  2.        <mvc:interceptor>

  3.            <mvc:mapping path="/api/**"/>

  4.            <bean class="life.majiang.ParameterInterceptor"></bean>

  5.        </mvc:interceptor>

  6.    </mvc:interceptors>

最后在 Converter里面转换实体的时候直接使用即可,这样就大功告成了。

 
 
  1. public class UserConverter {

  2.    public static ResultDO toDO(User user) {

  3.        ResultDO resultDO = new ResultDO();

  4.        resultDO.setUser(user);

  5.        BaseSignatureRequest baseSignatureRequest = ThreadLocalCache.baseSignatureRequestThreadLocal.get();

  6.        String device = baseSignatureRequest.getDevice();

  7.        if (StringUtils.equals(device, "ios")) {

  8.            resultDO.setLink("https://itunes.apple.com/us/app/**");

  9.        } else {

  10.            resultDO.setLink("https://play.google.com/store/apps/details?id=***");

  11.        }

  12.        return resultDO;

  13.    }

总结

这种机制很方便,因为他避免了在调用每一个方法时都要传递执行上下文信息,合理的使用 ThreadLocal可以起到事倍功半的效果,但是需要避免滥用,例如将所有的全局变量作为 ThreadLocal对象, ThreadLocal类似全局变量,他能降低代码的可重用性,并在类之间引入隐含的耦合性,所以再使用前需要格外小心。

▼往期精彩回顾▼

微服务为什么一定要用docker

一个程序媛的奇葩经历和吐槽

如何彻底理解volatile关键字?

为什么阿里巴巴禁止在 foreach 循环里进行元素的 remove/add 操作

SpringBoot 定时任务踩坑记录

使用docker部署spring cloud项目详细步骤

几道和「堆栈、队列」有关的面试算法题

在Spring Boot中格式化JSON日期

使用windows版Docker并在IntelliJ IDEA使用Docker运行Spring Cloud项目

Springboot项目的接口防刷

实体与模型之间的映射,就用Mapstruct

Java高级开发必会的50个性能优化的细节(珍藏版)

记下来,spring 装配bean的三种方式!

厉害!这届码农追星玩出了新花样

Java生成二维码

与 30 家公司过招,得到了这章面试心法

一道让你拍案叫绝的算法题

了解一下Spring中用了哪些设计模式?这样回答面试官才稳

dubbo 面试18问

拜托!面试请不要再问我Spring Cloud底层原理

稳了!Java并发编程71道面试题及答案

【附答案】Java面试2019常考题目汇总(一)

这10道springboot常见面试题你需要了解下

JVM面试题
巧用这19条MySQL优化,效率至少提高3倍

640?wx_fmt=jpeg

长按关注,更多精彩

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
优雅使用ThreadLocal的方法有以下几点: 1. 创建ThreadLocal对象:在需要使用ThreadLocal的地方,我们首先需要创建一个ThreadLocal对象,用来存储线程局部变量的值。 2. 存储线程局部变量:通过调用ThreadLocal对象的set方法,将需要存储的值与当前线程关联起来。这样每个线程都会有自己独立的副本。 3. 获取线程局部变量:通过调用ThreadLocal对象的get方法,可以获取当前线程所关联的线程局部变量的值。每个线程获取到的值都是独立的。 4. 移除线程局部变量:在使用完线程局部变量后,为了释放资源和防止内存泄漏,需要及时调用ThreadLocal对象的remove方法来移除当前线程所关联的线程局部变量。 5. 注意存储和获取的一致性:为了确保存储和获取的是同一个ThreadLocal对象,需要在存储和获取过程中使用同一个ThreadLocal对象。 通过这样的方式,可以实现线程间的隔离,保证变量在不同线程中的独立性,从而避免了线程安全问题。ThreadLocal为每个线程提供了一个独立的变量副本,使得多个线程可以并发地访问同一个变量,而不会相互影响。这在一些需要在多个线程中共享数据又需要保证线程安全的场景下非常有用。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [优雅使用 ThreadLocal](https://blog.csdn.net/b644ROfP20z37485O35M/article/details/89392048)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值