一、本地線程變量使用場景
並發應用的一個關鍵地方就是共享數據。如果你創建一個類對象,實現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 為每個運行的線程,綁定了私有的本地實例存取空間,從而為多線程環境常出現的並發訪問問題提供了一種隔離機制。
我們還是先來看一個例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class
ConnectionManager {
private
static
Connection connect =
null
;
public
static
Connection openConnection() {
if
(connect ==
null
){
connect = DriverManager.getConnection();
}
return
connect;
}
public
static
void
closeConnection() {
if
(connect!=
null
)
connect.close();
}
}
|
假設有這樣一個數據庫鏈接管理類,這段代碼在單線程中使用是沒有任何問題的,但是如果在多線程中使用呢?很顯然,在多線程中使用會存在線程安全問題:第一,這里面的2個方法都沒有進行同步,很可能在openConnection方法中會多次創建connect;第二,由於connect是共享變量,那么必然在調用connect的地方需要使用到同步來保障線程安全,因為很可能一個線程在使用connect進行數據庫操作,而另外一個線程調用closeConnection關閉鏈接。
所以出於線程安全的考慮,必須將這段代碼的兩個方法進行同步處理,並且在調用connect的地方需要進行同步處理。
這樣將會大大影響程序執行效率,因為一個線程在使用connect進行數據庫操作的時候,其他線程只有等待。
那么大家來仔細分析一下這個問題,這地方到底需不需要將connect變量進行共享?事實上,是不需要的。假如每個線程中都有一個connect變量,各個線程之間對connect變量的訪問實際上是沒有依賴關系的,即一個線程不需要關心其他線程是否對這個connect進行了修改的。
到這里,可能會有朋友想到,既然不需要在線程之間共享這個變量,可以直接這樣處理,在每個需要使用數據庫連接的方法中具體使用時才創建數據庫鏈接,然后在方法調用完畢再釋放這個連接。比如下面這樣:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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類提供的幾個方法:
1
2
3
4
|
public
T get() { }
public
void
set(T value) { }
public
void
remove() { }
protected
T initialValue() { }
|
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能達到在每個線程中創建變量副本的效果:
public
class
Test {
ThreadLocal<Long> longLocal =
new
ThreadLocal<Long>();
ThreadLocal<String> stringLocal =
new
ThreadLocal<String>();
public
void
set() {
longLocal.set(Thread.currentThread().getId());
stringLocal.set(Thread.currentThread().getName());
}
public
long
getLong() {
return
longLocal.get();
}
public
String getString() {
return
stringLocal.get();
}
public
static
void
main(String[] args)
throws
InterruptedException {
final
Test test =
new
Test();
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
Thread thread1 =
new
Thread(){
public
void
run() {
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
};
};
thread1.start();
thread1.join();
System.out.println(test.getLong());
System.out.println(test.getString());
}
}
|
這段代碼的輸出結果為:
從這段代碼的輸出結果可以看出,在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>() {
@Override
protected
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() {
@Override
public
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 : 5
Thread-4 : 5
Thread-2 : 5
Thread-1 : 5
Thread-3 : 5
我們看到,每個線程累加后的結果都是5,各個線程處理自己的本地變量值,線程之間互不影響。
如:數據庫連接:
Session連接: