详解线程本地变量ThreadLocal
并發應用的一個關鍵地方就是共享數據。如果你創建一個類對象,實現Runnable接口,然后多個Thread對象使用同樣的Runnable對象,全部的線程都共享同樣的屬性。這意味著,如果你在一個線程里改變一個屬性,全部的線程都會受到這個改變的影響。
有時,你希望程序里的各個線程的屬性不會被共享。 Java 并發 API提供了一個很清楚的機制叫本地線程變量即ThreadLocal。
模擬ThreadLocal類實現:線程范圍內的共享變量,每個線程只能訪問他自己的,不能訪問別的線程。
一、本地線程變量使用場景
并發應用的一個關鍵地方就是共享數據。如果你創建一個類對象,實現Runnable接口,然后多個Thread對象使用同樣的Runnable對象,全部的線程都共享同樣的屬性。這意味著,如果你在一個線程里改變一個屬性,全部的線程都會受到這個改變的影響。
有時,你希望程序里的各個線程的屬性不會被共享。 Java 并發 API提供了一個很清楚的機制叫本地線程變量即ThreadLocal。
模擬ThreadLocal類實現:線程范圍內的共享變量,每個線程只能訪問他自己的,不能訪問別的線程。
二、對ThreadLocal的理解
ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做本地線程變量,其實意思差不多。
ThreadLocal和本地線程沒有半毛錢關系,更不是一個特殊的Thread,它只是一個線程的局部變量(其實就是一個Map用于存儲每一個線程的變量副本,Map中元素的Key為線程對象,而Value對應線程的變量副本),ThreadLocal會為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。
對于多線程資源共享的問題,同步機制(Synchronized)采用了“以時間換空間”的方式,而ThreadLocal采用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。
官方對ThreadLocal的描述:
1、每個線程都有自己的局部變量
每個線程都有一個獨立于其他線程的上下文來保存這個變量,一個線程的本地變量對其他線程是不可見的(有前提,后面解釋) 2、獨立于變量的初始化副本
ThreadLocal可以給一個初始值,而每個線程都會獲得這個初始化值的一個副本,這樣才能保證不同的線程都有一份拷貝。
3、狀態與某一個線程相關聯 ThreadLocal
不是用于解決共享變量的問題的,不是為了協調線程同步而存在,而是為了方便每個線程處理自己的狀態而引入的一個機制,理解這點對正確使用ThreadLocal至關重要。
通過ThreadLocal存取的數據,總是與當前線程相關,也就是說,JVM
為每個運行的線程,綁定了私有的本地實例存取空間,從而為多線程環境常出現的并發訪問問題提供了一種隔離機制。 我們還是先來看一個例子:
假設有這樣一個數據庫鏈接管理類,這段代碼在單線程中使用是沒有任何問題的,但是如果在多線程中使用呢?很顯然,在多線程中使用會存在線程安全問題:第一,這里面的2個方法都沒有進行同步,很可能在openConnection方法中會多次創建connect;第二,由于connect是共享變量,那么必然在調用connect的地方需要使用到同步來保障線程安全,因為很可能一個線程在使用connect進行數據庫操作,而另外一個線程調用closeConnection關閉鏈接。
所以出于線程安全的考慮,必須將這段代碼的兩個方法進行同步處理,并且在調用connect的地方需要進行同步處理。
這樣將會大大影響程序執行效率,因為一個線程在使用connect進行數據庫操作的時候,其他線程只有等待。
那么大家來仔細分析一下這個問題,這地方到底需不需要將connect變量進行共享?事實上,是不需要的。假如每個線程中都有一個connect變量,各個線程之間對connect變量的訪問實際上是沒有依賴關系的,即一個線程不需要關心其他線程是否對這個connect進行了修改的。
到這里,可能會有朋友想到,既然不需要在線程之間共享這個變量,可以直接這樣處理,在每個需要使用數據庫連接的方法中具體使用時才創建數據庫鏈接,然后在方法調用完畢再釋放這個連接。比如下面這樣:
class ConnectionManager {private Connection connect = null;public Connection openConnection() {if(connect == null){connect = DriverManager.getConnection();}return connect;}public void closeConnection() {if(connect!=null)connect.close();} }class Dao{public void insert() {ConnectionManager connectionManager = new ConnectionManager();Connection connection = connectionManager.openConnection();//使用connection進行操作connectionManager.closeConnection();} }這樣處理確實也沒有任何問題,由于每次都是在方法內部創建的連接,那么線程之間自然不存在線程安全問題。但是這樣會有一個致命的影響:導致服務器壓力非常大,并且嚴重影響程序執行性能。由于在方法中需要頻繁地開啟和關閉數據庫連接,這樣不盡嚴重影響程序執行效率,還可能導致服務器壓力巨大。
那么這種情況下使用ThreadLocal是再適合不過的了,因為ThreadLocal在每個線程中對該變量會創建一個副本,即每個線程內部都會有一個該變量,且在線程內部任何地方都可以使用,線程之間互不影響,這樣一來就不存在線程安全問題,也不會嚴重影響程序執行性能。
但是要注意,雖然ThreadLocal能夠解決上面說的問題,但是由于在每個線程中都創建了副本,所以要考慮它對資源的消耗,比如內存的占用會比不使用ThreadLocal要大。
三、深入解析ThreadLocal類
在上面談到了對ThreadLocal的一些理解,那我們下面來看一下具體ThreadLocal是如何實現的。
先了解一下ThreadLocal類提供的幾個方法:
get()方法是用來獲取ThreadLocal在當前線程中保存的變量副本; set()用來設置當前線程中變量的副本;
remove()用來移除當前線程中變量的副本;
initialValue()是一個protected方法,一般是用來在使用時進行重寫的,它是一個延遲加載方法;
ThreadLocal是如何為每個線程創建變量的副本的:
1、在每個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,Key為當前ThreadLocal變量,value為變量副本(即T類型的變量)。
2、初始時,在Thread里面,threadLocals為空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,并且以當前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals。
3、在當前線程里面,如果要使用副本變量,就可以通過get方法在threadLocals里面查找。
下面通過一個例子來證明通過ThreadLocal能達到在每個線程中創建變量副本的效果:
這段代碼的輸出結果為:
從這段代碼的輸出結果可以看出,在main線程中和thread1線程中,longLocal保存的副本值和stringLocal保存的副本值都不一樣。最后一次在main線程再次打印副本值是為了證明在main線程中和thread1線程中的副本值確實是不同的。
總結一下:
1)實際的通過ThreadLocal創建的副本是存儲在每個線程自己的threadLocals中的;
2)為何threadLocals的類型ThreadLocalMap的鍵值為ThreadLocal對象,因為每個線程中可有多個threadLocal變量,就像上面代碼中的longLocal和stringLocal;
四.ThreadLocal的應用場景
最常見的ThreadLocal使用場景為:用來解決數據庫連接、Session管理,多線程單例模式訪問;
訂單處理包含一系列操作:減少庫存量、增加一條流水臺賬、修改總賬,這幾個操作要在同一個事務中完成,通常也即同一個線程中進行處理,如果累加公司應收款的操作失敗了,則應該把前面的操作回滾,否則,提交所有操作,這要求這些操作使用相同的數據庫連接對象,而這些操作的代碼分別位于不同的模塊類中。
銀行轉賬包含一系列操作:
把轉出帳戶的余額減少,把轉入帳戶的余額增加,這兩個操作要在同一個事務中完成,它們必須使用相同的數據庫連接對象,轉入和轉出操作的代碼分別是兩個不同的帳戶對象的方法。
我們先看一個簡單的例子:
public class ThreadLocalTest {//創建一個Integer型的線程本地變量public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>() {@Overrideprotected Integer initialValue() {return 0;}};public static void main(String[] args) throws InterruptedException {Thread[] threads = new Thread[5];for (int j = 0; j < 5; j++) { threads[j] = new Thread(new Runnable() {@Overridepublic void run() {//獲取當前線程的本地變量,然后累加5次int num = local.get();for (int i = 0; i < 5; i++) {num++;}//重新設置累加后的本地變量local.set(num);System.out.println(Thread.currentThread().getName() + " : "+ local.get());}}, "Thread-" + j);}for (Thread thread : threads) {thread.start();}} }運行后結果:
Thread-0 : 5Thread-4 : 5Thread-2 : 5Thread-1 : 5Thread-3 : 5我們看到,每個線程累加后的結果都是5,各個線程處理自己的本地變量值,線程之間互不影響。
如:數據庫連接:
Session連接:
五、ThreadLocal使用的一般步驟
1、在多線程的類(如ThreadDemo類)中,創建一個ThreadLocal對象threadXxx,用來保存線程間需要隔離處理的對象xxx。
2、在ThreadDemo類中,創建一個獲取要隔離訪問的數據的方法getXxx(),在方法中判斷,若ThreadLocal對象為null時候,應該new()一個隔離訪問類型的對象,并強制轉換為要應用的類型。
3、在ThreadDemo類的run()方法中,通過getXxx()方法獲取要操作的數據,這樣可以保證每個線程對應一個數據對象,在任何時刻都操作的是這個對象。
相關鏈接請參考:http://my.oschina.net/xianggao/blog/392440?fromerr=nKHw4fBT
總結
以上是生活随笔為你收集整理的详解线程本地变量ThreadLocal的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SpringMVC一个Controlle
- 下一篇: 浅析Java各种变量线程安全问题