天天看点

优雅的使用 ThreadLocal

我们知道

Java

Web

项目大部分都是基于

Tomcat

,每次访问都是一个新的线程,看到这里让我们联想到了

ThreadLocal

,每一个线程都独享一个

ThreadLocal

,在接收请求的时候

set

特定内容,在需要的时候

get

这个值。下面我们就进入主题。

ThreadLocal

维持线程封闭性的一种更规范的方法就是使用

ThreadLocal

,这个类能使线程中的某个值与保存的值的对象关联起来。

ThreadLocal

提供

get

set

等接口或方法,这些方法为每一个使用这个变量的线程都存有一份独立的副本,因此

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

方法直接返回当前

Thread

threadLocals

变量,这样说明了之所以说

ThreadLocal

线程局部变量

就是因为它只是通过

ThreadLocal

变量

存在了

Thread

本身而已。

createMap

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

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

  3. }

set

的时候如果不存在

threadLocals

,直接创建对象。由上看出,放入

map

key

是当前的

ThreadLocal

value

是需要存放的内容,所以我们设置属性的时候需要注意存放和获取的是一个

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. }

然后准备一个

static

ThreadLocal

类用来存放

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

类似全局变量,他能降低代码的可重用性,并在类之间引入隐含的耦合性,所以再使用前需要格外小心。

原文发布时间为:2018-09-15

本文作者:麻酱

本文来自云栖社区合作伙伴“

Web项目聚集地

”,了解相关信息可以关注“

”。

继续阅读