前期回顧
JAVA線程安全及性能的優(yōu)化筆記(四)——什么是線程安全狂秘?
一浸踩、ThreadLocal原理
1. 線程程序介紹
早在JDK 1.2的版本中就提供java.lang.ThreadLocal版仔,ThreadLocal為解決多線程程序的并發(fā)問(wèn)題提供了一種新的思路包券。使用這個(gè)工具類(lèi)可以很簡(jiǎn)潔地編寫(xiě)出優(yōu)美的多線程程序首妖。
2. Threadlocal變量
ThreadLocal很容易讓人望文生義娜庇,想當(dāng)然地認(rèn)為是一個(gè)“本地線程”塔次。其實(shí),ThreadLocal并不是一個(gè)Thread名秀,而是Thread的局部變量俺叭,也許把它命名為T(mén)hreadLocalVariable更容易讓人理解一些。
當(dāng)使用ThreadLocal維護(hù)變量時(shí)泰偿,ThreadLocal為每個(gè)使用該變量的線程提供獨(dú)立的變量副本熄守,所以每一個(gè)線程都可以獨(dú)立地改變自己的副本,而不會(huì)影響其它線程所對(duì)應(yīng)的副本耗跛。
從線程的角度看裕照,目標(biāo)變量就像是線程的本地變量,這也是類(lèi)名中“Local”所要表達(dá)的意思调塌。
線程局部變量并不是Java的新發(fā)明晋南,很多語(yǔ)言(如IBM IBM XL FORTRAN)在語(yǔ)法層面就提供線程局部變量。在Java中沒(méi)有提供在語(yǔ)言級(jí)支持羔砾,而是變相地通過(guò)ThreadLocal的類(lèi)提供支持负间。
所以偶妖,在Java中編寫(xiě)線程局部變量的代碼相對(duì)來(lái)說(shuō)要笨拙一些,因此造成線程局部變量沒(méi)有在Java開(kāi)發(fā)者中得到很好的普及政溃。
ThreadLocal的接口方法
ThreadLocal類(lèi)接口很簡(jiǎn)單趾访,只有4個(gè)方法,我們先來(lái)了解一下:
void set(Object value)
設(shè)置當(dāng)前線程的線程局部變量的值董虱。
public Object get()
該方法返回當(dāng)前線程所對(duì)應(yīng)的線程局部變量扼鞋。
public void remove()
將當(dāng)前線程局部變量的值刪除,目的是為了減少內(nèi)存的占用愤诱,該方法是JDK 5.0新增的方法云头。需要指出的是,當(dāng)線程結(jié)束后淫半,對(duì)應(yīng)該線程的局部變量將自動(dòng)被垃圾回收溃槐,所以顯式調(diào)用該方法清除線程的局部變量并不是必須的操作,但它可以加快內(nèi)存回收的速度科吭。
protected Object initialValue()
返回該線程局部變量的初始值竿痰,該方法是一個(gè)protected的方法,顯然是為了讓子類(lèi)覆蓋而設(shè)計(jì)的砌溺。這個(gè)方法是一個(gè)延遲調(diào)用方法,在線程第1次調(diào)用get()或set(Object)時(shí)才執(zhí)行变隔,并且僅執(zhí)行1次规伐。ThreadLocal中的缺省實(shí)現(xiàn)直接返回一個(gè)null。
值得一提的是匣缘,在JDK5.0中猖闪,ThreadLocal已經(jīng)支持泛型,該類(lèi)的類(lèi)名已經(jīng)變?yōu)門(mén)hreadLocal<T>肌厨。API方法也相應(yīng)進(jìn)行了調(diào)整培慌,新版本的API方法分別是void set(T value)、T get()以及T initialValue()柑爸。
ThreadLocal是如何做到為每一個(gè)線程維護(hù)變量的副本的呢吵护?其實(shí)實(shí)現(xiàn)的思路很簡(jiǎn)單:在ThreadLocal類(lèi)中定義了一個(gè)ThreadLocalMap,每一個(gè)Tread中都有一個(gè)該類(lèi)型的變量——threadLocals——用于存儲(chǔ)每一個(gè)線程的變量副本表鳍,Map中元素的鍵為線程對(duì)象馅而,而值對(duì)應(yīng)線程的變量副本。
二譬圣、基本概念
為每一個(gè)使用該變量的線程都提供一個(gè)變量值的副本瓮恭,使每一個(gè)線程都可以獨(dú)立地改變自己的副本,而不會(huì)和其它線程的副本沖突厘熟。從線程的角度看屯蹦,就好像每一個(gè)線程都完全擁有該變量维哈。使用場(chǎng)景To keep state with a thread (user-id, transaction-id, logging-id) To cache objects which you need frequentlyThreadLocal類(lèi)
它主要由四個(gè)方法組成initialValue(),get()登澜,set(T)阔挠,remove(),其中值得注意的是initialValue()帖渠,該方法是一個(gè)protected的方法谒亦,顯然是為了子類(lèi)重寫(xiě)而特意實(shí)現(xiàn)的。該方法返回當(dāng)前線程在該線程局部變量的初始值空郊,這個(gè)方法是一個(gè)延遲調(diào)用方法份招,在一個(gè)線程第1次調(diào)用get()或者set(Object)時(shí)才執(zhí)行,并且僅執(zhí)行1次狞甚。ThreadLocal中的確實(shí)實(shí)現(xiàn)直接返回一個(gè)null:
舉例
ThreadLocal的原理
ThreadLocal是如何做到為每一個(gè)線程維護(hù)變量的副本的呢锁摔?其實(shí)實(shí)現(xiàn)的思路很簡(jiǎn)單,在ThreadLocal類(lèi)中有一個(gè)Map哼审,用于存儲(chǔ)每一個(gè)線程的變量的副本谐腰。比如下面的示例實(shí)現(xiàn):
public class ThreadLocal
{
private Map values = Collections.synchronizedMap(new HashMap());
public Object get()
{
Thread curThread = Thread.currentThread();
Object o = values.get(curThread);
if (o == null && !values.containsKey(curThread))
{
o = initialValue();
values.put(curThread, o);
}
return o;
}
public void set(Object newValue)
{
values.put(Thread.currentThread(), newValue);
}
public Object initialValue()
{
return null;
}
}
使用方法
ThreadLocal 的使用
使用方法一:Hibernate的文檔時(shí)看到了關(guān)于使ThreadLocal管理多線程訪問(wèn)的部分。具體代碼如下
public static final ThreadLocal session = new ThreadLocal();
public static Session currentSession() {
Session s = (Session)session.get();
//open a new session,if this session has none
if(s == null){
s = sessionFactory.openSession();
session.set(s);
}
return s;
}
我們逐行分析
- 初始化一個(gè)ThreadLocal對(duì)象涩盾,ThreadLocal有三個(gè)成員方法 get()十气、set()、initialvalue()春霍。
- 如果不初始化initialvalue砸西,則initialvalue返回null。
- session的get根據(jù)當(dāng)前線程返回其對(duì)應(yīng)的線程內(nèi)部變量址儒,也就是我們需要的net.sf.hibernate.Session(相當(dāng)于對(duì)應(yīng)每個(gè)數(shù)據(jù)庫(kù)連接).多線程情況下共享數(shù)據(jù)庫(kù)鏈接是不安全的芹枷。
- ThreadLocal保證了每個(gè)線程都有自己的s(數(shù)據(jù)庫(kù)連接)。
- 如果是該線程初次訪問(wèn)莲趣,自然鸳慈,s(數(shù)據(jù)庫(kù)連接)會(huì)是null,接著創(chuàng)建一個(gè)Session喧伞,具體就是行6走芋。
- 創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)連接實(shí)例 s
- 保存該數(shù)據(jù)庫(kù)連接s到ThreadLocal中。
- 如果當(dāng)前線程已經(jīng)訪問(wèn)過(guò)數(shù)據(jù)庫(kù)了潘鲫,則從session中g(shù)et()就可以獲取該線程上次獲取過(guò)的連接實(shí)例绿聘。
使用方法二:當(dāng)要給線程初始化一個(gè)特殊值時(shí),需要自己實(shí)現(xiàn)ThreadLocal的子類(lèi)并重寫(xiě)該方法次舌,通常使用一個(gè)內(nèi)部匿名類(lèi)對(duì)ThreadLocal進(jìn)行子類(lèi)化熄攘,EasyDBO中創(chuàng)建jdbc連接上下文就是這樣做的:
public class JDBCContext{
private static Logger logger = Logger.getLogger(JDBCContext.class);
private DataSource ds;
protected Connection connection;
private Boolean isValid = true;
private static ThreadLocal jdbcContext;
private JDBCContext(DataSource ds){
this.ds = ds;
createConnection();
}
public static JDBCContext getJdbcContext(javax.sql.DataSource ds)
{
if(jdbcContext==null)jdbcContext=new JDBCContextThreadLocal(ds);
JDBCContext context = (JDBCContext) jdbcContext.get();
if (context == null) {
context = new JDBCContext(ds);
}
return context;
}
private static class JDBCContextThreadLocal extends ThreadLocal {
public javax.sql.DataSource ds;
public JDBCContextThreadLocal(javax.sql.DataSource ds)
{
this.ds=ds;
}
protected synchronized Object initialValue() {
return new JDBCContext(ds);
}
}
}
簡(jiǎn)單的實(shí)現(xiàn)版本
代碼清單1 SimpleThreadLocal
public class SimpleThreadLocal {
private Map valueMap = Collections.synchronizedMap(new HashMap());
public void set(Object newValue) {
valueMap.put(Thread.currentThread(), newValue);
①鍵為線程對(duì)象,值為本線程的變量副本
}
public Object get() {
Thread currentThread = Thread.currentThread();
Object o = valueMap.get(currentThread);
②返回本線程對(duì)應(yīng)的變量
if (o == null && !valueMap.containsKey(currentThread)) {
③如果在Map中不存在彼念,放到Map
中保存起來(lái)挪圾。
o = initialValue();
valueMap.put(currentThread, o);
}
return o;
}
public void remove() {
valueMap.remove(Thread.currentThread());
}
public Object initialValue() {
return null;
}
}
雖然代碼清單9 3這個(gè)ThreadLocal實(shí)現(xiàn)版本顯得比較幼稚浅萧,但它和JDK所提供的ThreadLocal類(lèi)在實(shí)現(xiàn)思路上是相近的。
舉例
下面哲思,我們通過(guò)一個(gè)具體的實(shí)例了解一下ThreadLocal的具體使用方法洼畅。
代碼清單2 SequenceNumber
package com.baobaotao.basic;
public class SequenceNumber {
①通過(guò)匿名內(nèi)部類(lèi)覆蓋ThreadLocal的initialValue()方法棚赔,指定初始值
private static ThreadLocal seqNum = new ThreadLocal(){
public Integer initialValue(){
return 0;
}
}
;
〉鄞亍②獲取下一個(gè)序列值
public int getNextNum(){
seqNum.set((Integer)seqNum.get()+1);
return (Integer)seqNum.get();
}
public static void main(String[] args)
{
SequenceNumber sn = new SequenceNumber();
③ 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 SequenceNumber sn;
public TestClient(SequenceNumber 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()+"]");
}
}
}
}
分析
通常我們通過(guò)匿名內(nèi)部類(lèi)的方式定義ThreadLocal的子類(lèi)丧肴,提供初始的變量值,如例子中①處所示胧后。TestClient線程產(chǎn)生一組序列號(hào)芋浮,在③處,我們生成3個(gè)TestClient壳快,它們共享同一個(gè)SequenceNumber實(shí)例纸巷。運(yùn)行以上代碼,在控制臺(tái)上輸出以下的結(jié)果:
thread[Thread-2] sn[1]
thread[Thread-0] sn[1]
thread[Thread-1] sn[1]
thread[Thread-2] sn[2]
thread[Thread-0] sn[2]
thread[Thread-1] sn[2]
thread[Thread-2] sn[3]
thread[Thread-0] sn[3]
thread[Thread-1] sn[3]
考察輸出的結(jié)果信息眶痰,我們發(fā)現(xiàn)每個(gè)線程所產(chǎn)生的序號(hào)雖然都共享同一個(gè)SequenceNumber實(shí)例瘤旨,但它們并沒(méi)有發(fā)生相互干擾的情況,而是各自產(chǎn)生獨(dú)立的序列號(hào)竖伯,這是因?yàn)槲覀兺ㄟ^(guò)ThreadLocal為每一個(gè)線程提供了單獨(dú)的副本存哲。
說(shuō)明
在Java的多線程編程中,為保證多個(gè)線程對(duì)共享變量的安全訪問(wèn)黔夭,通常會(huì)使用synchronized來(lái)保證同一時(shí)刻只有一個(gè)線程對(duì)共享變量進(jìn)行操作。但在有些情況下羽嫡,synchronized不能保證多線程對(duì)共享變量的正確讀寫(xiě)本姥。例如類(lèi)有一個(gè)類(lèi)變量,該類(lèi)變量會(huì)被多個(gè)類(lèi)方法讀寫(xiě)杭棵,當(dāng)多線程操作該類(lèi)的實(shí)例對(duì)象時(shí)婚惫,如果線程對(duì)類(lèi)變量有讀取、寫(xiě)入操作就會(huì)發(fā)生類(lèi)變量讀寫(xiě)錯(cuò)誤魂爪,即便是在類(lèi)方法前加上synchronized也無(wú)效先舷,因?yàn)橥粋€(gè)線程在兩次調(diào)用方法之間時(shí)鎖是被釋放的,這時(shí)其它線程可以訪問(wèn)對(duì)象的類(lèi)方法,讀取或修改類(lèi)變量。這種情況下可以將類(lèi)變量放到ThreadLocal類(lèi)型的對(duì)象中坐慰,使變量在每個(gè)線程中都有獨(dú)立拷貝泌霍,不會(huì)出現(xiàn)一個(gè)線程讀取變量時(shí)而被另一個(gè)線程修改的現(xiàn)象鹅心。
下面舉例說(shuō)明:
public class QuerySvc {
private String sql;
private static ThreadLocal sqlHolder = new ThreadLocal();
public QuerySvc() {
}
public void execute() {
System.out.println("Thread " + Thread.currentThread().getId() +" Sql is " + sql);
System.out.println("Thread " + Thread.currentThread().getId() +" Thread Local variable Sql is " + sqlHolder.get());
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
sqlHolder.set(sql);
}
}
三猎塞、多線程訪問(wèn)
為了說(shuō)明多線程訪問(wèn)對(duì)于類(lèi)變量和ThreadLocal變量的影響垢乙,QuerySvc中分別設(shè)置了類(lèi)變量sql和ThreadLocal變量渣慕,使用時(shí)先創(chuàng)建 QuerySvc的一個(gè)實(shí)例對(duì)象氮兵,然后產(chǎn)生多個(gè)線程裂逐,分別設(shè)置不同的sql實(shí)例對(duì)象,然后再調(diào)用execute方法泣栈,讀取sql的值卜高,看是否是set方法中寫(xiě)入的值。這種場(chǎng)景類(lèi)似web應(yīng)用中多個(gè)請(qǐng)求線程攜帶不同查詢(xún)條件對(duì)一個(gè)servlet實(shí)例的訪問(wèn)南片,然后servlet調(diào)用業(yè)務(wù)對(duì)象掺涛,并傳入不同查詢(xún)條件,最后要保證每個(gè)請(qǐng)求得到的結(jié)果是對(duì)應(yīng)的查詢(xún)條件的結(jié)果铃绒。
使用QuerySvc的工作線程如下:
public class Work extends Thread {
private QuerySvc querySvc;
private String sql;
public Work(QuerySvc querySvc,String sql) {
this.querySvc = querySvc;
this.sql = sql;
}
public void run() {
querySvc.setSql(sql);
querySvc.execute();
}
}
運(yùn)行線程代碼如下:
QuerySvc qs = new QuerySvc();
for (int k=0; k<10; k++)
String sql = "Select * from table where id =" + k;
new Work(qs,sql).start();
}
先創(chuàng)建一個(gè)QuerySvc實(shí)例對(duì)象鸽照,然后創(chuàng)建若干線程來(lái)調(diào)用QuerySvc的set和execute方法,每個(gè)線程傳入的sql都不一樣颠悬,從運(yùn)行結(jié)果可以看出sql變量中值不能保證在execute中值和set設(shè)置的值一樣矮燎,在 web應(yīng)用中就表現(xiàn)為一個(gè)用戶(hù)查詢(xún)的結(jié)果不是自己的查詢(xún)條件返回的結(jié)果,而是另一個(gè)用戶(hù)查詢(xún)條件的結(jié)果赔癌;而ThreadLocal中的值總是和set中設(shè)置的值一樣诞外,這樣通過(guò)使用ThreadLocal獲得了線程安全性。
如果一個(gè)對(duì)象要被多個(gè)線程訪問(wèn)灾票,而該對(duì)象存在類(lèi)變量被不同類(lèi)方法讀寫(xiě)峡谊,為獲得線程安全,可以用ThreadLocal來(lái)替代類(lèi)變量刊苍。
四既们、Thread同步機(jī)制的比較
說(shuō)明
ThreadLocal和線程同步機(jī)制相比有什么優(yōu)勢(shì)呢?ThreadLocal和線程同步機(jī)制都是為了解決多線程中相同變量的訪問(wèn)沖突問(wèn)題正什。
在同步機(jī)制中啥纸,通過(guò)對(duì)象的鎖機(jī)制保證同一時(shí)間只有一個(gè)線程訪問(wèn)變量。這時(shí)該變量是多個(gè)線程共享的婴氮,使用同步機(jī)制要求程序慎密地分析什么時(shí)候?qū)ψ兞窟M(jìn)行讀寫(xiě)斯棒,什么時(shí)候需要鎖定某個(gè)對(duì)象,什么時(shí)候釋放對(duì)象鎖等繁雜的問(wèn)題主经,程序設(shè)計(jì)和編寫(xiě)難度相對(duì)較大荣暮。
而ThreadLocal則從另一個(gè)角度來(lái)解決多線程的并發(fā)訪問(wèn)。ThreadLocal會(huì)為每一個(gè)線程提供一個(gè)獨(dú)立的變量副本罩驻,從而隔離了多個(gè)線程對(duì)數(shù)據(jù)的訪問(wèn)沖突穗酥。因?yàn)槊恳粋€(gè)線程都擁有自己的變量副本,從而也就沒(méi)有必要對(duì)該變量進(jìn)行同步了。ThreadLocal提供了線程安全的共享對(duì)象迷扇,在編寫(xiě)多線程代碼時(shí)百揭,可以把不安全的變量封裝進(jìn)ThreadLocal。
由于ThreadLocal中可以持有任何類(lèi)型的對(duì)象蜓席,低版本JDK所提供的get()返回的是Object對(duì)象器一,需要強(qiáng)制類(lèi)型轉(zhuǎn)換。但JDK 5.0通過(guò)泛型很好的解決了這個(gè)問(wèn)題厨内,在一定程度地簡(jiǎn)化ThreadLocal的使用祈秕,代碼清單 9 2就使用了JDK 5.0新的ThreadLocal<T>版本。
概括起來(lái)說(shuō)雏胃,對(duì)于多線程資源共享的問(wèn)題请毛,同步機(jī)制采用了“以時(shí)間換空間”的方式,而ThreadLocal采用了“以空間換時(shí)間”的方式瞭亮。前者僅提供一份變量方仿,讓不同的線程排隊(duì)訪問(wèn),而后者為每一個(gè)線程都提供了一份變量统翩,因此可以同時(shí)訪問(wèn)而互不影響仙蚜。
五、Spring使用ThreadLocal解決線程安全問(wèn)題
我們知道在一般情況下厂汗,只有無(wú)狀態(tài)的Bean才可以在多線程環(huán)境下共享委粉,在Spring中,絕大部分Bean都可以聲明為singleton作用域娶桦。就是因?yàn)镾pring對(duì)一些Bean(如RequestContextHolder贾节、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全狀態(tài)采用ThreadLocal進(jìn)行處理衷畦,讓它們也成為線程安全的狀態(tài)栗涂,因?yàn)橛袪顟B(tài)的Bean就可以在多線程中共享了。
一般的Web應(yīng)用劃分為展現(xiàn)層祈争、服務(wù)層和持久層三個(gè)層次斤程,在不同的層中編寫(xiě)對(duì)應(yīng)的邏輯,下層通過(guò)接口向上層開(kāi)放功能調(diào)用铛嘱。在一般情況下暖释,從接收請(qǐng)求到返回響應(yīng)所經(jīng)過(guò)的所有程序調(diào)用都同屬于一個(gè)線程袭厂,如圖9 2所示:
這樣你就可以根據(jù)需要墨吓,將一些非線程安全的變量以ThreadLocal存放,在同一次請(qǐng)求響應(yīng)的調(diào)用線程中纹磺,所有關(guān)聯(lián)的對(duì)象引用到的都是同一個(gè)變量帖烘。
下面的實(shí)例能夠體現(xiàn)Spring對(duì)有狀態(tài)Bean的改造思路:
代碼清單3 TopicDao:非線程安全
public class TopicDao {
private Connection conn;
①一個(gè)非線程安全的變量
public void addTopic(){
Statement stat = conn.createStatement();
②引用非線程安全變量
…
}
}
由于①處的conn是成員變量,因?yàn)閍ddTopic()方法是非線程安全的橄杨,必須在使用時(shí)創(chuàng)建一個(gè)新TopicDao實(shí)例(非singleton)秘症。下面使用ThreadLocal對(duì)conn這個(gè)非線程安全的“狀態(tài)”進(jìn)行改造:
代碼清單4 TopicDao:線程安全
import java.sql.Connection;
import java.sql.Statement;
public class TopicDao {
≌肇浴①使用ThreadLocal保存Connection變量
private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
public static Connection getConnection(){
②如果connThreadLocal沒(méi)有本線程對(duì)應(yīng)的Connection創(chuàng)建一個(gè)新的Connection乡摹,
并將其保存到線程本地變量中役耕。
if (connThreadLocal.get() == null) {
Connection conn = ConnectionManager.getConnection();
connThreadLocal.set(conn);
return conn;
} else{
return connThreadLocal.get();
③直接返回線程本地變量
}
}
public void addTopic() {
④從ThreadLocal中獲取線程對(duì)應(yīng)的Connection
Statement stat = getConnection().createStatement();
}
}
不同的線程在使用TopicDao時(shí)聪廉,先判斷connThreadLocal.get()是否是null瞬痘,如果是null,則說(shuō)明當(dāng)前線程還沒(méi)有對(duì)應(yīng)的Connection對(duì)象板熊,這時(shí)創(chuàng)建一個(gè)Connection對(duì)象并添加到本地線程變量中框全;如果不為null,則說(shuō)明當(dāng)前的線程已經(jīng)擁有了Connection對(duì)象干签,直接使用就可以了津辩。這樣,就保證了不同的線程使用線程相關(guān)的Connection容劳,而不會(huì)使用其它線程的Connection喘沿。因此,這個(gè)TopicDao就可以做到singleton共享了鸭蛙。
當(dāng)然摹恨,這個(gè)例子本身很粗糙,將Connection的ThreadLocal直接放在DAO只能做到本DAO的多個(gè)方法共享Connection時(shí)不發(fā)生線程安全問(wèn)題娶视,但無(wú)法和其它DAO共用同一個(gè)Connection晒哄,要做到同一事務(wù)多DAO共享同一Connection,必須在一個(gè)共同的外部類(lèi)使用ThreadLocal保存Connection肪获。
小結(jié)
解決方法
ThreadLocal是解決線程安全問(wèn)題一個(gè)很好的思路寝凌,它通過(guò)為每個(gè)線程提供一個(gè)獨(dú)立的變量副本解決了變量并發(fā)訪問(wèn)的沖突問(wèn)題。在很多情況下孝赫,ThreadLocal比直接使用synchronized同步機(jī)制解決線程安全問(wèn)題更簡(jiǎn)單较木,更方便,且結(jié)果程序擁有更高的并發(fā)性青柄。
六伐债、ThreadLocal線程局部變量
1. 什么是線程局部變量
什么是線程局部變量(thread-local variable)?輕松使用線程: 不共享有時(shí)是最好的
ThreadLocal 類(lèi)是悄悄地出現(xiàn)在 Java 平臺(tái)版本 1.2 中的致开。雖然支持線程局部變量早就是許多線程工具(例如 Posix pthreads 工具)的一部分峰锁,但 Java Threads API 的最初設(shè)計(jì)卻沒(méi)有這項(xiàng)有用的功能。而且双戳,最初的實(shí)現(xiàn)也相當(dāng)?shù)托Ш缃S捎谶@些原因, ThreadLocal 極少受到關(guān)注,但對(duì)簡(jiǎn)化線程安全并發(fā)程序的開(kāi)發(fā)來(lái)說(shuō)魄衅,它卻是很方便的峭竣。在 輕松使用線程的第 3 部分,Java 軟件顧問(wèn) Brian Goetz 研究了 ThreadLocal 并提供了一些使用技巧晃虫。
編寫(xiě)線程安全類(lèi)是困難的皆撩。它不但要求仔細(xì)分析在什么條件可以對(duì)變量進(jìn)行讀寫(xiě),而且要求仔細(xì)分析其它類(lèi)能如何使用某個(gè)類(lèi)哲银。 有時(shí)毅访,要在不影響類(lèi)的功能、易用性或性能的情況下使類(lèi)成為線程安全的是很困難的盘榨。有些類(lèi)保留從一個(gè)方法調(diào)用到下一個(gè)方法調(diào)用的狀態(tài)信息喻粹,要在實(shí)踐中使這樣 的類(lèi)成為線程安全的是困難的。
管理非線程安全類(lèi)的使用比試圖使類(lèi)成為線程安全的要更容易些草巡。非線程安全類(lèi)通呈匚兀可以安全地在多線程程序中使用,只要您能確保一個(gè)線程所用的類(lèi)的實(shí)例不被其它線程使用山憨。例如查乒,JDBC Connection 類(lèi)是非線程安全的 — 兩個(gè)線程不能在小粒度級(jí)上安全地共享一個(gè) Connection — 但如果每個(gè)線程都有它自己的 Connection ,那么多個(gè)線程就可以同時(shí)安全地進(jìn)行數(shù)據(jù)庫(kù)操作郁竟。
不使用 ThreadLocal 為每個(gè)線程維護(hù)一個(gè)單獨(dú)的 JDBC 連接(或任何其它對(duì)象)當(dāng)然是可能的玛迄;Thread API 給了我們把對(duì)象和線程聯(lián)系起來(lái)所需的所有工具。而 ThreadLocal 則使我們能更容易地把線程和它的每線程(per-thread)數(shù)據(jù)成功地聯(lián)系起來(lái)棚亩。
2. 什么是線程局部變量(thread-local variable)蓖议?
線程局部變量高效地為每個(gè)使用它的線程提供單獨(dú)的線程局部變量值的副本。每個(gè)線程只能看到與自己相聯(lián)系的值讥蟆,而不知道別的線程可 能正在使用或修改它們自己的副本勒虾。一些編譯器(例如 Microsoft Visual C++ 編譯器或 IBM XL FORTRAN 編譯器)用存儲(chǔ)類(lèi)別修飾符(像 static 或 volatile )把對(duì)線程局部變量的支持集成到了其語(yǔ)言中。Java 編譯器對(duì)線程局部變量不提供特別的語(yǔ)言支持瘸彤;相反地修然,它用 ThreadLocal 類(lèi)實(shí)現(xiàn)這些支持, 核心 Thread 類(lèi)中有這個(gè)類(lèi)的特別支持质况。
因?yàn)榫€程局部變量是通過(guò)一個(gè)類(lèi)來(lái)實(shí)現(xiàn)的愕宋,而不是作為 Java 語(yǔ)言本身的一部分,所以 Java 語(yǔ)言線程局部變量的使用語(yǔ)法比內(nèi)建線程局部變量語(yǔ)言的使用語(yǔ)法要笨拙一些结榄。要?jiǎng)?chuàng)建一個(gè)線程局部變量中贝,請(qǐng)實(shí)例化類(lèi) ThreadLocal 的一個(gè)對(duì)象。 ThreadLocal 類(lèi)的行為與 java.lang.ref 中的各種 Reference 類(lèi)的行為很相似潭陪; ThreadLocal 類(lèi)充當(dāng)存儲(chǔ)或檢索一個(gè)值時(shí)的間接句柄雄妥。清單 1 顯示了 ThreadLocal 接口。
清單 1. ThreadLocal 接口
public class ThreadLocal {
public Object get();
public void set(Object newValue);
public Object initialValue();
}
get() 訪問(wèn)器檢索變量的當(dāng)前線程的值依溯; set() 訪問(wèn)器修改當(dāng)前線程的值老厌。 initialValue() 方法是可選的,如果線程未使用過(guò)某個(gè)變量黎炉,那么您可以用這個(gè)方法來(lái)設(shè)置這個(gè)變量的初始值枝秤;它允許延遲初始化。用一個(gè)示例實(shí)現(xiàn)來(lái)說(shuō)明 ThreadLocal 的工作方式是最好的方法慷嗜。清單 2 顯示了 ThreadLocal 的一個(gè)實(shí)現(xiàn)方式淀弹。它不是一個(gè)特別好的實(shí)現(xiàn)(雖然它與最初實(shí)現(xiàn)非常相似),所以很可能性能不佳庆械,但它清楚地說(shuō)明了 ThreadLocal 的工作方式薇溃。
清單 2. ThreadLocal 的糟糕實(shí)現(xiàn)
public class ThreadLocal {
private Map values = Collections.synchronizedMap(new HashMap());
public Object get() {
Thread curThread = Thread.currentThread();
Object o = values.get(curThread);
if (o == null && !values.containsKey(curThread)) {
o = initialValue();
values.put(curThread, o);
}
return o;
}
public void set(Object newValue) {
values.put(Thread.currentThread(), newValue);
}
public Object initialValue() {
return null;
}
}
這個(gè)實(shí)現(xiàn)的性能不會(huì)很好,因?yàn)槊總€(gè) get() 和 set() 操作都需要 values 映射表上的同步缭乘,而且如果多個(gè)線程同時(shí)訪問(wèn)同一個(gè) ThreadLocal 沐序,那么將發(fā)生爭(zhēng)用。此外堕绩,這個(gè)實(shí)現(xiàn)也是不切實(shí)際的策幼,因?yàn)橛?Thread 對(duì)象做 values 映射表中的關(guān)鍵字將導(dǎo)致無(wú)法在線程退出后對(duì) Thread 進(jìn)行垃圾回收,而且也無(wú)法對(duì)死線程的 ThreadLocal 的特定于線程的值進(jìn)行垃圾回收奴紧。
3. 用 ThreadLocal 實(shí)現(xiàn)每線程 Singleton
線程局部變量常被用來(lái)描繪有狀態(tài)“單子”(Singleton) 或線程安全的共享對(duì)象特姐,或者是通過(guò)把不安全的整個(gè)變量封裝進(jìn) ThreadLocal ,或者是通過(guò)把對(duì)象的特定于線程的狀態(tài)封裝進(jìn) ThreadLocal 黍氮。例如唐含,在與數(shù)據(jù)庫(kù)有緊密聯(lián)系的應(yīng)用程序中,程序的很多方法可能都需要訪問(wèn)數(shù)據(jù)庫(kù)沫浆。在系統(tǒng)的每個(gè)方法中都包含一個(gè) Connection 作為參數(shù)是不方便的 — 用“單子”來(lái)訪問(wèn)連接可能是一個(gè)雖然更粗糙觉壶,但卻方便得多的技術(shù)。然而件缸,多個(gè)線程不能安全地共享一個(gè) JDBC Connection 铜靶。如清單 3 所示,通過(guò)使用“單子”中的 ThreadLocal 他炊,我們就能讓我們的程序中的任何類(lèi)容易地獲取每線程 Connection 的一個(gè)引用争剿。這樣,我們可以認(rèn)為 ThreadLocal 允許我們創(chuàng)建 每線程單子痊末。
清單 3. 把一個(gè) JDBC 連接存儲(chǔ)到一個(gè)每線程 Singleton 中
public class ConnectionDispenser {
private static class ThreadLocalConnection extends ThreadLocal {
public Object initialValue() {
return DriverManager.getConnection(ConfigurationSingleton.getDbUrl());
}
}
private ThreadLocalConnection conn = new ThreadLocalConnection();
public static Connection getConnection() {
return (Connection) conn.get();
}
}
任何創(chuàng)建的花費(fèi)比使用的花費(fèi)相對(duì)昂貴些的有狀態(tài)或非線程安全的對(duì)象蚕苇,例如 JDBC Connection 或正則表達(dá)式匹配器,都是可以使用每線程單子(singleton)技術(shù)的好地方凿叠。當(dāng)然涩笤,在類(lèi)似這樣的地方嚼吞,您可以使用其它技術(shù),例如用池蹬碧,來(lái)安全地管理 共享訪問(wèn)舱禽。然而,從可伸縮性角度看恩沽,即使是用池也存在一些潛在缺陷誊稚。因?yàn)槌貙?shí)現(xiàn)必須使用同步,以維護(hù)池?cái)?shù)據(jù)結(jié)構(gòu)的完整性罗心,如果所有線程使用同一個(gè)池里伯,那么 在有很多線程頻繁地對(duì)池進(jìn)行訪問(wèn)的系統(tǒng)中,程序性能將因爭(zhēng)用而降低渤闷。
4. 用 ThreadLocal 簡(jiǎn)化調(diào)試日志紀(jì)錄
其它適合使用 ThreadLocal 但用池卻不能成為很好的替代技術(shù)的應(yīng)用程序包括存儲(chǔ)或累積每線程上下文信息以備稍后檢索之用這樣的應(yīng)用程序疾瓮。例如,假設(shè)您想創(chuàng)建一個(gè)用于管理多線程應(yīng)用程序調(diào)試信息的工具飒箭。您可以用如清單 4 所示的 DebugLogger 類(lèi)作為線程局部容器來(lái)累積調(diào)試信息爷贫。在一個(gè)工作單元的開(kāi)頭,您清空容器补憾,而當(dāng)一個(gè)錯(cuò)誤出現(xiàn)時(shí)漫萄,您查詢(xún)?cè)撊萜饕詸z索這個(gè)工作單元迄今為止生成的所有調(diào)試信息。
清單 4. 用 ThreadLocal 管理每線程調(diào)試日志
public class DebugLogger {
private static class ThreadLocalList extends ThreadLocal {
public Object initialValue() {
return new ArrayList();
}
public List getList() {
return (List) super.get();
}
}
private ThreadLocalList list = new ThreadLocalList();
private static String[] stringArray = new String[0];
public void clear() {
list.getList().clear();
}
public void put(String text) {
list.getList().add(text);
}
public String[] get() {
return list.getList().toArray(stringArray);
}
}
在您的代碼中盈匾,您可以調(diào)用 DebugLogger.put() 來(lái)保存您的程序正在做什么的信息腾务,而且,稍后如果有必要(例如發(fā)生了一個(gè)錯(cuò)誤)削饵,您能夠容易地檢索與某個(gè)特定線程相關(guān)的調(diào)試信息岩瘦。 與簡(jiǎn)單地把所有信息轉(zhuǎn)儲(chǔ)到一個(gè)日志文件,然后努力找出哪個(gè)日志記錄來(lái)自哪個(gè)線程(還要擔(dān)心線程爭(zhēng)用日志紀(jì)錄對(duì)象)相比窿撬,這種技術(shù)簡(jiǎn)便得多启昧,也有效得多。
ThreadLocal 在基于 servlet 的應(yīng)用程序或工作單元是一個(gè)整體請(qǐng)求的任何多線程應(yīng)用程序服務(wù)器中也是很有用的劈伴,因?yàn)樵谔幚碚?qǐng)求的整個(gè)過(guò)程中將要用到單個(gè)線程密末。您可以通過(guò)前面講述的每線程單子技術(shù)用 ThreadLocal 變量來(lái)存儲(chǔ)各種每請(qǐng)求(per-request)上下文信息。
5. ThreadLocal 的線程安全性稍差的堂兄弟跛璧,InheritableThreadLocal
ThreadLocal 類(lèi)有一個(gè)親戚严里,InheritableThreadLocal,它以相似的方式工作追城,但適用于種類(lèi)完全不同的應(yīng)用程序刹碾。創(chuàng)建一個(gè)線程時(shí)如果保存了所有 InheritableThreadLocal 對(duì)象的值,那么這些值也將自動(dòng)傳遞給子線程座柱。如果一個(gè)子線程調(diào)用 InheritableThreadLocal 的 get() 迷帜,那么它將與它的父線程看到同一個(gè)對(duì)象物舒。為保護(hù)線程安全性,您應(yīng)該只對(duì)不可變對(duì)象(一旦創(chuàng)建戏锹,其狀態(tài)就永遠(yuǎn)不會(huì)被改變的對(duì)象)使用 InheritableThreadLocal 冠胯,因?yàn)閷?duì)象被多個(gè)線程共享。 InheritableThreadLocal 很合適用于把數(shù)據(jù)從父線程傳到子線程景用,例如用戶(hù)標(biāo)識(shí)(user id)或事務(wù)標(biāo)識(shí)(transaction id),但不能是有狀態(tài)對(duì)象惭蹂,例如 JDBC Connection 伞插。
七、ThreadLocal 的性能
雖然線程局部變量早已赫赫有名并被包括 Posix pthreads 規(guī)范在內(nèi)的很多線程框架支持盾碗,但最初的 Java 線程設(shè)計(jì)中卻省略了它媚污,只是在 Java 平臺(tái)的版本 1.2 中才添加上去。在很多方面廷雅, ThreadLocal 仍在發(fā)展之中耗美;在版本 1.3 中它被重寫(xiě),版本 1.4 中又重寫(xiě)了一次航缀,兩次都專(zhuān)門(mén)是為了性能問(wèn)題商架。
在 JDK 1.2 中, ThreadLocal 的實(shí)現(xiàn)方式與清單 2 中的方式非常相似芥玉,除了用同步 WeakHashMap 代替 HashMap 來(lái)存儲(chǔ) values 之外蛇摸。(以一些額外的性能開(kāi)銷(xiāo)為代價(jià),使用 WeakHashMap 解決了無(wú)法對(duì) Thread 對(duì)象進(jìn)行垃圾回收的問(wèn)題灿巧。)不用說(shuō)赶袄, ThreadLocal 的性能是相當(dāng)差的。
Java 平臺(tái)版本 1.3 提供的 ThreadLocal 版本已經(jīng)盡量更好了抠藕;它不使用任何同步饿肺,從而不存在可伸縮性問(wèn)題,而且它也不使用弱引用盾似。相反地敬辣,人們通過(guò)給 Thread 添加一個(gè)實(shí)例變量(該變量用于保存當(dāng)前線程的從線程局部變量到它的值的映射的 HashMap )來(lái)修改 Thread 類(lèi)以支持 ThreadLocal 。因?yàn)闄z索或設(shè)置一個(gè)線程局部變量的過(guò)程不涉及對(duì)可能被另一個(gè)線程讀寫(xiě)的數(shù)據(jù)的讀寫(xiě)操作零院,所以您可以不用任何同步就實(shí)現(xiàn) ThreadLocal.get() 和 set() 购岗。而且,因?yàn)槊烤€程值的引用被存儲(chǔ)在自已的 Thread 對(duì)象中门粪,所以當(dāng)對(duì) Thread 進(jìn)行垃圾回收時(shí)喊积,也能對(duì)該 Thread 的每線程值進(jìn)行垃圾回收。
不幸的是玄妈,即使有了這些改進(jìn)乾吻,Java 1.3 中的 ThreadLocal 的性能仍然出奇地慢髓梅。據(jù)我的粗略測(cè)量,在雙處理器 Linux 系統(tǒng)上的 Sun 1.3 JDK 中進(jìn)行 ThreadLocal.get() 操作绎签,所耗費(fèi)的時(shí)間大約是無(wú)爭(zhēng)用同步的兩倍枯饿。性能這么差的原因是 Thread.currentThread() 方法的花費(fèi)非常大,占了 ThreadLocal.get() 運(yùn)行時(shí)間的三分之二還多诡必。雖然有這些缺點(diǎn)奢方,JDK 1.3 ThreadLocal.get() 仍然比爭(zhēng)用同步快得多,所以如果在任何存在嚴(yán)重爭(zhēng)用的地方(可能是有非常多的線程爸舒,或者同步塊被頻繁地執(zhí)行蟋字,或者同步塊很大), ThreadLocal 可能仍然要高效得多扭勉。
在 Java 平臺(tái)的最新版本鹊奖,即版本 1.4b2 中, ThreadLocal 和 Thread.currentThread() 的性能都有了很大提高涂炎。有了這些提高忠聚, ThreadLocal 應(yīng)該比其它技術(shù),如用池唱捣,更快两蟀。由于它比其它技術(shù)更簡(jiǎn)單,也更不易出錯(cuò)震缭,人們最終將發(fā)現(xiàn)它是避免線程間出現(xiàn)不希望的交互的有效途徑垫竞。
八、ThreadLocal 的好處
ThreadLocal 能帶來(lái)很多好處蛀序。它常常是把有狀態(tài)類(lèi)描繪成線程安全的欢瞪,或者封裝非線程安全類(lèi)以使它們能夠在多線程環(huán)境中安全地使用的最容易的方式。使用 ThreadLocal 使我們可以繞過(guò)為實(shí)現(xiàn)線程安全而對(duì)何時(shí)需要同步進(jìn)行判斷的復(fù)雜過(guò)程徐裸,而且因?yàn)樗恍枰魏瓮角补模砸哺纳屏丝缮炜s性。除簡(jiǎn)單之外重贺,用 ThreadLocal 存儲(chǔ)每線程單子或每線程上下文信息在歸檔方面還有一個(gè)頗有價(jià)值好處 — 通過(guò)使用 ThreadLocal 骑祟,存儲(chǔ)在 ThreadLocal 中的對(duì)象都是 不被線程共享的是清晰的,從而簡(jiǎn)化了判斷一個(gè)類(lèi)是否線程安全的工作气笙。