j2ee servlet 和 threadlocal ,synchronized 与 web容器
生活随笔
收集整理的這篇文章主要介紹了
j2ee servlet 和 threadlocal ,synchronized 与 web容器
小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
在傳統(tǒng)的Web開發(fā)中,我們處理Http請求最常用的方式是通過實(shí)現(xiàn)Servlet對象來進(jìn)行Http請求的響應(yīng)。Servlet是J2EE的重要標(biāo)準(zhǔn)之一,規(guī)定了Java如何響應(yīng)Http請求的規(guī)范。通過HttpServletRequest和HttpServletResponse對象,我們能夠輕松地與Web容器交互。當(dāng)Web容器收到一個Http請求時,Web容器中的一個主調(diào)度線程會從事先定義好的線程池中分配一個當(dāng)前工作線程,將請求分配給當(dāng)前的工作線程,由該線程來執(zhí)行對應(yīng)的Servlet對象中的service方法。如果這個工作線程正在執(zhí)行的時候,Web容器收到另外一個請求,主調(diào)度線程會同樣從線程池中選擇另一個工作線程來服務(wù)新的請求。Web容器本身并不關(guān)心這個新的請求是否訪問的是同一個Servlet實(shí)例。因此,我們可以得出一個結(jié)論:對于同一個Servlet對象的多個請求,Servlet的service方法將在一個多線程的環(huán)境中并發(fā)執(zhí)行。所以,Web容器默認(rèn)采用單實(shí)例(單Servlet實(shí)例)多線程的方式來處理Http請求。這種處理方式能夠減少新建Servlet實(shí)例的開銷,從而縮短了對Http請求的響應(yīng)時間。但是,這樣的處理方式會導(dǎo)致變量訪問的線程安全問題。也就是說,Servlet對象并不是一個線程安全的對象。下面的測試代碼將證實(shí)這一點(diǎn):public class ThreadSafeTestServlet extends HttpServlet { // 定義一個實(shí)例變量,并非一個線程安全的變量 private int counter = 0; public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); } public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 輸出當(dāng)前Servlet的信息以及當(dāng)前線程的信息System.out.println(this + ":" + Thread.currentThread()); // 循環(huán),并增加實(shí)例變量counter的值for (int i = 0; i < 5; i++) { System.out.println("Counter = " + counter); try { Thread.sleep((long) Math.random() * 1000); counter++; } catch (InterruptedException exc) { } } }
}
這里參閱了網(wǎng)絡(luò)上一段著名的對Servlet線程安全性進(jìn)行測試的代碼(http://zwchen.iteye.com/blog/91088)。運(yùn)行之后,我們可以看一下這個例子的輸出:sample.SimpleServlet@11e1bbf:Thread[http-8081-Processor23,5,main]Counter = 60 Counter = 61 Counter = 62 Counter = 65 Counter = 68 Counter = 71 Counter = 74 Counter = 77 Counter = 80 Counter = 83 sample.SimpleServlet@11e1bbf:Thread[http-8081-Processor22,5,main] Counter = 61 Counter = 63 Counter = 66 Counter = 69 Counter= 72 Counter = 75 Counter = 78 Counter = 81 Counter = 84 Counter = 87 sample.SimpleServlet@11e1bbf:Thread[http-8081-Processor24,5,main] Counter = 61 Counter = 64 Counter = 67 Counter = 70 Counter = 73 Counter = 76 Counter = 79 Counter = 82 Counter = 85 Counter= 88通過上面的輸出,我們可以得出以下三個Servlet對象的運(yùn)行特性:1. Servlet對象是一個無狀態(tài)的單例對象(Singleton),因?yàn)槲覀兛吹蕉啻握埱蟮膖his指針?biāo)蛴〕鰜淼膆ashcode值都相同2. Servlet在不同的線程(線程池)中運(yùn)行,如http-8081-Processor22和http-8081-Processor23等輸出值可以明顯區(qū)分出不同的線程執(zhí)行了同一段Servlet邏輯代碼。3. Counter變量在不同的線程中共享,而且它的值被不同的線程修改,輸出時已經(jīng)不是順序輸出。也就是說,其他的線程會篡改當(dāng)前線程中實(shí)例變量的值,針對這些對象的訪問不是線程安全的。【有關(guān)線程安全的概念范疇】談到線程安全,對于許多初學(xué)者來說很容易引起概念上的混淆。線程安全,指的是在多線程環(huán)境下,一個類在執(zhí)行某個方法時,對類的內(nèi)部實(shí)例變量的訪問安全與否。因此,對于下面列出來的2類變量,不存在任何線程安全的說法:1)方法簽名中的任何參數(shù)變量。2)處于方法內(nèi)部的局部變量。任何針對上述形式的變量的訪問都是線程安全的,因?yàn)樗鼈兌继幱诜椒w的內(nèi)部,由當(dāng)前的執(zhí)行線程獨(dú)自管理。這就是線程安全問題的由來:在傳統(tǒng)的基于Servlet的開發(fā)模式中,Servlet對象內(nèi)部的實(shí)例變量不是線程安全的。在多線程環(huán)境中,這些變量的訪問需要通過特殊的手段進(jìn)行訪問控制。解決線程安全訪問的方法很多,比較容易想到的一種方案是使用同步機(jī)制,但是出于對Web應(yīng)用效率的考慮,這種機(jī)制在Web開發(fā)中的可行性很低,也違背了Servlet的設(shè)計初衷。因此,我們需要另辟蹊徑來解決這一困擾我們的問題。4.1.2ThreadLocal模式的實(shí)現(xiàn)機(jī)理在JDK的早期版本中,提供了一種解決多線程并發(fā)問題的方案: java.lang.ThreadLocal類。ThreadLocal類在維護(hù)變量時,實(shí)際使用了當(dāng)前線程(Thread)中的一個叫做ThreadLocalMap的獨(dú)立副本,每個線程可以獨(dú)立修改屬于自己的副本而不會互相影響,從而隔離了線程和線程,避免了線程訪問實(shí)例變量發(fā)生沖突的問題。ThreadLocal本身并不是一個線程,而是通過操作當(dāng)前線程(Thread)中的一個內(nèi)部變量來達(dá)到與其他線程隔離的目的。之所以取名為ThreadLocal,所期望表達(dá)的含義是其操作的對象是線程(Thread)的一個本地變量。如果我們看一下Thread的源碼實(shí)現(xiàn),就會發(fā)現(xiàn)這一變量,如代碼清單4-2所示:publicclass Thread implements Runnable { // 這里省略了許多其他的代碼ThreadLocal.ThreadLocalMap threadLocals = null;}這是JDK中Thread源碼的一部分,從中我們可以看出ThreadLocalMap跟隨著當(dāng)前的線程而存在。不同的線程Thread,擁有不同的ThreadLocalMap的本地實(shí)例變量,這也就是“副本”的含義。接下來我們再來看看ThreadLocal.ThreadLocalMap是如何定義的,以及ThreadLocal如何來操作它,如代碼清單4-3所示:public class ThreadLocal<T> {// 這里省略了許多其他代碼// 將value的值保存于當(dāng)前線程的本地變量中 public void set(T value) {// 獲取當(dāng)前線程Thread t = Thread.currentThread();// 調(diào)用getMap方法獲得當(dāng)前線程中的本地變量ThreadLocalMapThreadLocalMap map = getMap(t);// 如果ThreadLocalMap已存在,直接使用if (map != null)// 以當(dāng)前的ThreadLocal的實(shí)例作為key,存儲于當(dāng)前線程的// ThreadLocalMap中,如果當(dāng)前線程中被定義了多個不同的ThreadLocal// 的實(shí)例,則它們會作為不同key進(jìn)行存儲而不會互相干擾map.set(this, value);else// ThreadLocalMap不存在,則為當(dāng)前線程創(chuàng)建一個新的createMap(t, value); }// 獲取當(dāng)前線程中以當(dāng)前ThreadLocal實(shí)例為key的變量值 public T get() {// 獲取當(dāng)前線程Thread t = Thread.currentThread();// 獲取當(dāng)前線程中的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {// 獲取當(dāng)前線程中以當(dāng)前ThreadLocal實(shí)例為key的變量值ThreadLocalMap.Entry e = map.getEntry(this);if (e != null)return (T)e.value;}// 當(dāng)map不存在時,設(shè)置初始值return setInitialValue(); }// 從當(dāng)前線程中獲取與之對應(yīng)的ThreadLocalMap ThreadLocalMap getMap(Thread t) {return t.threadLocals; }// 創(chuàng)建當(dāng)前線程中的ThreadLocalMap void createMap(Thread t, T firstValue) {// 調(diào)用構(gòu)造函數(shù)生成當(dāng)前線程中的ThreadLocalMapt.threadLocals = new ThreadLocalMap(this, firstValue); }// ThreadLoaclMap的定義 static class ThreadLocalMap {// 這里省略了許多代碼 } }
從上述代碼中,我們看到了ThreadLocal類的大致結(jié)構(gòu)和進(jìn)行ThreadLocalMap的操作。我們可以從中得出以下的結(jié)論:1. ThreadLocalMap變量屬于線程(Thread)的內(nèi)部屬性,不同的線程(Thread)擁有完全不同的ThreadLocalMap變量。2. 線程(Thread)中的ThreadLocalMap變量的值是在ThreadLocal對象進(jìn)行set或者get操作時創(chuàng)建的。3. 在創(chuàng)建ThreadLocalMap之前,會首先檢查當(dāng)前線程(Thread)中的ThreadLocalMap變量是否已經(jīng)存在,如果不存在則創(chuàng)建一個;如果已經(jīng)存在,則使用當(dāng)前線程(Thread)已創(chuàng)建的ThreadLocalMap。4.使用當(dāng)前線程(Thread)的ThreadLocalMap的關(guān)鍵在于使用當(dāng)前的ThreadLocal的實(shí)例作為key進(jìn)行存儲。ThreadLocal模式,至少從兩個方面完成了數(shù)據(jù)訪問隔離,有了橫向和縱向的兩種不同的隔離方式,ThreadLocal模式就能真正地做到線程安全:縱向隔離 —— 線程(Thread)與線程(Thread)之間的數(shù)據(jù)訪問隔離。這一點(diǎn)由線程(Thread)的數(shù)據(jù)結(jié)構(gòu)保證。因?yàn)槊總€線程(Thread)在進(jìn)行對象訪問時,訪問的都是各自線程自己的ThreadLocalMap。橫向隔離 ——同一個線程中,不同的ThreadLocal實(shí)例操作的對象之間的相互隔離。這一點(diǎn)由ThreadLocalMap在存儲時,采用當(dāng)前ThreadLocal的實(shí)例作為key來保證。ThreadLocal模式并不是什么高深的學(xué)問,它甚至從JDK1.2開始就存在于Java世界中。由此可見,我們掌握一種知識的最終目的是熟練而合理地運(yùn)用它。【深入比較ThreadLocal模式與synchronized關(guān)鍵字】ThreadLocal模式與synchronized關(guān)鍵字都是用于處理多線程并發(fā)訪問變量的問題。只是兩者處理問題的角度和思路不同。1)ThreadLocal是一個Java類,通過對當(dāng)前線程(Thread)中的局部變量的操作來解決不同線程的變量訪問的沖突問題。所以,ThreadLocal提供了線程安全的共享對象機(jī)制,每個線程(Thread)都擁有其副本。2)Java中的synchronized是一個保留字,它依靠JVM的鎖機(jī)制來實(shí)現(xiàn)臨界區(qū)的函數(shù)或者變量在訪問中的原子性。在同步機(jī)制中,通過對象的鎖機(jī)制保證同一時間只有一個線程訪問變量。此時,被用作“鎖機(jī)制”的變量是多個線程共享的。同步機(jī)制采用了“以時間換空間”的方式,提供一份變量,讓不同的線程排隊(duì)訪問。而ThreadLocal采用了“以空間換時間”的方式,為每一個線程都提供了一份變量的副本,從而實(shí)現(xiàn)同時訪問而互不影響。4.1.3ThreadLocal模式的應(yīng)用場景在分析了ThreadLocal的源碼之后,我們來看看ThreadLocal模式最合適的業(yè)務(wù)場景。在一個完整的“請求-響應(yīng)”過程中,主線程的執(zhí)行過程總是貫穿始終。當(dāng)這個主線程的執(zhí)行過程中被加入了ThreadLocal的讀寫時,會對整個過程產(chǎn)生怎樣的影響呢?我們根據(jù)之前源碼分析的結(jié)果,并結(jié)合分層開發(fā)模式,把整個流程畫下來,如圖4-1所示:從上面圖中我們可以看到,由于ThreadLocal所操作的是維持于整個Thread生命周期的副本(ThreadLocalMap),所以無論在J2EE程序程序的哪個層次(表示層、業(yè)務(wù)邏輯層或者持久層),只要在一個Thread的生命周期之內(nèi),存儲于ThreadLocalMap中的對象都是線程安全的(因?yàn)門hreadLocalMap本身僅僅隸屬于當(dāng)前的執(zhí)行線程,是執(zhí)行線程內(nèi)部的一個屬性變量。我們用圖中的陰影部分來表示這個變量的存儲空間)。而這一點(diǎn),正是被我們用于來解決多線程環(huán)境中的變量共享問題的核心技術(shù)。ThreadLocal的這一特性也使其能夠被廣泛地應(yīng)用于J2EE開發(fā)中的許多業(yè)務(wù)場景?!緮?shù)據(jù)共享OR 數(shù)據(jù)傳遞?】ThreadLocal模式由于利用了Java自身的語法特性而顯得異常簡單和便利,因而被廣泛應(yīng)用于J2EE開發(fā),尤其是應(yīng)對跨層次的資源共享,例如在Spring中,就有使用ThreadLocal模式來管理數(shù)據(jù)庫連接或者Hibernate的Session的范例。在一些比較著名的論壇中,有著很多關(guān)于使用ThreadLocal模式來做數(shù)據(jù)傳遞的討論。事實(shí)上,這是對ThreadLocal模式的一個極大的誤解。讀者需要注意的是,ThreadLocal模式解決的是同一線程中隸屬于不同開發(fā)層次的數(shù)據(jù)共享問題,而不是在不同的開發(fā)層次中進(jìn)行數(shù)據(jù)傳遞。1)ThreadLocal模式的核心在于實(shí)現(xiàn)一個共享環(huán)境(類的內(nèi)部封裝了ThreadLocal的靜態(tài)實(shí)例)。所以,在操作ThreadLocal時,這一共享環(huán)境會跨越多個開發(fā)層次而隨處存在。2)隨處存在的共享環(huán)境造成了所有的開發(fā)層次的共同依賴,從而使得所有的開發(fā)層次都耦合在了一起,從而變得無法獨(dú)立測試。3)數(shù)據(jù)傳遞應(yīng)該通過接口函數(shù)的簽名顯式聲明,這樣才能夠從接口聲明中表達(dá)接口所表達(dá)的真正含義。ThreadLocal模式位于實(shí)現(xiàn)的內(nèi)部,從而使得接口與接口之間無法達(dá)成一致的聲明契約。Struts2的解耦合的設(shè)計理念使得Struts2的MVC實(shí)現(xiàn)成為了使用ThreadLocal模式的天然場所。在第三章中,我們已經(jīng)介紹了一些基本概念,Struts2通過引入XWork框架,將整個Http請求的過程拆分成為與Web容器有關(guān)和與Web容器無關(guān)的兩個執(zhí)行階段。而這兩個階段的數(shù)據(jù)交互就是通過ThreadLocal模式中的線程共享副本安全地進(jìn)行。在其中,我們沒有看到數(shù)據(jù)傳遞,存在的只是整個執(zhí)行線程的數(shù)據(jù)共享。4.1.4ThreadLocal模式的核心元素仔細(xì)分析上一節(jié)的示意圖(圖4-1),我們可以發(fā)現(xiàn),要完成ThreadLocal模式,其中最關(guān)鍵的地方就是創(chuàng)建一個任何地方都可以訪問到的ThreadLocal實(shí)例(也就是執(zhí)行示意圖中的菱形部分)。而這一點(diǎn),我們可以通過類的靜態(tài)實(shí)例變量來實(shí)現(xiàn),這個用于承載靜態(tài)實(shí)例變量的類就被視作是一個共享環(huán)境。我們來看一個例子,如代碼清單4-4所示:public class Counter {// 新建一個靜態(tài)的ThreadLocal變量,并通過get方法將其變?yōu)橐粋€可訪問的對象 privatestatic ThreadLocal<Integer> counterContext = new ThreadLocal<Integer>() { protected synchronized Integer initialValue() { return 10; }};// 通過靜態(tài)的get方法訪問ThreadLocal中存儲的值public static Integer get() {return counterContext.get();}// 通過靜態(tài)的set方法將變量值設(shè)置到ThreadLocal中publicstatic void set(Integer value) {counterContext.set(value);}// 封裝業(yè)務(wù)邏輯,操作存儲于ThreadLocal中的變量public static Integer getNextCounter() { counterContext.set(counterContext.get() + 1); return counterContext.get();} }在這個Counter類中,我們實(shí)現(xiàn)了一個靜態(tài)的ThreadLocal變量,并通過get方法將ThreadLocal中存儲的值暴露出來。我們還封裝了一個帶有業(yè)務(wù)邏輯的方法getNextCounter,操作ThreadLocal中的值,將其加1,并返回計算后的值。此時,Counter類就變成了一個數(shù)據(jù)共享環(huán)境,我們也擁有了實(shí)現(xiàn)ThreadLocal模式的關(guān)鍵要素。有了它,我們來編寫一個簡單的測試,如代碼清單4-5所示:publicclass ThreadLocalTest extends Thread { public void run() {for(int i = 0; i < 3; i++){ System.out.println("Thread[" + Thread.currentThread().getName() + "],counter=" + Counter.getNextCounter());}}}這是一個簡單的線程類,循環(huán)輸出當(dāng)前線程的名稱和getNextCounter的結(jié)果,由于getNextCounter中的邏輯所操作的是ThreadLocal中的變量,所以無論同時有多少個線程在運(yùn)行,返回的值將僅與當(dāng)前線程的變量值有關(guān),也就是說,在同一個線程中,變量值會被連續(xù)累加。這一點(diǎn)可以通過如下的測試代碼證實(shí):publicclass Test {public static void main(String[] args) throws Exception {ThreadLocalTest testThread1 = new ThreadLocalTest();ThreadLocalTest testThread2 = new ThreadLocalTest();ThreadLocalTest testThread3 = new ThreadLocalTest();testThread1.start();testThread2.start();testThread3.start();}}我們來運(yùn)行一下上面的代碼,并看看輸出結(jié)果:Thread[Thread-2],counter=11Thread[Thread-2],counter=12Thread[Thread-2],counter=13Thread[Thread-0],counter=11Thread[Thread-0],counter=12Thread[Thread-0],counter=13Thread[Thread-1],counter=11Thread[Thread-1],counter=12Thread[Thread-1],counter=13上面的輸出結(jié)果也證實(shí)了,counter的值在多線程環(huán)境中的訪問是線程安全的。從對例子的分析中我們可以再次體會到,ThreadLocal模式最合適的使用場景:在同一個線程(Thread)的不同開發(fā)層次中共享數(shù)據(jù)。從上面的例子中,我們可以簡單總結(jié)出實(shí)現(xiàn)ThreadLocal模式的兩個主要步驟:1.建立一個類,并在其中封裝一個靜態(tài)的ThreadLocal變量,使其成為一個共享數(shù)據(jù)環(huán)境。2. 在類中實(shí)現(xiàn)訪問靜態(tài)ThreadLocal變量的靜態(tài)方法(設(shè)值和取值)。建立在ThreadLocal模式的實(shí)現(xiàn)步驟之上,ThreadLocal的使用則更加簡單。在線程執(zhí)行的任何地方,我們都可以通過訪問共享數(shù)據(jù)類中所提供的ThreadLocal變量的設(shè)值和取值方法安全地獲得當(dāng)前線程中安全的變量值。這兩個步驟,我們之后會在Struts2的實(shí)現(xiàn)中多次提及,讀者只要能充分理解ThreadLocal處理多線程訪問的基本原理,就能對Struts2的數(shù)據(jù)訪問和數(shù)據(jù)共享的設(shè)計有一個整體的認(rèn)識。講到這里,我們回過頭來看看ThreadLocal模式的引入,到底對我們的編程模型有什么重要的意義呢?downpour寫道結(jié)論 使用ThreadLocal模式,可以使得數(shù)據(jù)在不同的編程層次得到有效地共享。這一點(diǎn),是由ThreadLocal模式的實(shí)現(xiàn)機(jī)理決定的。因?yàn)閷?shí)現(xiàn)ThreadLocal模式的一個重要步驟,就是構(gòu)建一個靜態(tài)的共享存儲空間。從而使得任何對象在任何時刻都可以安全地對數(shù)據(jù)進(jìn)行訪問。downpour 寫道結(jié)論 使用ThreadLocal模式,可以對執(zhí)行邏輯與執(zhí)行數(shù)據(jù)進(jìn)行有效解耦。這一點(diǎn)是ThreadLocal模式給我們帶來的最為核心的一個影響。因?yàn)樵谝话闱闆r下,Java對象之間的協(xié)作關(guān)系,主要通過參數(shù)和返回值來進(jìn)行消息傳遞,這也是對象協(xié)作之間的一個重要依賴。而ThreadLocal模式徹底打破了這種依賴關(guān)系,通過線程安全的共享對象來進(jìn)行數(shù)據(jù)共享,可以有效避免在編程層次之間形成數(shù)據(jù)依賴。這也成為了XWork事件處理體系設(shè)計的核心。
這里參閱了網(wǎng)絡(luò)上一段著名的對Servlet線程安全性進(jìn)行測試的代碼(http://zwchen.iteye.com/blog/91088)。運(yùn)行之后,我們可以看一下這個例子的輸出:sample.SimpleServlet@11e1bbf:Thread[http-8081-Processor23,5,main]Counter = 60 Counter = 61 Counter = 62 Counter = 65 Counter = 68 Counter = 71 Counter = 74 Counter = 77 Counter = 80 Counter = 83 sample.SimpleServlet@11e1bbf:Thread[http-8081-Processor22,5,main] Counter = 61 Counter = 63 Counter = 66 Counter = 69 Counter= 72 Counter = 75 Counter = 78 Counter = 81 Counter = 84 Counter = 87 sample.SimpleServlet@11e1bbf:Thread[http-8081-Processor24,5,main] Counter = 61 Counter = 64 Counter = 67 Counter = 70 Counter = 73 Counter = 76 Counter = 79 Counter = 82 Counter = 85 Counter= 88通過上面的輸出,我們可以得出以下三個Servlet對象的運(yùn)行特性:1. Servlet對象是一個無狀態(tài)的單例對象(Singleton),因?yàn)槲覀兛吹蕉啻握埱蟮膖his指針?biāo)蛴〕鰜淼膆ashcode值都相同2. Servlet在不同的線程(線程池)中運(yùn)行,如http-8081-Processor22和http-8081-Processor23等輸出值可以明顯區(qū)分出不同的線程執(zhí)行了同一段Servlet邏輯代碼。3. Counter變量在不同的線程中共享,而且它的值被不同的線程修改,輸出時已經(jīng)不是順序輸出。也就是說,其他的線程會篡改當(dāng)前線程中實(shí)例變量的值,針對這些對象的訪問不是線程安全的。【有關(guān)線程安全的概念范疇】談到線程安全,對于許多初學(xué)者來說很容易引起概念上的混淆。線程安全,指的是在多線程環(huán)境下,一個類在執(zhí)行某個方法時,對類的內(nèi)部實(shí)例變量的訪問安全與否。因此,對于下面列出來的2類變量,不存在任何線程安全的說法:1)方法簽名中的任何參數(shù)變量。2)處于方法內(nèi)部的局部變量。任何針對上述形式的變量的訪問都是線程安全的,因?yàn)樗鼈兌继幱诜椒w的內(nèi)部,由當(dāng)前的執(zhí)行線程獨(dú)自管理。這就是線程安全問題的由來:在傳統(tǒng)的基于Servlet的開發(fā)模式中,Servlet對象內(nèi)部的實(shí)例變量不是線程安全的。在多線程環(huán)境中,這些變量的訪問需要通過特殊的手段進(jìn)行訪問控制。解決線程安全訪問的方法很多,比較容易想到的一種方案是使用同步機(jī)制,但是出于對Web應(yīng)用效率的考慮,這種機(jī)制在Web開發(fā)中的可行性很低,也違背了Servlet的設(shè)計初衷。因此,我們需要另辟蹊徑來解決這一困擾我們的問題。4.1.2ThreadLocal模式的實(shí)現(xiàn)機(jī)理在JDK的早期版本中,提供了一種解決多線程并發(fā)問題的方案: java.lang.ThreadLocal類。ThreadLocal類在維護(hù)變量時,實(shí)際使用了當(dāng)前線程(Thread)中的一個叫做ThreadLocalMap的獨(dú)立副本,每個線程可以獨(dú)立修改屬于自己的副本而不會互相影響,從而隔離了線程和線程,避免了線程訪問實(shí)例變量發(fā)生沖突的問題。ThreadLocal本身并不是一個線程,而是通過操作當(dāng)前線程(Thread)中的一個內(nèi)部變量來達(dá)到與其他線程隔離的目的。之所以取名為ThreadLocal,所期望表達(dá)的含義是其操作的對象是線程(Thread)的一個本地變量。如果我們看一下Thread的源碼實(shí)現(xiàn),就會發(fā)現(xiàn)這一變量,如代碼清單4-2所示:publicclass Thread implements Runnable { // 這里省略了許多其他的代碼ThreadLocal.ThreadLocalMap threadLocals = null;}這是JDK中Thread源碼的一部分,從中我們可以看出ThreadLocalMap跟隨著當(dāng)前的線程而存在。不同的線程Thread,擁有不同的ThreadLocalMap的本地實(shí)例變量,這也就是“副本”的含義。接下來我們再來看看ThreadLocal.ThreadLocalMap是如何定義的,以及ThreadLocal如何來操作它,如代碼清單4-3所示:public class ThreadLocal<T> {// 這里省略了許多其他代碼// 將value的值保存于當(dāng)前線程的本地變量中 public void set(T value) {// 獲取當(dāng)前線程Thread t = Thread.currentThread();// 調(diào)用getMap方法獲得當(dāng)前線程中的本地變量ThreadLocalMapThreadLocalMap map = getMap(t);// 如果ThreadLocalMap已存在,直接使用if (map != null)// 以當(dāng)前的ThreadLocal的實(shí)例作為key,存儲于當(dāng)前線程的// ThreadLocalMap中,如果當(dāng)前線程中被定義了多個不同的ThreadLocal// 的實(shí)例,則它們會作為不同key進(jìn)行存儲而不會互相干擾map.set(this, value);else// ThreadLocalMap不存在,則為當(dāng)前線程創(chuàng)建一個新的createMap(t, value); }// 獲取當(dāng)前線程中以當(dāng)前ThreadLocal實(shí)例為key的變量值 public T get() {// 獲取當(dāng)前線程Thread t = Thread.currentThread();// 獲取當(dāng)前線程中的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {// 獲取當(dāng)前線程中以當(dāng)前ThreadLocal實(shí)例為key的變量值ThreadLocalMap.Entry e = map.getEntry(this);if (e != null)return (T)e.value;}// 當(dāng)map不存在時,設(shè)置初始值return setInitialValue(); }// 從當(dāng)前線程中獲取與之對應(yīng)的ThreadLocalMap ThreadLocalMap getMap(Thread t) {return t.threadLocals; }// 創(chuàng)建當(dāng)前線程中的ThreadLocalMap void createMap(Thread t, T firstValue) {// 調(diào)用構(gòu)造函數(shù)生成當(dāng)前線程中的ThreadLocalMapt.threadLocals = new ThreadLocalMap(this, firstValue); }// ThreadLoaclMap的定義 static class ThreadLocalMap {// 這里省略了許多代碼 } }
從上述代碼中,我們看到了ThreadLocal類的大致結(jié)構(gòu)和進(jìn)行ThreadLocalMap的操作。我們可以從中得出以下的結(jié)論:1. ThreadLocalMap變量屬于線程(Thread)的內(nèi)部屬性,不同的線程(Thread)擁有完全不同的ThreadLocalMap變量。2. 線程(Thread)中的ThreadLocalMap變量的值是在ThreadLocal對象進(jìn)行set或者get操作時創(chuàng)建的。3. 在創(chuàng)建ThreadLocalMap之前,會首先檢查當(dāng)前線程(Thread)中的ThreadLocalMap變量是否已經(jīng)存在,如果不存在則創(chuàng)建一個;如果已經(jīng)存在,則使用當(dāng)前線程(Thread)已創(chuàng)建的ThreadLocalMap。4.使用當(dāng)前線程(Thread)的ThreadLocalMap的關(guān)鍵在于使用當(dāng)前的ThreadLocal的實(shí)例作為key進(jìn)行存儲。ThreadLocal模式,至少從兩個方面完成了數(shù)據(jù)訪問隔離,有了橫向和縱向的兩種不同的隔離方式,ThreadLocal模式就能真正地做到線程安全:縱向隔離 —— 線程(Thread)與線程(Thread)之間的數(shù)據(jù)訪問隔離。這一點(diǎn)由線程(Thread)的數(shù)據(jù)結(jié)構(gòu)保證。因?yàn)槊總€線程(Thread)在進(jìn)行對象訪問時,訪問的都是各自線程自己的ThreadLocalMap。橫向隔離 ——同一個線程中,不同的ThreadLocal實(shí)例操作的對象之間的相互隔離。這一點(diǎn)由ThreadLocalMap在存儲時,采用當(dāng)前ThreadLocal的實(shí)例作為key來保證。ThreadLocal模式并不是什么高深的學(xué)問,它甚至從JDK1.2開始就存在于Java世界中。由此可見,我們掌握一種知識的最終目的是熟練而合理地運(yùn)用它。【深入比較ThreadLocal模式與synchronized關(guān)鍵字】ThreadLocal模式與synchronized關(guān)鍵字都是用于處理多線程并發(fā)訪問變量的問題。只是兩者處理問題的角度和思路不同。1)ThreadLocal是一個Java類,通過對當(dāng)前線程(Thread)中的局部變量的操作來解決不同線程的變量訪問的沖突問題。所以,ThreadLocal提供了線程安全的共享對象機(jī)制,每個線程(Thread)都擁有其副本。2)Java中的synchronized是一個保留字,它依靠JVM的鎖機(jī)制來實(shí)現(xiàn)臨界區(qū)的函數(shù)或者變量在訪問中的原子性。在同步機(jī)制中,通過對象的鎖機(jī)制保證同一時間只有一個線程訪問變量。此時,被用作“鎖機(jī)制”的變量是多個線程共享的。同步機(jī)制采用了“以時間換空間”的方式,提供一份變量,讓不同的線程排隊(duì)訪問。而ThreadLocal采用了“以空間換時間”的方式,為每一個線程都提供了一份變量的副本,從而實(shí)現(xiàn)同時訪問而互不影響。4.1.3ThreadLocal模式的應(yīng)用場景在分析了ThreadLocal的源碼之后,我們來看看ThreadLocal模式最合適的業(yè)務(wù)場景。在一個完整的“請求-響應(yīng)”過程中,主線程的執(zhí)行過程總是貫穿始終。當(dāng)這個主線程的執(zhí)行過程中被加入了ThreadLocal的讀寫時,會對整個過程產(chǎn)生怎樣的影響呢?我們根據(jù)之前源碼分析的結(jié)果,并結(jié)合分層開發(fā)模式,把整個流程畫下來,如圖4-1所示:從上面圖中我們可以看到,由于ThreadLocal所操作的是維持于整個Thread生命周期的副本(ThreadLocalMap),所以無論在J2EE程序程序的哪個層次(表示層、業(yè)務(wù)邏輯層或者持久層),只要在一個Thread的生命周期之內(nèi),存儲于ThreadLocalMap中的對象都是線程安全的(因?yàn)門hreadLocalMap本身僅僅隸屬于當(dāng)前的執(zhí)行線程,是執(zhí)行線程內(nèi)部的一個屬性變量。我們用圖中的陰影部分來表示這個變量的存儲空間)。而這一點(diǎn),正是被我們用于來解決多線程環(huán)境中的變量共享問題的核心技術(shù)。ThreadLocal的這一特性也使其能夠被廣泛地應(yīng)用于J2EE開發(fā)中的許多業(yè)務(wù)場景?!緮?shù)據(jù)共享OR 數(shù)據(jù)傳遞?】ThreadLocal模式由于利用了Java自身的語法特性而顯得異常簡單和便利,因而被廣泛應(yīng)用于J2EE開發(fā),尤其是應(yīng)對跨層次的資源共享,例如在Spring中,就有使用ThreadLocal模式來管理數(shù)據(jù)庫連接或者Hibernate的Session的范例。在一些比較著名的論壇中,有著很多關(guān)于使用ThreadLocal模式來做數(shù)據(jù)傳遞的討論。事實(shí)上,這是對ThreadLocal模式的一個極大的誤解。讀者需要注意的是,ThreadLocal模式解決的是同一線程中隸屬于不同開發(fā)層次的數(shù)據(jù)共享問題,而不是在不同的開發(fā)層次中進(jìn)行數(shù)據(jù)傳遞。1)ThreadLocal模式的核心在于實(shí)現(xiàn)一個共享環(huán)境(類的內(nèi)部封裝了ThreadLocal的靜態(tài)實(shí)例)。所以,在操作ThreadLocal時,這一共享環(huán)境會跨越多個開發(fā)層次而隨處存在。2)隨處存在的共享環(huán)境造成了所有的開發(fā)層次的共同依賴,從而使得所有的開發(fā)層次都耦合在了一起,從而變得無法獨(dú)立測試。3)數(shù)據(jù)傳遞應(yīng)該通過接口函數(shù)的簽名顯式聲明,這樣才能夠從接口聲明中表達(dá)接口所表達(dá)的真正含義。ThreadLocal模式位于實(shí)現(xiàn)的內(nèi)部,從而使得接口與接口之間無法達(dá)成一致的聲明契約。Struts2的解耦合的設(shè)計理念使得Struts2的MVC實(shí)現(xiàn)成為了使用ThreadLocal模式的天然場所。在第三章中,我們已經(jīng)介紹了一些基本概念,Struts2通過引入XWork框架,將整個Http請求的過程拆分成為與Web容器有關(guān)和與Web容器無關(guān)的兩個執(zhí)行階段。而這兩個階段的數(shù)據(jù)交互就是通過ThreadLocal模式中的線程共享副本安全地進(jìn)行。在其中,我們沒有看到數(shù)據(jù)傳遞,存在的只是整個執(zhí)行線程的數(shù)據(jù)共享。4.1.4ThreadLocal模式的核心元素仔細(xì)分析上一節(jié)的示意圖(圖4-1),我們可以發(fā)現(xiàn),要完成ThreadLocal模式,其中最關(guān)鍵的地方就是創(chuàng)建一個任何地方都可以訪問到的ThreadLocal實(shí)例(也就是執(zhí)行示意圖中的菱形部分)。而這一點(diǎn),我們可以通過類的靜態(tài)實(shí)例變量來實(shí)現(xiàn),這個用于承載靜態(tài)實(shí)例變量的類就被視作是一個共享環(huán)境。我們來看一個例子,如代碼清單4-4所示:public class Counter {// 新建一個靜態(tài)的ThreadLocal變量,并通過get方法將其變?yōu)橐粋€可訪問的對象 privatestatic ThreadLocal<Integer> counterContext = new ThreadLocal<Integer>() { protected synchronized Integer initialValue() { return 10; }};// 通過靜態(tài)的get方法訪問ThreadLocal中存儲的值public static Integer get() {return counterContext.get();}// 通過靜態(tài)的set方法將變量值設(shè)置到ThreadLocal中publicstatic void set(Integer value) {counterContext.set(value);}// 封裝業(yè)務(wù)邏輯,操作存儲于ThreadLocal中的變量public static Integer getNextCounter() { counterContext.set(counterContext.get() + 1); return counterContext.get();} }在這個Counter類中,我們實(shí)現(xiàn)了一個靜態(tài)的ThreadLocal變量,并通過get方法將ThreadLocal中存儲的值暴露出來。我們還封裝了一個帶有業(yè)務(wù)邏輯的方法getNextCounter,操作ThreadLocal中的值,將其加1,并返回計算后的值。此時,Counter類就變成了一個數(shù)據(jù)共享環(huán)境,我們也擁有了實(shí)現(xiàn)ThreadLocal模式的關(guān)鍵要素。有了它,我們來編寫一個簡單的測試,如代碼清單4-5所示:publicclass ThreadLocalTest extends Thread { public void run() {for(int i = 0; i < 3; i++){ System.out.println("Thread[" + Thread.currentThread().getName() + "],counter=" + Counter.getNextCounter());}}}這是一個簡單的線程類,循環(huán)輸出當(dāng)前線程的名稱和getNextCounter的結(jié)果,由于getNextCounter中的邏輯所操作的是ThreadLocal中的變量,所以無論同時有多少個線程在運(yùn)行,返回的值將僅與當(dāng)前線程的變量值有關(guān),也就是說,在同一個線程中,變量值會被連續(xù)累加。這一點(diǎn)可以通過如下的測試代碼證實(shí):publicclass Test {public static void main(String[] args) throws Exception {ThreadLocalTest testThread1 = new ThreadLocalTest();ThreadLocalTest testThread2 = new ThreadLocalTest();ThreadLocalTest testThread3 = new ThreadLocalTest();testThread1.start();testThread2.start();testThread3.start();}}我們來運(yùn)行一下上面的代碼,并看看輸出結(jié)果:Thread[Thread-2],counter=11Thread[Thread-2],counter=12Thread[Thread-2],counter=13Thread[Thread-0],counter=11Thread[Thread-0],counter=12Thread[Thread-0],counter=13Thread[Thread-1],counter=11Thread[Thread-1],counter=12Thread[Thread-1],counter=13上面的輸出結(jié)果也證實(shí)了,counter的值在多線程環(huán)境中的訪問是線程安全的。從對例子的分析中我們可以再次體會到,ThreadLocal模式最合適的使用場景:在同一個線程(Thread)的不同開發(fā)層次中共享數(shù)據(jù)。從上面的例子中,我們可以簡單總結(jié)出實(shí)現(xiàn)ThreadLocal模式的兩個主要步驟:1.建立一個類,并在其中封裝一個靜態(tài)的ThreadLocal變量,使其成為一個共享數(shù)據(jù)環(huán)境。2. 在類中實(shí)現(xiàn)訪問靜態(tài)ThreadLocal變量的靜態(tài)方法(設(shè)值和取值)。建立在ThreadLocal模式的實(shí)現(xiàn)步驟之上,ThreadLocal的使用則更加簡單。在線程執(zhí)行的任何地方,我們都可以通過訪問共享數(shù)據(jù)類中所提供的ThreadLocal變量的設(shè)值和取值方法安全地獲得當(dāng)前線程中安全的變量值。這兩個步驟,我們之后會在Struts2的實(shí)現(xiàn)中多次提及,讀者只要能充分理解ThreadLocal處理多線程訪問的基本原理,就能對Struts2的數(shù)據(jù)訪問和數(shù)據(jù)共享的設(shè)計有一個整體的認(rèn)識。講到這里,我們回過頭來看看ThreadLocal模式的引入,到底對我們的編程模型有什么重要的意義呢?downpour寫道結(jié)論 使用ThreadLocal模式,可以使得數(shù)據(jù)在不同的編程層次得到有效地共享。這一點(diǎn),是由ThreadLocal模式的實(shí)現(xiàn)機(jī)理決定的。因?yàn)閷?shí)現(xiàn)ThreadLocal模式的一個重要步驟,就是構(gòu)建一個靜態(tài)的共享存儲空間。從而使得任何對象在任何時刻都可以安全地對數(shù)據(jù)進(jìn)行訪問。downpour 寫道結(jié)論 使用ThreadLocal模式,可以對執(zhí)行邏輯與執(zhí)行數(shù)據(jù)進(jìn)行有效解耦。這一點(diǎn)是ThreadLocal模式給我們帶來的最為核心的一個影響。因?yàn)樵谝话闱闆r下,Java對象之間的協(xié)作關(guān)系,主要通過參數(shù)和返回值來進(jìn)行消息傳遞,這也是對象協(xié)作之間的一個重要依賴。而ThreadLocal模式徹底打破了這種依賴關(guān)系,通過線程安全的共享對象來進(jìn)行數(shù)據(jù)共享,可以有效避免在編程層次之間形成數(shù)據(jù)依賴。這也成為了XWork事件處理體系設(shè)計的核心。
總結(jié)
以上是生活随笔為你收集整理的j2ee servlet 和 threadlocal ,synchronized 与 web容器的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Struts标签、Ognl表达式、el表
- 下一篇: spring 总结