ThreadLocal

ThreadLocal

JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal為解決多執行緒程式的並發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多執行緒程式,ThreadLocal並不是一個Thread,而是Thread的局部變數

基本介紹

  • 中文名:執行緒局部變數
  • 外文名:ThreadLocal
  • 版本:JDK 1.2
  • 定位局部變數
  • 作用:提供了一種新的思路
介紹,執行緒程式介紹,關於其變數,執行緒,執行緒舉例,使用方法,實例,舉例,分析,場景,場景說明,多執行緒訪問,說明,解決方法,

介紹

執行緒程式介紹

早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal為解決多執行緒程式的並發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多執行緒程式。

關於其變數

ThreadLocal很容易讓人望文生義,想當然地認為是一個“本地執行緒”。其實,ThreadLocal並不是一個Thread,而是Thread的局部變數,也許把它命名為ThreadLocalVariable更容易讓人理解一些。
所以,在Java中編寫執行緒局部變數的代碼相對來說要笨拙一些,因此造成執行緒局部變數沒有在Java開發者中得到很好的普及。
ThreadLocal的接口方法
ThreadLocal類接口很簡單,只有4個方法,我們先來了解一下:
void set(Object value)
public void remove()
將當前執行緒局部變數的值刪除,目的是為了減少記憶體的占用,該方法是JDK 1.5 新增的方法。需要指出的是,當執行緒結束後,對應該執行緒的局部變數將自動被垃圾回收,所以顯式調用該方法清除執行緒的局部變數並不是必須的操作,但它可以加快記憶體回收的速度。
protected Object initialValue()
返回該執行緒局部變數的初始值,該方法是一個protected的方法,顯然是為了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在執行緒第1次調用get()或set(Object)時才執行,並且僅執行1次。ThreadLocal中的預設實現直接返回一個null。
值得一提的是,在JDK5.0中,ThreadLocal已經支持泛型,該類的類名已經變為ThreadLocal<T>。API方法也相應進行了調整,新版本的API方法分別是void set(T value)、T get()以及T initialValue()。
ThreadLocal是如何做到為每一個執行緒維護變數的副本的呢?其實實現的思路很簡單:在ThreadLocal類中定義了一個ThreadLocalMap,每一個Thread中都有一個該類型的變數——threadLocals——用於存儲每一個執行緒的變數副本,Map中元素的鍵為執行緒對象,而值對應執行緒的變數副本。

執行緒

To keep state with a thread (user-id, transaction-id, logging-id) To cache objects which you need frequentlyThreadLocal類
它主要由四個方法組成initialValue(),get(),set(T),remove(),其中值得注意的是initialValue(),該方法是一個protected的方法,顯然是為了子類重寫而特意實現的。該方法返回當前執行緒在該執行緒局部變數的初始值,這個方法是一個延遲調用方法,在一個執行緒第1次調用get()時才執行,並且僅執行1次(即:最多在每次訪問執行緒來獲得每個執行緒局部變數時調用此方法一次,即執行緒第一次使用get()方法訪問變數的時候。如果執行緒先於get方法調用set(T)方法,則不會線上程中再調用initialValue方法)。ThreadLocal中的預設實現直接返回一個null:

執行緒舉例

ThreadLocal的原理
在ThreadLocal類中有一個Map,用於存儲每一個執行緒的變數的副本。比如下面的示例實現:
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);
}
values.put(Thread.currentThread(), newValue);
return o ;
}
public Object initialValue()
{
return null;
}
}

使用方法

