并發(fā)編程和ThreadLocal
點(diǎn)擊上方 Java學(xué)習(xí)之道,選擇 設(shè)為星標(biāo)
來源: cnblogs.com/xiaoxi/p/7755253.htmll
作者: 平凡希
Part1對(duì)ThreadLocal的理解
ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲(chǔ),其實(shí)意思差不多??赡芎芏嗯笥讯贾繲hreadLocal為變量在每個(gè)線程中都創(chuàng)建了一個(gè)副本,那么每個(gè)線程可以訪問自己內(nèi)部的副本變量。 這句話從字面上看起來很容易理解,但是真正理解并不是那么容易。
ThreadLocal的官方API解釋為:
"該類提供了線程局部 (thread-local) 變量。這些變量不同于它們的普通對(duì)應(yīng)物,因?yàn)樵L問某個(gè)變量(通過其 get 或 set 方法)的每個(gè)線程都有自己的局部變量,它獨(dú)立于變量的初始化副本。ThreadLocal 實(shí)例通常是類中的 private static 字段,它們希望將狀態(tài)與某一個(gè)線程(例如,用戶 ID 或事務(wù) ID)相關(guān)聯(lián)。"
大概的意思有兩點(diǎn):
1、ThreadLocal提供了一種訪問某個(gè)變量的特殊方式:訪問到的變量屬于當(dāng)前線程,即保證每個(gè)線程的變量不一樣,而同一個(gè)線程在任何地方拿到的變量都是一致的,這就是所謂的線程隔離。
如果要使用ThreadLocal,通常定義為private static類型,在我看來最好是定義為private static final類型。
很多博客都這樣說:ThreadLocal為解決多線程程序的并發(fā)問題提供了一種新的思路;ThreadLocal的目的是為了解決多線程訪問資源時(shí)的共享問題。如果你也這樣認(rèn)為的,那現(xiàn)在給你10秒鐘,清空之前對(duì)ThreadLocal的錯(cuò)誤的認(rèn)知!
ThreadLocal可以總結(jié)為一句話:ThreadLocal的作用是提供線程內(nèi)的局部變量,這種變量在線程的生命周期內(nèi)起作用,減少同一個(gè)線程內(nèi)多個(gè)函數(shù)或者組件之間一些公共變量的傳遞的復(fù)雜度。
我們還是先來看一個(gè)例子:
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();
}
}
假設(shè)有這樣一個(gè)數(shù)據(jù)庫鏈接管理類,這段代碼在單線程中使用是沒有任何問題的,但是如果在多線程中使用呢?很顯然,在多線程中使用會(huì)存在線程安全問題:第一,這里面的2個(gè)方法都沒有進(jìn)行同步,很可能在openConnection方法中會(huì)多次創(chuàng)建connect;第二,由于connect是共享變量,那么必然在調(diào)用connect的地方需要使用到同步來保障線程安全,因?yàn)楹芸赡芤粋€(gè)線程在使用connect進(jìn)行數(shù)據(jù)庫操作,而另外一個(gè)線程調(diào)用closeConnection關(guān)閉鏈接。
所以出于線程安全的考慮,必須將這段代碼的兩個(gè)方法進(jìn)行同步處理,并且在調(diào)用connect的地方需要進(jìn)行同步處理。這樣將會(huì)大大影響程序執(zhí)行效率,因?yàn)橐粋€(gè)線程在使用connect進(jìn)行數(shù)據(jù)庫操作的時(shí)候,其他線程只有等待。
那么大家來仔細(xì)分析一下這個(gè)問題,這地方到底需不需要將connect變量進(jìn)行共享?事實(shí)上,是不需要的。假如每個(gè)線程中都有一個(gè)connect變量,各個(gè)線程之間對(duì)connect變量的訪問實(shí)際上是沒有依賴關(guān)系的,即一個(gè)線程不需要關(guān)心其他線程是否對(duì)這個(gè)connect進(jìn)行了修改的。
到這里,可能會(huì)有朋友想到,既然不需要在線程之間共享這個(gè)變量,可以直接這樣處理,在每個(gè)需要使用數(shù)據(jù)庫連接的方法中具體使用時(shí)才創(chuàng)建數(shù)據(jù)庫鏈接,然后在方法調(diào)用完畢再釋放這個(gè)連接。比如下面這樣:
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進(jìn)行操作
connectionManager.closeConnection();
}
}
這樣處理確實(shí)也沒有任何問題,由于每次都是在方法內(nèi)部創(chuàng)建的連接,那么線程之間自然不存在線程安全問題。但是這樣會(huì)有一個(gè)致命的影響:導(dǎo)致服務(wù)器壓力非常大,并且嚴(yán)重影響程序執(zhí)行性能。由于在方法中需要頻繁地開啟和關(guān)閉數(shù)據(jù)庫連接,這樣不盡嚴(yán)重影響程序執(zhí)行效率,還可能導(dǎo)致服務(wù)器壓力巨大。
那么這種情況下使用ThreadLocal是再適合不過的了,因?yàn)門hreadLocal在每個(gè)線程中對(duì)該變量會(huì)創(chuàng)建一個(gè)副本,即每個(gè)線程內(nèi)部都會(huì)有一個(gè)該變量,且在線程內(nèi)部任何地方都可以使用,線程之間互不影響,這樣一來就不存在線程安全問題,也不會(huì)嚴(yán)重影響程序執(zhí)行性能。
但是要注意,雖然ThreadLocal能夠解決上面說的問題,但是由于在每個(gè)線程中都創(chuàng)建了副本,所以要考慮它對(duì)資源的消耗,比如內(nèi)存的占用會(huì)比不使用ThreadLocal要大。
Part2深入解析ThreadLocal類
在上面談到了對(duì)ThreadLocal的一些理解,那我們下面來看一下具體ThreadLocal是如何實(shí)現(xiàn)的。先了解一下ThreadLocal類提供的幾個(gè)方法:
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
get()方法是用來獲取ThreadLocal在當(dāng)前線程中保存的變量副本,set()用來設(shè)置當(dāng)前線程中變量的副本,remove()用來移除當(dāng)前線程中變量的副本,initialValue()是一個(gè)protected方法,用來返回此線程局部變量的當(dāng)前線程的初始值,一般是在使用時(shí)進(jìn)行重寫的,它是一個(gè)延遲加載方法, 下面會(huì)詳細(xì)說明。
首先我們來看一下ThreadLocal類是如何為每個(gè)線程創(chuàng)建一個(gè)變量的副本的。先看下get方法的實(shí)現(xiàn):
public T get() {
//1.首先獲取當(dāng)前線程
Thread t = Thread.currentThread();
//2.獲取線程的map對(duì)象
ThreadLocalMap map = getMap(t);
//3.如果map不為空,以threadlocal實(shí)例為key獲取到對(duì)應(yīng)Entry,然后從Entry中取出對(duì)象即可。
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
//如果map為空,也就是第一次沒有調(diào)用set直接get(或者調(diào)用過set,又調(diào)用了remove)時(shí),為其設(shè)定初始值
return setInitialValue();
}
第3行是取得當(dāng)前線程,然后通過getMap(t)方法獲取到一個(gè)map,map的類型為ThreadLocalMap。然后接著下面獲取到<key,value>鍵值對(duì),注意這里獲取鍵值對(duì)傳進(jìn)去的是 this,而不是當(dāng)前線程t。如果獲取成功,則返回value值。如果map為空,則調(diào)用setInitialValue方法返回value。
下面我們對(duì)上面的每一句來仔細(xì)分析。首先看一下getMap方法中做了什么:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可能大家沒有想到的是,在getMap中,是調(diào)用當(dāng)期線程t,返回當(dāng)前線程t中的一個(gè)成員變量threadLocals。那么我們繼續(xù)取Thread類中取看一下成員變量threadLocals是什么:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
實(shí)際上就是一個(gè)ThreadLocalMap,這個(gè)類型是ThreadLocal類的一個(gè)內(nèi)部類,我們繼續(xù)取看ThreadLocalMap的實(shí)現(xiàn):
可以看到ThreadLocalMap的Entry繼承了WeakReference,并且使用ThreadLocal作為鍵值。
總結(jié)
get()方法的第3和第5行很明顯是獲取屬于當(dāng)前線程的ThreadLocalMap,如果這個(gè)map不為空,我們就以當(dāng)前的ThreadLocal為鍵,去獲取相應(yīng)的Entry,Entry是ThreadLocalMap的靜態(tài)內(nèi)部類,它繼承于弱引用,所以在get()方法里面如第10行一樣調(diào)用e.value方法就可以獲取實(shí)際的資源副本值。但是如果有一個(gè)為空,說明屬于該線程的資源副本還不存在,則需要去創(chuàng)建資源副本,從代碼中可以看到是調(diào)用setInitialValue()方法,其定義如下:
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
第8行調(diào)用initialValue()方法初始化一個(gè)值。接下來是判斷線程的ThreadLocalMap是否為空,不為空就直接設(shè)置值(鍵為this,值為value),為空則創(chuàng)建一個(gè)Map,調(diào)用方法為createMap(),其定義如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
簡(jiǎn)單明了,而ThreadLocalMap的這個(gè)構(gòu)造方法的實(shí)現(xiàn)如下:
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
實(shí)例化table數(shù)組用于存儲(chǔ)鍵值對(duì),然后通過映射將鍵值對(duì)存儲(chǔ)進(jìn)入相應(yīng)的位置。
下面再來看set方法。
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
// 獲取當(dāng)前線程對(duì)象
Thread t = Thread.currentThread();
// 獲取當(dāng)前線程本地變量Map
ThreadLocalMap map = getMap(t);
// map不為空
if (map != null)
// 存值
map.set(this, value);
else
// 創(chuàng)建一個(gè)當(dāng)前線程本地變量Map
createMap(t, value);
}
在這個(gè)方法內(nèi)部我們看到,首先通過getMap(Thread t)方法獲取一個(gè)和當(dāng)前線程相關(guān)的ThreadLocalMap,然后將變量的值設(shè)置到這個(gè)ThreadLocalMap對(duì)象中,當(dāng)然如果獲取到的ThreadLocalMap對(duì)象為空,就通過createMap方法創(chuàng)建。
至此,可能大部分朋友已經(jīng)明白了ThreadLocal是如何為每個(gè)線程創(chuàng)建變量的副本的:
首先,在每個(gè)線程Thread內(nèi)部有一個(gè)ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個(gè)threadLocals就是用來存儲(chǔ)實(shí)際的變量副本的,鍵值為當(dāng)前ThreadLocal變量,value為變量副本(即T類型的變量)。
初始時(shí),在Thread里面,threadLocals為空,當(dāng)通過ThreadLocal變量調(diào)用get()方法或者set()方法,就會(huì)對(duì)Thread類中的threadLocals進(jìn)行初始化,并且以當(dāng)前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals。
然后在當(dāng)前線程里面,如果要使用副本變量,就可以通過get方法在threadLocals里面查找。
Part3示例
示例1:
下面通過一個(gè)例子來證明通過ThreadLocal能達(dá)到在每個(gè)線程中創(chuàng)建變量副本的效果:
package com.demo.test;
public class TestThreadLocal {
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 TestThreadLocal test = new TestThreadLocal();
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());
}
}
這段代碼的輸出結(jié)果為:
1
main
8
Thread-0
1
main 從這段代碼的輸出結(jié)果可以看出,在main線程中和thread1線程中,longLocal保存的副本值和stringLocal保存的副本值都不一樣。最后一次在main線程再次打印副本值是為了證明在main線程中和thread1線程中的副本值確實(shí)是不同的。
總結(jié)一下:
1)實(shí)際的通過ThreadLocal創(chuàng)建的副本是存儲(chǔ)在每個(gè)線程自己的threadLocals中的;
2)為何threadLocals的類型ThreadLocalMap的鍵值為ThreadLocal對(duì)象,因?yàn)槊總€(gè)線程中可有多個(gè)threadLocal變量,就像上面代碼中的longLocal和stringLocal;
3)在進(jìn)行g(shù)et之前,必須先set,否則會(huì)報(bào)空指針異常。如果想在get之前不需要調(diào)用set就能正常訪問的話,必須重寫initialValue()方法。
因?yàn)樵谏厦娴拇a分析過程中,我們發(fā)現(xiàn)如果沒有先set的話,即在map中查找不到對(duì)應(yīng)的存儲(chǔ),則會(huì)通過調(diào)用setInitialValue方法返回i,而在setInitialValue方法中,有一個(gè)語句是T value = initialValue(), 而默認(rèn)情況下,initialValue方法返回的是null。
注意 :默認(rèn)情況下 initValue(), 返回 null 。線程在沒有調(diào)用 set 之前,第一次調(diào)用 get 的時(shí)候, get方法會(huì)默認(rèn)去調(diào)用 initValue 這個(gè)方法。所以如果沒有覆寫這個(gè)方法,可能導(dǎo)致 get 返回的是 null 。當(dāng)然如果調(diào)用過 set 就不會(huì)有這種情況了。但是往往在多線程情況下我們不能保證每個(gè)線程的在調(diào)用 get 之前都調(diào)用了set ,所以最好對(duì) initValue 進(jìn)行覆寫,以免導(dǎo)致空指針異常。
看下面這個(gè)例子:
package com.demo.test;
public class TestThreadLocal {
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 TestThreadLocal test = new TestThreadLocal();
//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線程中,沒有先set,直接get的話,運(yùn)行時(shí)會(huì)報(bào)空指針異常。
Exception in thread "main" java.lang.NullPointerException
at com.demo.test.TestThreadLocal.getLong(TestThreadLocal.java:14)
at com.demo.test.TestThreadLocal.main(TestThreadLocal.java:25)
但是如果改成下面這段代碼,即重寫了initialValue方法:
package com.demo.test;
public class TestThreadLocal {
ThreadLocal<Long> longLocal = new ThreadLocal<Long>(){
protected Long initialValue() {
return Thread.currentThread().getId();
};
};
ThreadLocal<String> stringLocal = new ThreadLocal<String>(){
protected String initialValue() {
return Thread.currentThread().getName();
};
};
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 TestThreadLocal test = new TestThreadLocal();
//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());
}
}
就可以直接不用先set而直接調(diào)用get了。
示例2:
package com.demo.test;
public class TestNum {
// ①通過匿名內(nèi)部類覆蓋ThreadLocal的initialValue()方法,指定初始值
private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
public Integer initialValue() {
return 0;
}
};
// ②獲取下一個(gè)序列值
public int getNextNum() {
seqNum.set(seqNum.get() + 1);
return seqNum.get();
}
public static void main(String[] args) {
TestNum sn = new TestNum();
// ③ 3個(gè)線程共享sn,各自產(chǎn)生序列號(hào)
TestClient t1 = new TestClient(sn);
TestClient t2 = new TestClient(sn);
TestClient t3 = new TestClient(sn);
t1.start();
t2.start();
t3.start();
}
private static class TestClient extends Thread {
private TestNum sn;
public TestClient(TestNum sn) {
this.sn = sn;
}
public void run() {
for (int i = 0; i < 3; i++) {
// ④每個(gè)線程打出3個(gè)序列值
System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["
+ sn.getNextNum() + "]");
}
}
}
}
通常我們通過匿名內(nèi)部類的方式定義ThreadLocal的子類,提供初始的變量值,如例子中①處所示。TestClient線程產(chǎn)生一組序列號(hào),在③處,我們生成3個(gè)TestClient,它們共享同一個(gè)TestNum實(shí)例。運(yùn)行以上代碼,在控制臺(tái)上輸出以下的結(jié)果:
thread[Thread-0] --> sn[1]
thread[Thread-1] --> sn[1]
thread[Thread-2] --> sn[1]
thread[Thread-1] --> sn[2]
thread[Thread-0] --> sn[2]
thread[Thread-1] --> sn[3]
thread[Thread-2] --> sn[2]
thread[Thread-0] --> sn[3]
thread[Thread-2] --> sn[3]
考察輸出的結(jié)果信息,我們發(fā)現(xiàn)每個(gè)線程所產(chǎn)生的序號(hào)雖然都共享同一個(gè)TestNum實(shí)例,但它們并沒有發(fā)生相互干擾的情況,而是各自產(chǎn)生獨(dú)立的序列號(hào),這是因?yàn)槲覀兺ㄟ^ThreadLocal為每一個(gè)線程提供了單獨(dú)的副本。
Part4ThreadLocal的應(yīng)用場(chǎng)景
最常見的ThreadLocal使用場(chǎng)景為 用來解決 數(shù)據(jù)庫連接、Session管理等。
private static ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
ThreadLocal對(duì)象通常用于防止對(duì)可變的單實(shí)例變量或全局變量進(jìn)行共享。
當(dāng)一個(gè)類中使用了static成員變量的時(shí)候,一定要多問問自己,這個(gè)static成員變量需要考慮線程安全嗎?也就是說,多個(gè)線程需要獨(dú)享自己的static成員變量嗎?如果需要考慮,不妨使用ThreadLocal。
ThreadLocal的主要應(yīng)用場(chǎng)景為多線程多實(shí)例(每個(gè)線程對(duì)應(yīng)一個(gè)實(shí)例)的對(duì)象的訪問,并且這個(gè)對(duì)象很多地方都要用到。例如:同一個(gè)網(wǎng)站登錄用戶,每個(gè)用戶服務(wù)器會(huì)為其開一個(gè)線程,每個(gè)線程中創(chuàng)建一個(gè)ThreadLocal,里面存用戶基本信息等,在很多頁面跳轉(zhuǎn)時(shí),會(huì)顯示用戶信息或者得到用戶的一些信息等頻繁操作,這樣多線程之間并沒有聯(lián)系而且當(dāng)前線程也可以及時(shí)獲取想要的數(shù)據(jù)。
ThreadLocal通常用來共享數(shù)據(jù),當(dāng)你想在多個(gè)方法中使用某個(gè)變量,這個(gè)變量是當(dāng)前線程的狀態(tài),其它線程不依賴這個(gè)變量,你第一時(shí)間想到的就是把變量定義在方法內(nèi)部,然后再方法之間傳遞參數(shù)來使用,這個(gè)方法能解決問題,但是有個(gè)煩人的地方就是,每個(gè)方法都需要聲明形參,多處聲明,多處調(diào)用。影響代碼的美觀和維護(hù)。有沒有一種方法能將變量像private static形式來訪問呢?這樣在類的任何一處地方就都能使用。這個(gè)時(shí)候ThreadLocal大顯身手了。
Part5總結(jié)
1實(shí)現(xiàn)思想
ThreadLocal 的實(shí)現(xiàn)思想,我們?cè)谇懊嬉呀?jīng)說了,每個(gè)線程維護(hù)一個(gè) ThreadLocalMap 的映射表,映射表的 key 是 ThreadLocal 實(shí)例本身,value 是要存儲(chǔ)的副本變量。ThreadLocal 實(shí)例本身并不存儲(chǔ)值,它只是提供一個(gè)在當(dāng)前線程中找到副本值的 key。如下圖所示:
2線程隔離的秘密
線程隔離的秘密,就在于ThreadLocalMap這個(gè)類。ThreadLocalMap是ThreadLocal類的一個(gè)靜態(tài)內(nèi)部類,它實(shí)現(xiàn)了鍵值對(duì)的設(shè)置和獲取(對(duì)比Map對(duì)象來理解),每個(gè)線程中都有一個(gè)獨(dú)立的ThreadLocalMap副本,它所存儲(chǔ)的值,只能被當(dāng)前線程讀取和修改。ThreadLocal類通過操作每一個(gè)線程特有的ThreadLocalMap副本,從而實(shí)現(xiàn)了變量訪問在不同線程中的隔離。因?yàn)槊總€(gè)線程的變量都是自己特有的,完全不會(huì)有并發(fā)錯(cuò)誤。還有一點(diǎn)就是,ThreadLocalMap存儲(chǔ)的鍵值對(duì)中的鍵是this對(duì)象指向的ThreadLocal對(duì)象,而值就是你所設(shè)置的對(duì)象了。
3ThreadLocalMap
ThreadLocalMap并不是為了解決線程安全問題,而是提供了一種將實(shí)例綁定到當(dāng)前線程的機(jī)制,類似于隔離的效果,實(shí)際上自己在方法中new出來變量也能達(dá)到類似的效果。ThreadLocalMap跟線程安全基本不搭邊,綁定上去的實(shí)例也不是多線程公用的,而是每個(gè)線程new一份,這個(gè)實(shí)例肯定不是共用的,如果共用了,那就會(huì)引發(fā)線程安全問題。ThreadLocalMap最大的用處就是用來把實(shí)例變量共享成全局變量,在程序的任何方法中都可以訪問到該實(shí)例變量而已。網(wǎng)上很多人說ThreadLocalMap是解決了線程安全問題,其實(shí)是望文生義,兩者不是同類問題。
4設(shè)計(jì)的初衷
ThreadLocal設(shè)計(jì)的初衷是為了解決多線程編程中的資源共享問題。提起這個(gè),大家一般會(huì)想到synchronized,synchronized采取的是“以時(shí)間換空間”的策略,本質(zhì)上是對(duì)關(guān)鍵資源上鎖,讓大家排隊(duì)操作。而ThreadLocal采取的是“以空間換時(shí)間”的思路,為每個(gè)使用該變量的線程提供獨(dú)立的變量副本,在本線程內(nèi)部,它相當(dāng)于一個(gè)“全局變量”,可以保證本線程任何時(shí)間操縱的都是同一個(gè)對(duì)象。
5最重要的概念
ThreadLocal類最重要的一個(gè)概念是,其原理是通過一個(gè)ThreadLocal的靜態(tài)內(nèi)部類ThreadLocalMap實(shí)現(xiàn),但是實(shí)際中,ThreadLocal不保存ThreadLocalMap,而是有每個(gè)Thread內(nèi)部維護(hù)ThreadLocal.ThreadLocalMap threadLocals一份數(shù)據(jù)結(jié)構(gòu)。
這里畫張圖更容易理解,假如我們有如下的代碼:
class ThreadLocalDemo
{
ThreadLocal<Integer> localA = new ThreadLocal<Integer>();
ThreadLocal<Integer> localB = new ThreadLocal<Integer>();
}
在多線程環(huán)境下,數(shù)據(jù)結(jié)構(gòu)應(yīng)該是如下圖所示:
6一般步驟
(1)在多線程的類(如ThreadDemo類)中,創(chuàng)建一個(gè)ThreadLocal對(duì)象threadXxx,用來保存線程間需要隔離處理的對(duì)象xxx。
(2)在ThreadDemo類中,創(chuàng)建一個(gè)獲取要隔離訪問的數(shù)據(jù)的方法getXxx(),在方法中判斷,若ThreadLocal對(duì)象為null時(shí)候,應(yīng)該new()一個(gè)隔離訪問類型的對(duì)象,并強(qiáng)制轉(zhuǎn)換為要應(yīng)用的類型。
(3)在ThreadDemo類的run()方法中,通過getXxx()方法獲取要操作的數(shù)據(jù),這樣可以保證每個(gè)線程對(duì)應(yīng)一個(gè)數(shù)據(jù)對(duì)象,在任何時(shí)刻都操作的是這個(gè)對(duì)象。
7ThreadLocal 與 synchronized 的對(duì)比
(1)ThreadLocal和synchonized都用于解決多線程并發(fā)訪問。但是ThreadLocal與synchronized有本質(zhì)的區(qū)別。synchronized是利用鎖的機(jī)制,使變量或代碼塊在某一時(shí)該只能被一個(gè)線程訪問。而ThreadLocal為每一個(gè)線程都提供了變量的副本,使得每個(gè)線程在某一時(shí)間訪問到的并不是同一個(gè)對(duì)象,這樣就隔離了多個(gè)線程對(duì)數(shù)據(jù)的數(shù)據(jù)共享。而synchronized卻正好相反,它用于在多個(gè)線程間通信時(shí)能夠獲得數(shù)據(jù)共享。
(2)synchronized用于線程間的數(shù)據(jù)共享,而ThreadLocal則用于線程間的數(shù)據(jù)隔離。
8一句話理解ThreadLocal
向ThreadLocal里面存東西就是向它里面的Map存東西的,然后ThreadLocal把這個(gè)Map掛到當(dāng)前的線程底下,這樣Map就只屬于這個(gè)線程了。
-
| 更多精彩文章 -
▽加我微信,交個(gè)朋友 長(zhǎng)按/掃碼添加↑↑↑



