前言
多線程的線程安全問題是微妙而且出乎意料的,因為在沒有進行適當同步的情況下多線程中各個操作的順序是不可預期的,多線程訪問同一個共享變量特別容易出現並發問題,特別是多個線程需要對一個共享變量進行寫入時候,為了保證線程安全,
一般需要使用者在訪問共享變量的時候進行適當的同步,如下圖所示:
可以看到同步的措施一般是加鎖,這就需要使用者對鎖也要有一定了解,這顯然加重了使用者的負擔。那麼有沒有一種方式當創建一個變量的時候,每個線程對其進行訪問的時候訪問的是自己線程的變量呢?其實ThreaLocal就可以做這個事情,注意一下,ThreadLocal的出現並不是為了解決上面的問題而出現的。
ThreadLocal是在JDK包裡面提供的,它提供了線程本地變量,也就是如果你創建了一個ThreadLocal變量,那麼訪問這個變量的每個線程都會有這個變量的本地拷貝,多個線程操作這個變量的時候,實際是操作自己本地內存裡面的變量,從而避免了線程安全問題,創建一個ThreadLocal變量後,
每個線程會拷貝一個變量到自己的本地內存,如下圖:
好了,現在我們思考一個問題:ThreadLocal的實現原理,ThreadLocal作為變量的線程隔離方式,其內部又是如何實現的呢?
首先我們要看ThreadLocal的類圖結構,如下圖所示:
如
上類圖可見,Thread類中有一個threadLocals和inheritableThreadLocals 都是ThreadLocalMap類型的變量,而ThreadLocalMap是一個定制化的Hashmap,默認每個線程中這兩個變量都為null,只有當線程第一次調用了ThreadLocal的set或者get方法的時候才會創建。
其實每個線程的本地變量不是存到ThreadLocal實例裡面的,而是存放到調用線程的threadLocals變量裡面。也就是說ThreadLocal類型的本地變量是存放到具體線程內存空間的。
ThreadLocal其實就是一個外殼,它通過set方法把value值放入調用線程threadLocals裡面存放起來,當調用線程調用它的get方法的時候再從當前線程的threadLocals變量裡面拿出來使用。如果調用線程如果一直不終止的話,那麼這個本地變量會一直存放到調用線程的threadLocals變量裡面,
因此,當不需要使用本地變量時候可以通過調用ThreadLocal變量的remove方法,從當前線程的threadLocals變量裡面刪除該本地變量。可能還有人會問threadLocals為什麼設計為Map結構呢?很明顯是因為每個線程裡面可以關聯多個ThreadLocal變量。
接下來我們可以進入到ThreadLocal中的源碼如看看,如下代碼所示:
主要看set,get,remove這三個方法的實現邏輯,如下:
先看set(T var1)方法
public void set(T var1) { //(1)獲取當前線程Thread var2 = Thread.currentThread(); //(2) 當前線程作為key,去查找對應的線程變量,找到則設置ThreadLocal.ThreadLocalMap var3 = this.getMap(var2); if(var3 != null) { var3.set(this, var1); } else { //(3) 第一次調用則創建當前線程對應的Hashmap this.createMap(var2, var1); } }如上代碼(1)首先獲取調用線程,然後使用當前線程作為參數調用了getMap(var2) 方法,getMap(Thread var2) 代碼如下:
ThreadLocal.ThreadLocalMap getMap(Thread var1) { return var1.threadLocals; }可知getMap(var2) 所作的就是獲取線程自己的變量threadLocals,threadlocal變量是綁定到了線程的成員變量裡面。
如果getMap(var2) 返回不為空,則把value 值設置進入到threadLocals,也就是把當前變量值放入了當前線程的內存變量threadLocals,threadLocals 是個HashMap 結構,其中key 就是當前ThreadLocal 的實例對象引用,value 是通過set 方法傳遞的值。
如果getMap(var2) 返回空那說明是第一次調用set 方法,則創建當前線程的threadLocals 變量,下面看createMap(var2, var1) 裡面做了啥呢?
void createMap(Thread var1, T var2) { var1.threadLocals = new ThreadLocal.ThreadLocalMap(this, var2); }可以看到的就是創建當前線程的threadLocals變量。
接下來我們再看get()方法,代碼如下:
public T get() { //(4)獲取當前線程Thread var1 = Thread.currentThread(); //(5)獲取當前線程的threadLocals變量ThreadLocal.ThreadLocalMap var2 = this.getMap(var1); //(6)如果threadLocals不為null,則返回對應本地變量值if(var2 != null) { ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this); if(var3 != null) { Object var4 = var3.value; return var4; } } //(7)threadLocals為空則初始化當前線程的threadLocals成員變量。 return this.setInitialValue(); }代碼(4)首先獲取當前線程實例,如果當前線程的threadLocals變量不為null則直接返回當前線程的本地變量。否則執行代碼(7)進行初始化,setInitialValue()的代碼如下:
private T setInitialValue() { //(8)初始化為null Object var1 = this.initialValue(); Thread var2 = Thread.currentThread(); ThreadLocal.ThreadLocalMap var3 = this.getMap(var2); //(9)如果當前線程變量的threadLocals變量不為空if(var3 != null) { var3.set(this, var1); //(10)如果當前線程的threadLocals變量為空} else { this.createMap(var2, var1); } return var1; }如上代碼如果當前線程的threadLocals 變量不為空,則設置當前線程的本地變量值為null,否者調用createMap 創建當前線程的createMap 變量。
接著我們在看看void remove()方法,代碼如下:
public void remove() { ThreadLocal.ThreadLocalMap var1 = this.getMap(Thread.currentThread()); if(var1 != null) { var1.remove(this); } }如上代碼,如果當前線程的threadLocals 變量不為空,則刪除當前線程中指定ThreadLocal 實例的本地變量。
接下來我們看看具體演示demo,代碼如下:
/** * Created by cong on 2018/6/3. */public class ThreadLocalTest { //(1)打印函數static void print(String str) { //1.1 打印當前線程本地內存中localVariable變量的值System.out.println(str + ":" + localVariable.get()); //1.2 清除當前線程本地內存中localVariable變量//localVariable.remove(); } //(2) 創建ThreadLocal變量static ThreadLocal<String> localVariable = new ThreadLocal<>(); public static void main(String[] args) { //(3) 創建線程one Thread threadOne = new Thread(new Runnable() { public void run() { //3.1 設置線程one中本地變量localVariable的值localVariable.set("線程1的本地變量"); //3.2 調用打印函數print("線程1----->"); //3.3打印本地變量值System.out.println("移除線程1本地變量後的結果" + ":" + localVariable.get()); } }); //(4) 創建線程two Thread threadTwo = new Thread(new Runnable() { public void run() { //4.1 設置線程one中本地變量localVariable的值localVariable.set("線程2的本地變量"); //4.2 調用打印函數print("線程2----->"); //4.3打印本地變量值System.out.println("移除線程2本地變量後的結果" + ":" + localVariable.get()); } }); //(5)啟動線程threadOne.start(); threadTwo.start(); }}代碼(2)創建了一個ThreadLocal 變量;
代碼(3)、(4)分別創建了線程1和2;
代碼(5)啟動了兩個線程;
線程1 中代碼3.1 通過set 方法設置了localVariable 的值,這個設置的其實是線程1 本地內存中的一個拷貝,這個拷貝線程2 是訪問不了的。然後代碼3.2 調用了print 函數,代碼1.1 通過get 函數獲取了當前線程(線程1)本地內存中localVariable 的值;
線程2 執行類似線程1。
運行結果如下:
這裡要注意一下ThreadLocal的內存洩漏問題
每個線程內部都有一個名字為threadLocals 的成員變量,該變量類型為HashMap,其中key 為我們定義的ThreadLocal 變量的this 引用,value 則為我們set 時候的值,每個線程的本地變量是存到線程自己的內存變量threadLocals 裡面的,如果當前線程一直不消失那麼這些本地變量會一直存到,
所以可能會造成內存洩露,所以使用完畢後要記得調用ThreadLocal 的remove 方法刪除對應線程的threadLocals 中的本地變量。
解開代碼1.2的註釋後,再次運行,運行結果如下:
我們有沒有想過這樣的一個問題:子線程中是否獲取到父線程中設置的ThreadLocal 變量的值呢?
這裡可以告訴大家,在子線程中是獲取不到父線程中設置的ThreadLocal 變量的值的。那麼有辦法讓子線程訪問到父線程中的值嗎?為了解決該問題InheritableThreadLocal 應運而生,InheritableThreadLocal 繼承自ThreadLocal,提供了一個特性,就是子線程可以訪問到父線程中設置的本地變量。
首先我們先進入InheritableThreadLocal這個類的源碼去看,如下:
public class InheritableThreadLocal<T> extends ThreadLocal<T> { public InheritableThreadLocal() { } //(1) protected T childValue(T var1) { return var1; } //(2) ThreadLocalMap getMap(Thread var1) { return var1.inheritableThreadLocals; } //(3) void createMap(Thread var1, T var2) { var1.inheritableThreadLocals = new ThreadLocalMap(this, var2); }}可以看到InheritableThreadlocal繼承ThreadLocal,並重寫了三個方法,在上面的代碼已經標出了。代碼(3)可知InheritableThreadLocal重寫createMap方法,那麼可以知道現在當第一次調用set方法時候創建的是當前線程的inhertableThreadLocals變量的實例,而不再是threadLocals。
代碼(2)可以知道當調用get方法獲取當前線程的內部map變量時候,獲取的是inheritableThreadLocals,而不再是threadLocals。
關鍵地方來了,重寫的代碼(1)是何時被執行的,以及如何實現子線程可以訪問父線程本地變量的。這個要從Thread創建的代碼看起,Thread的默認構造函數以及Thread.java類的構造函數如下:
/** * Created by cong on 2018/6/3. */ public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc) { //... //(4)獲取當前線程Thread parent = currentThread(); //... //(5)如果父線程的inheritableThreadLocals變量不為null if (parent.inheritableThreadLocals != null) //(6)設置子線程中的inheritableThreadLocals變量this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); this.stackSize = stackSize; tid = nextThreadID(); }創建線程時候在構造函數里面會調用init方法,前面講到了inheritableThreadLocal類get,set方法操作的是變量inheritableThreadLocals,所以這裡inheritableThreadLocal變量就不為null,所以會執行代碼(6),下面看createInheritedMap方法源碼,如下:
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); }可以看到createInheritedMap內部使用父線程的inheritableThreadLocals變量作為構造函數創建了一個新的ThreadLocalMap變量,然後賦值給了子線程的inheritableThreadLocals變量,那麼接著進入到ThreadLocalMap的構造函數里面做了什麼,源碼如下:
private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { //(7)調用重寫的方法Object value = key.childValue(e.value);//返回e.value Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } }如上代碼所做的事情就是把父線程的inhertableThreadLocals成員變量的值複製到新的ThreadLocalMap對象,其中代碼(7)InheritableThreadLocal類重寫的代碼(1)也映入眼簾了。
總的來說:InheritableThreadLocal類通過重寫代碼(2)和(3)讓本地變量保存到了具體線程的inheritableThreadLocals變量裡面,線程通過InheritableThreadLocal類實例的set 或者get方法設置變量時候就會創建當前線程的inheritableThreadLocals變量。當父線程創建子線程時候,
構造函數里面就會把父線程中inheritableThreadLocals變量裡面的本地變量拷貝一份複製到子線程的inheritableThreadLocals變量裡面。
好了原理了解到位了,接下來進行一個例子來驗證上面所了解的東西,如下:
package com.hjc;/** * Created by cong on 2018/6/3. */public class InheritableThreadLocalTest { //(1) 創建線程變量public static ThreadLocal<String> threadLocal = new ThreadLocal<String>(); public static void main(String[] args) { //(2) 設置線程變量threadLocal.set("hello Java"); //(3) 啟動子線程Thread thread = new Thread(new Runnable() { public void run() { //(4)子線程輸出線程變量的值System.out.println("子線程:" + threadLocal.get()); } }); thread.start(); //(5)主線程輸出線程變量值System.out.println("父線程:" + threadLocal.get()); }}運行結果如下:
也就是說同一個ThreadLocal 變量在父線程中設置值後,在子線程中是獲取不到的。根據上節的介紹,這個應該是正常現象,因為子線程調用get 方法時候當前線程為子線程,而調用set 方法設置線程變量是main 線程,兩者是不同的線程,自然子線程訪問時候返回null。
那麼有辦法讓子線程訪問到父線程中的值嗎?答案是有,就用我們上面原理分析的InheritableThreadLocal。
將上面例子的代碼(1)修改為:
//(1) 創建線程變量public static ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>();
運行結果如下:
可知現在可以從子線程中正常的獲取到線程變量值了。那麼什麼情況下需要子線程可以獲取到父線程的threadlocal 變量呢?
情況還是蠻多的,比如存放用戶登錄信息的threadlocal 變量,很有可能子線程中也需要使用用戶登錄信息,再比如一些中間件需要用統一的追踪ID 把整個調用鏈路記錄下來的情景。
Spring Request Scope 作用域Bean 中ThreadLocal 的使用
我們知道Spring 中在XML 裡面配置Bean 的時候可以指定scope 屬性來配置該Bean 的作用域為singleton、prototype、request、session 等,其中作用域為request 的實現原理就是使用ThreadLocal 實現的。如果你想讓你Spring 容器裡的某個Bean 擁有Web 的某種作用域,
則除了需要Bean 級上配置相應的scope 屬性,還必須在web.xml 裡面配置如下:
<listener> <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class></listener>
這裡主要看RequestContextListener的兩個方法:
public void requestInitialized(ServletRequestEvent requestEvent)
和
public void requestDestroyed(ServletRequestEvent requestEvent)
當一個web請求過來時候會執行requestInitialized方法:
public void requestInitialized(ServletRequestEvent requestEvent) { .......省略HttpServletRequest request = (HttpServletRequest) requestEvent.getServletRequest(); ServletRequestAttributes attributes = new ServletRequestAttributes(request); request.setAttribute(REQUEST_ATTRIBUTES_ATTRIBUTE, attributes); LocaleContextHolder.setLocale(request.getLocale()); //設置屬性到threadlocal變量RequestContextHolder.setRequestAttributes(attributes); } public static void setRequestAttributes(RequestAttributes attributes) { setRequestAttributes(attributes, false); } public static void setRequestAttributes(RequestAttributes attributes, boolean inheritable) { if (attributes == null) { resetRequestAttributes(); } else { //默認inheritable=false if (inheritable) { inheritableRequestAttributesHolder.set(attributes); requestAttributesHolder.remove(); } else { requestAttributesHolder.set(attributes); inheritableRequestAttributesHolder.remove(); } } }可以看到上面源碼,由於默認inheritable 為FALSE,我們的屬性值都放到了requestAttributesHoder裡面,而它的定義是:
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =new NamedThreadLocal<RequestAttributes>("Request attributes"); private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =new NamedInheritableThreadLocal<RequestAttributes>("Request context");其中NamedThreadLocal<T> extends ThreadLocal<T>,所以不具有繼承性。
其中NamedThreadLocal<T> extends ThreadLocal<T>,所以不具有繼承性。
NameInheritableThreadLocal<T> extends InheritableThreadLocal<T>,所以具有繼承性,所以默認放入到RequestContextHolder裡面的屬性值在子線程中獲取不到。
當請求結束時候調用requestDestroyed方法,源碼如下:
public void requestDestroyed(ServletRequestEvent requestEvent) { ServletRequestAttributes attributes = (ServletRequestAttributes) requestEvent.getServletRequest().getAttribute(REQUEST_ATTRIBUTES_ATTRIBUTE); ServletRequestAttributes threadAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (threadAttributes != null) { // 我們很有可能在最初的請求線程中if (attributes == null) { attributes = threadAttributes; } //請求結束則清除當前線程的線程變量。 LocaleContextHolder.resetLocaleContext(); RequestContextHolder.resetRequestAttributes(); } if (attributes != null) { attributes.requestCompleted(); } }接下來從時序圖看一下Web請求調用邏輯如何:
也就是說每次發起一個Web請求在Tomcat中context(具體應用)處理前,host匹配後會設置下RequestContextHolder屬性,讓requestAttributesHolder不為空,在請求結束時會清除。
因此,默認情況下放入RequestContextHolder裡面的屬性子線程訪問不到,Spring 的request作用域的bean是使用threadlocal實現的。
接下來進行一個例子模擬請求,代碼如下:
web.xml配置如下:
因為是request 作用域,所以必須是Web 項目,並且需要配置RequestContextListener 到web.xml。
<listener> <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class></listener>
接著注入一個request 作用域bean 到IOC 容器。代碼如下:
<bean id="requestBean" scope="request"> <property name="name" value="hjc" /> <aop:scoped-proxy /> </bean>
測試代碼如下:
@WebResource("/testService")public class TestRpc { @Autowired private RequestBean requestInfo; @ResourceMapping("test") public ActionResult test(ErrorContext context) { ActionResult result = new ActionResult(); pvgInfo.setName("hjc"); String name = requestInfo.getName(); result.setValue(name); return result; }}如上首先配置RequestContextListener 到web.xml 裡面,然後注入了Request 作用域的RequestBean 的實例到IOC 容器,最後TestRpc 內註入了RequestBean 的實例,方法test 首先調用了requestInfo 的方法setName 設置name 屬性,然後獲取name 屬性並返回。
這裡如果requestInfo 對像是單例的,那麼多個線程同時調用test 方法後,每個線程都是設置-獲取的操作,這個操作不是原子性的,會導致線程安全問題。而這裡聲明的作用域為request 級別,也是每個線程都有一個requestInfo 的本地變量。
上面例子方法請求的時序圖如下:
我們要著重關注調用test時候發生了什麼:
其實前面創建的requestInfo 是被經過CGliB 代理後的(感興趣的可以研究下ScopedProxyFactoryBean 這類),所以這裡調用setName 或者getName 時候會被DynamicAdvisedInterceptor 攔截的,攔擊器裡面最終會調用到RequestScope 的get 方法獲取當前線程持有的本地變量。
關鍵來了,我們要看一下RequestScope的get方法的源碼如下:
public Object get(String name, ObjectFactory objectFactory) { RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();//(1) Object scopedObject = attributes.getAttribute(name, getScope()); if (scopedObject == null) { scopedObject = objectFactory.getObject();//(2) attributes.setAttribute(name, scopedObject, getScope());//(3) } return scopedObject; }可知當發起一個請求時候,首先會通過RequestContextListener.requestInitialized 裡面調用RequestContextHolder.setRequestAttributess 設置requestAttributesHolder。
然後請求被路由到TestRpc 的test 方法後,test 方法內第一次調用setName 方法時候,最終會調用RequestScope.get()方法,get 方法內代碼(1)獲取通過RequestContextListener.requestInitialized 設置的線程本地變量requestAttributesHolder 保存的屬性集的值。
接著看該屬性集裡面是否有名字為requestInfo 的屬性,由於是第一次調用,所以不存在,所以會執行代碼(2)讓Spring 創建一個RequestInfo 對象,然後設置到屬性集attributes,也就是保存到了當前請求線程的本地內存裡面了。然後返回創建的對象,調用創建對象的setName。
最後test 方法內緊接著調用了getName 方法,最終會調用RequestScope.get() 方法,get 方法內代碼(1)獲取通過RequestContextListener.requestInitialized 設置的線程本地變量RequestAttributes,然後看該屬性集裡面是否有名字為requestInfo 的屬性,
由於是第一次調用setName 時候已經設置名字為requestInfo 的bean 到ThreadLocal 變量裡面了,並且調用setName 和getName 的是同一個線程,所以這裡直接返回了調用setName 時候創建的RequestInfo 對象,然後調用它的getName 方法。
到目前為止我們了解ThreadLocal 的實現原理,並指出ThreadLocal 不支持繼承性;然後緊接著講解了InheritableThreadLocal 是如何補償了ThreadLocal 不支持繼承的特性;最後簡單的介紹了Spring 框架中如何使用ThreadLocal 實現了Reqeust Scope 的Bean。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對武林網的支持。