ThreadLocal 的使用
使用方法一:
Hibernate的文檔時看到了關於使ThreadLocal管理多執行緒訪問的部分。具體代碼如下
1. public static final ThreadLocal session = new ThreadLocal();
2. public static Session currentSession() {
3. Session s = (Session)session.get();
4. //open a new session,if this session has none
5. if(s == null){
6. s = sessionFactory.openSession();
7. session.set(s);
8. }
return s;
9. }
我們逐行分析
1。 初始化一個ThreadLocal對象,ThreadLocal有三個成員方法 get()、set()、initialvalue()。
如果不初始化initialvalue,則initialvalue返回null。
3。session的get根據當前執行緒返回其對應的執行緒內部變數,也就是我們需要的net.sf.hibernate.Session(相當於對應每個資料庫連線).多執行緒情況下共享資料庫連結是不安全的。ThreadLocal保證了每個執行緒都有自己的s(資料庫連線)。
5。如果是該執行緒初次訪問,自然,s(資料庫連線)會是null,接著創建一個Session,具體就是行6。
6。創建一個資料庫連線實例 s
7。保存該資料庫連線s到ThreadLocal中。
8。如果當前執行緒已經訪問過資料庫了,則從session中get()就可以獲取該執行緒上次獲取過的連線實例。
使用方法二
當要給執行緒初始化一個特殊值時,需要自己實現ThreadLocal的子類並重寫該方法,通常使用一個內部匿名類對ThreadLocal進行子類化,EasyDBO中創建jdbc連線上下文就是這樣做的:
public class JDBCContext{
private static Logger logger = Logger.getLogger(JDBCContext.class);
private DataSource ds;
protected Connection connection;
}
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);
}
}
}
簡單的實現版本
代碼清單1 SimpleThreadLocal
public class SimpleThreadLocal {
private Map valueMap = Collections.synchronizedMap(new HashMap());
public void set(Object newValue) {
valueMap.put(Thread.currentThread(), newValue);①鍵為執行緒對象,值為本執行緒的變數副本
}
public Object get() {
valueMap.put(currentThread, o);
}
return o;
}
public void remove() {
valueMap.remove(Thread.currentThread());
}
public Object initialValue() {
return null;
}
}
雖然代碼清單9‑3這個ThreadLocal實現版本顯得比較幼稚,但它和JDK所提供的ThreadLocal類在實現思路上是相近的。

實例

舉例

下面,我們通過一個具體的實例了解一下ThreadLocal的具體使用方法。
代碼清單2 SequenceNumber
package com.baobaotao.basic;
public class SequenceNumber {
// ①通過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值
private static ThreadLocal seqNum = new ThreadLocal() {
public Integer initialValue() {
return 0;
}
};
// ②獲取下一個序列值
public int getNextNum() {
seqNum.set((Integer) seqNum.get() + 1);
return (Integer) seqNum.get();
}
public static void main(String[] args) {
SequenceNumber sn = new SequenceNumber();
// ③ 3個執行緒共享sn,各自產生序列號
TestClient t1 = new TestClient(sn);
TestClient t2 = new TestClient(sn);
TestClient t3 = new TestClient(sn);
new Thread(t1).start();
new Thread(t2).start();
new Thread(t3).start();
}
}
class TestClient implements Runnable {
private SequenceNumber sn;
public TestClient(SequenceNumber sn) {
super();
this.sn = sn;
}
public void run() {
for (int i = 0; i < 3; i++) {
// ④每個執行緒打出3個序列值
System.out.println("thread[" + Thread.currentThread().getName()
+ "] sn[" + sn.getNextNum() + "]");
}
}
}

分析

通常我們通過匿名內部類的方式定義ThreadLocal的子類,提供初始的變數值,如例子中①處所示。TestClient執行緒產生一組序列號,在③處,我們生成3個TestClient,它們共享同一個SequenceNumber實例。運行以上代碼,在控制台上輸出以下的結果:
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]
考察輸出的結果信息,我們發現每個執行緒所產生的序號雖然都共享同一個SequenceNumber實例,但它們並沒有發生相互干擾的情況,而是各自產生獨立的序列號,這是因為我們通過ThreadLocal為每一個執行緒提供了單獨的副本。
ThreadLocal中的數據不是副本!!!!
用Integer來舉例證明是錯誤的選擇,integer在執行運算後已經不是原理的哪個integer了。

場景

場景說明

在Java的多執行緒編程中,為保證多個執行緒對共享變數的安全訪問,通常會使用synchronized來保證同一時刻只有一個執行緒對共享變數進行操作。這種情況下可以將類變數放到ThreadLocal類型的對象中,使變數在每個執行緒中都有獨立拷貝,不會出現一個執行緒讀取變數時而被另一個執行緒修改的現象。
下面舉例說明:
public class QuerySvc {
private static ThreadLocal sqlHolder = new ThreadLocal();
public QuerySvc() {
}
public void execute() {
System.out.println("Thread " + Thread.currentThread().getId() +" Sql is " +sqlHolder.get());
System.out.println("Thread " + Thread.currentThread().getId() +" Thread Local variable Sql is " + sqlHolder.get());
}
public void setSql(String sql) {
sqlHolder.set(sql);
}
}

多執行緒訪問

