優雅的使用 ThreadLocal

  • 2019 年 10 月 6 日
  • 筆記

前言

在我們日常 Web 開發中難免遇到需要把一個參數層層的傳遞到最內層,然後中間層根本不需要使用這個參數,或者是僅僅在特定的工具類中使用,這樣我們完全沒有必要在每一個方法裡面都傳遞這樣一個 通用的參數。如果有一個辦法能夠在任何一個類裡面想用的時候直接拿來使用就太好了。JavaWeb項目大部分都是基於 Tomcat,每次訪問都是一個新的執行緒,這樣讓我們聯想到了 ThreadLocal,每一個執行緒都獨享一個 ThreadLocal,在接收請求的時候 set特定內容,在需要的時候 get這個值。下面我們就進入主題。

ThreadLocal

維持執行緒封閉性的一種更規範的方法就是使用 ThreadLocal,這個類能使執行緒中的某個值與保存的值的對象關聯起來。ThreadLocal提供 getset等介面或方法,這些方法為每一個使用這個變數的執行緒都存有一份獨立的副本,因此 get總是返回由當前執行緒在調用 set時設置的最新值。ThreadLocal有如下方法

public T get() { }  public void set(T value) { }  public void remove() { }  protected T initialValue() { }

get()方法是用來獲取 ThreadLocal在當前執行緒中保存的變數副本 set()用來設置當前執行緒中變數的副本 remove()用來移除當前執行緒中變數的副本 initialValue()是一個 protected方法,一般是用來在使用時進行重寫的,如果在沒有set的時候就調用 get,會調用 initialValue方法初始化內容。為了使用的更放心,我們簡單的看一下具體的實現:

set方法

public void set(T value) {          Thread t = Thread.currentThread();          ThreadLocalMap map = getMap(t);          if (map != null)              map.set(this, value);          else              createMap(t, value);      }

set方法會獲取當前的執行緒,通過當前執行緒獲取 ThreadLocalMap對象。然後把需要存儲的值放到這個 map裡面。如果沒有就調用 createMap創建對象。

getMap方法

ThreadLocalMap getMap(Thread t) {          return t.threadLocals;      }

getMap方法直接返回當前 ThreadthreadLocals變數,這樣說明了之所以說 ThreadLocal執行緒局部變數就是因為它只是通過 ThreadLocal變數存在了 Thread本身而已。

createMap方法

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

set的時候如果不存在 threadLocals,直接創建對象。由上看出,放入 mapkey是當前的 ThreadLocalvalue是需要存放的內容,所以我們設置屬性的時候需要注意存放和獲取的是一個 ThreadLocal

get方法

public T get() {          Thread t = Thread.currentThread();          ThreadLocalMap map = getMap(t);          if (map != null) {              ThreadLocalMap.Entry e = map.getEntry(this);              if (e != null)                  return (T)e.value;          }          return setInitialValue();      }

get方法就比較簡單,獲取當前執行緒,嘗試獲取當前執行緒裡面的 threadLocals,如果沒有獲取到就調用 setInitialValue方法, setInitialValue基本和 set是一樣的,就不累累述了。

場景

本文應用 ThreadLocal的場景:在調用API介面的時候傳遞了一些公共參數,這些公共參數攜帶了一些設備資訊,服務端介面根據不同的資訊組裝不同的格式數據返回給客戶端。假定伺服器端需要通過設備類型(device)來下發下載地址,當然介面也有同樣的其他邏輯,我們只要在返回數據的時候判斷好是什麼類型的客戶端就好了。如下:

場景一

請求

GET api/users?device=android

返回

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

場景二

請求

GET api/users?device=ios

返回

{          user : {          },          link : "https://itunes.apple.com/us/app/**"      }

實現

首先準備一個 BaseSigntureRequest類用來存放公共參數

public class BaseSignatureRequest {      private String device;        public String getDevice() {          return device;      }        public void setDevice(String device) {          this.device = device;      }  }

然後準備一個 staticThreadLocal類用來存放 ThreadLocal,以便存儲和獲取時候的 ThreadLocal一致。

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

然後編寫一個 Interceptor,在請求的時候獲取 device參數,存入當前執行緒的 ThreadLocal中。這裡需要注意的是,重寫了 afterCompletion方法,當請求結束的時候把 ThreadLocal remove,移除不必須要鍵值對。

public class ParameterInterceptor implements HandlerInterceptor {      @Override      public boolean preHandle(HttpServletRequest request, HttpServletResponse response,                               Object handler) throws Exception {          String device = request.getParameter("device");          BaseSignatureRequest baseSignatureRequest = new BaseSignatureRequest();          baseSignatureRequest.setDevice(device);          ThreadLocalCache.baseSignatureRequestThreadLocal.set(baseSignatureRequest);          return true;      }        @Override      public void afterCompletion(HttpServletRequest request, HttpServletResponse response,                                  Object handler, Exception ex) throws Exception {          ThreadLocalCache.baseSignatureRequestThreadLocal.remove();      }        @Override      public void postHandle(HttpServletRequest httpServletRequest,                             HttpServletResponse httpServletResponse,                             Object o, ModelAndView modelAndView) throws Exception {        }  }

當然需要在 spring裡面配置 interceptor

<mvc:interceptors>          <mvc:interceptor>              <mvc:mapping path="/api/**"/>              <bean class="life.majiang.ParameterInterceptor"></bean>          </mvc:interceptor>      </mvc:interceptors>

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

public class UserConverter {      public static ResultDO toDO(User user) {          ResultDO resultDO = new ResultDO();          resultDO.setUser(user);          BaseSignatureRequest baseSignatureRequest = ThreadLocalCache.baseSignatureRequestThreadLocal.get();          String device = baseSignatureRequest.getDevice();          if (StringUtils.equals(device, "ios")) {              resultDO.setLink("https://itunes.apple.com/us/app/**");          } else {              resultDO.setLink("https://play.google.com/store/apps/details?id=***");          }          return resultDO;      }

總結

這種機制很方便,因為他避免了在調用每一個方法時都要傳遞執行上下文資訊,合理的使用 ThreadLocal可以起到事倍功半的效果,但是需要避免濫用,例如將所有的全局變數作為 ThreadLocal對象, ThreadLocal類似全局變數,他能降低程式碼的可重用性,並在類之間引入隱含的耦合性,所以再使用前需要格外小心。