天天看點

優雅的使用 ThreadLocal

前言

在我們日常 

Java Web

 開發中難免遇到需要把一個參數層層的傳遞到最内層,然後中間層根本不需要使用這個參數,或者是僅僅在特定的工具類中使用,這樣我們完全沒有必要在每一個方法裡面都傳遞這樣一個

通用

的參數。如果有一個辦法能夠在任何一個類裡面想用的時候直接拿來使用就太好了。

Java

Web

項目大部分都是基于

Tomcat

,每次通路都是一個新的線程,這樣讓我們聯想到了

ThreadLocal

,每一個線程都獨享一個

ThreadLocal

,在接收請求的時候

set

特定内容,在需要的時候

get

這個值。下面我們就進入主題。

ThreadLocal

維持線程封閉性的一種更規範的方法就是使用

ThreadLocal

,這個類能使線程中的某個值與儲存的值的對象關聯起來。

ThreadLocal

提供

get

set

等接口或方法,這些方法為每一個使用這個變量的線程都存有一份獨立的副本,是以

get

總是傳回由目前線程在調用

set

時設定的最新值。

ThreadLocal

有如下方法:

1public T get() { }
2public void set(T value) { }
3public void remove() { }
4protected T initialValue() { }
           

get()

方法是用來擷取

ThreadLocal

在目前線程中儲存的變量副本  

set()

用來設定目前線程中變量的副本  

remove()

用來移除目前線程中變量的副本   

initialValue()

是一個

protected

方法,一般是用來在使用時進行重寫的,如果在沒有set的時候就調用

get

,會調用

initialValue

方法初始化内容。

為了使用的更放心,我們簡單的看一下具體的實作:

set方法

1public 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方法

1void createMap(Thread t, T firstValue) {
2    t.threadLocals = new ThreadLocalMap(this, firstValue);
3}
           

set

的時候如果不存在

threadLocals

,直接建立對象。由上看出,放入

map

key

是目前的

ThreadLocal

value

是需要存放的内容,是以我們設定屬性的時候需要注意存放和擷取的是一個

ThreadLocal

get方法

1public 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)來下發下載下傳位址,當然接口也有同樣的其他邏輯,我們隻要在傳回資料的時候判斷好是什麼類型的用戶端就好了。如下:

場景一

請求

1GET api/users?device=android
           

傳回

1 {
2  user : {  
3  },
4  link : "https://play.google.com/store/apps/details?id=***"
5 }
           

場景二

1GET api/users?device=ios
           
1 {
2  user : { 
3  },
4  link : "https://itunes.apple.com/us/app/**"
5 }
           

實作

首先準備一個

BaseSigntureRequest

類用來存放公共參數

1public class BaseSignatureRequest {
 2    private String device;
 3
 4    public String getDevice() {
 5        return device;
 6    }
 7
 8    public void setDevice(String device) {
 9        this.device = device;
10    }
11}
           

然後準備一個

static

ThreadLocal

類用來存放

ThreadLocal

,以便存儲和擷取時候的

ThreadLocal

一緻。

1public class ThreadLocalCache {
2    public static ThreadLocal<BaseSignatureRequest> 
3     baseSignatureRequestThreadLocal = new ThreadLocal<>();
4}
           

然後編寫一個

Interceptor

,在請求的時候擷取

device

參數,存入目前線程的

ThreadLocal

中。這裡需要注意的是,重寫了

afterCompletion

方法,當請求結束的時候把

ThreadLocal

remove

,移除不必須要鍵值對。

1public 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
12    @Override
13    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
14                                Object handler, Exception ex) throws Exception {
15        ThreadLocalCache.baseSignatureRequestThreadLocal.remove();
16    }
17
18    @Override
19    public void postHandle(HttpServletRequest httpServletRequest,
20                           HttpServletResponse httpServletResponse, 
21                           Object o, ModelAndView modelAndView) throws Exception {
22
23    }
24}
           

當然需要在

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

裡面轉換實體的時候直接使用即可,這樣就大功告成了。

1public 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-07

本文作者:碼匠筆記

本文來自雲栖社群合作夥伴“IT先森養成記”,了解相關資訊可以關注“IT先森養成記”。

繼續閱讀