為了說明多執行緒訪問對於類變數和ThreadLocal變數的影響,QuerySvc中分別設定了類變數sql和ThreadLocal變數,這種場景類似web套用中多個請求執行緒攜帶不同查詢條件對一個servlet實例的訪問,然後servlet調用業務對象,並傳入不同查詢條件,最後要保證每個請求得到的結果是對應的查詢條件的結果。
使用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();
}
}
運行執行緒代碼如下:
QuerySvc qs = new QuerySvc();
for (int k=0; k<10; k++)
String sql = "Select * from table where id =" + k;
new Work(qs,sql).start();
}
先創建一個QuerySvc實例對象,而ThreadLocal中的值總是和set中設定的值一樣,這樣通過使用ThreadLocal獲得了執行緒安全性
如果一個對象要被多個執行緒訪問,而該對象存在類變數被不同類方法讀寫,為獲得執行緒安全,可以用ThreadLocal來替代類變數。
Thread同步機制的比較

說明

ThreadLocal和執行緒同步機制相比有什麼優勢呢?ThreadLocal和執行緒同步機制都是為了解決多執行緒中相同變數的訪問衝突問題。
在同步機制中,通過對象的鎖機制保證同一時間只有一個執行緒訪問變數。這時該變數是多個執行緒共享的,使用同步機制要求程式慎密地分析什麼時候對變數進行讀寫,什麼時候需要鎖定某個對象,什麼時候釋放對象鎖等繁雜的問題,程式設計和編寫難度相對較大。
而ThreadLocal則從另一個角度來解決多執行緒的並發訪問。在編寫多執行緒代碼時,可以把不安全的變數封裝進ThreadLocal。
由於ThreadLocal中可以持有任何類型的對象,低版本JDK所提供的get()返回的是Object對象,需要強制類型轉換。但JDK 5.0通過泛型很好的解決了這個問題,在一定程度地簡化ThreadLocal的使用,代碼清單 9 2就使用了JDK 5.0新的ThreadLocal<T>版本。
概括起來說,對於多執行緒資源共享的問題,同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。前者僅提供一份變數,讓不同的執行緒排隊訪問,而後者為每一個執行緒都提供了一份變數,因此可以同時訪問而互不影響。
Spring使用ThreadLocal解決執行緒安全問題
我們知道在一般情況下,TransactionSynchronizationManager、LocaleContextHolder等)中非執行緒安全狀態採用ThreadLocal進行處理,讓它們也成為執行緒安全的狀態,因為有狀態的Bean就可以在多執行緒中共享了。
一般的Web套用劃分為展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過接口向上層開放功能調用。在一般情況下,從接收請求到返迴響應所經過的所有程式調用都同屬於一個執行緒,如圖9‑2所示:圖1同一執行緒貫通三層
這樣你就可以根據需要,將一些非執行緒安全的變數以ThreadLocal存放,在同一次請求回響的調用執行緒中,所有關聯的對象引用到的都是同一個變數。
下面的實例能夠體現Spring對有狀態Bean的改造思路:
代碼清單3 TopicDao:非執行緒安全
public class TopicDao {
private Connection conn;①一個非執行緒安全的變數
public void addTopic(){
Statement stat = conn.createStatement();②引用非執行緒安全變數
}
}
由於①處的conn是成員變數,因為addTopic()方法是非執行緒安全的,必須在使用時創建一個新TopicDao實例(非singleton)。下面使用ThreadLocal對conn這個非執行緒安全的“狀態”進行改造:
代碼清單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沒有本執行緒對應的Connection創建一個新的Connection,
並將其保存到執行緒本地變數中。
return connThreadLocal;
}
public void addTopic() {
④從ThreadLocal中獲取執行緒對應的Connection
Statement stat = getConnection().createStatement();
}
}
不同的執行緒在使用TopicDao時,這樣,就保證了不同的執行緒使用執行緒相關的Connection,而不會使用其它執行緒的Connection。因此,這個TopicDao就可以做到singleton共享了。
當然,這個例子本身很粗糙,將Connection的ThreadLocal直接放在DAO只能做到本DAO的多個方法共享Connection時不發生執行緒安全問題,但無法和其它DAO共用同一個Connection,要做到同一事務多DAO共享同一Connection,必須在一個共同的外部類使用ThreadLocal保存Connection。
小結

解決方法

ThreadLocal是解決執行緒安全問題一個很好的思路,它通過為每個執行緒提供一個獨立的變數副本解決了變數並發訪問的衝突問題。在很多情況下,ThreadLocal比直接使用synchronized同步機制解決執行緒安全問題更簡單,更方便,且結果程式擁有更高的並發性。

相關詞條

熱門詞條

聯絡我們