ThreadLocal類的實現用法
ThreadLocal的接口方法
ThreadLocal類接口很簡單,只有4個方法,我們先來了解一下:
- void set(Object value)設置當前線程的線程局部變量的值。
- public Object get()該方法返回當前線程所對應的線程局部變量。
- public void remove()將當前線程局部變量的值刪除,目的是為了減少內存的占用,該方法是JDK 5.0新增的方法。需要指出的是,當線程結束后,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量并不是必須的操作,但它可以加快內存回收的速度。
- protected Object initialValue()返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是為了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,并且僅執行1次。ThreadLocal中的缺省實現直接返回一個null。
模擬實現ThreadLocal代碼:
1 public class ThreadShareData {
2 static int num = 0;
3
4
5 public static void main(String[] args) {
6 Map<Thread, Integer> map = new HashMap<Thread, Integer>();
7 for (int i = 0; i < 2; i++) {
8 new Thread(new Runnable() {
9 @Override
10 public void run() {
11 int num = new Random().nextInt();
12 System.out.println(Thread.currentThread().getName()+":"+" get num: "+num);
13 new A().get();
14 new B().get();
15 }
16 }).start();
17 }
18 }
19
20 static class A{
21 public void get(){
22 System.out.println("A: "+Thread.currentThread().getName() +"get num: "+num);
23 }
24 }
25
26 static class B{
27 public void get(){
28 System.out.println("A: "+Thread.currentThread().getName() +"get num: "+num);
29 }
30 }
31
32 }
ThreadLocal的經典用法:
1 public class ThreadLocalShareData2 {
2 static ThreadLocal<People> threadLocal = new ThreadLocal<People>();
3 public static void main(String[] args) {
4 for (int i = 0; i < 2; i++) {
5 new Thread(new Runnable() {
6 @Override
7 public void run() {
8 int data = new Random().nextInt();
9 People people = new People().getInstance();
10 people.setName("name"+data);
11 people.setAge(data);
12 System.out.println(Thread.currentThread().getName()+" set name "+people.getName()+" set age "+people.getAge());
13 new A().get();
14 new B().get();
15 }
16 }).start();
17 }
18 }
19
20 static class A{
21 public void get(){
22 System.out.println("A: "+Thread.currentThread().getName() +"get name "+new People().getInstance().getName()+" get age "+new People().getInstance().getAge());
23 }
24 }
25 static class B{
26 public void get(){
27 System.out.println("B: "+Thread.currentThread().getName() +"get name "+new People().getInstance().getName()+" get age "+new People().getInstance().getAge());
28 }
29 }
30 static class People{
31 private People(){
32
33 }
34 public People getInstance(){
35 People people = threadLocal.get();
36 if(people == null){
37 people = new People();
38 threadLocal.set(people);
39 }
40 return people;
41 }
42 private int age;
43 private String name;
44 public int getAge() {
45 return age;
46 }
47 public String getName() {
48 return name;
49 }
50 public void setAge(int age) {
51 this.age = age;
52 }
53 public void setName(String name) {
54 this.name = name;
55 }
56 }
57 }
將ThreadLocal仿單例模式進行實現,更加面向對象。
在ThreadLocal類中有一個Map,用于存儲每一個線程的變量副本,Map中元素的鍵為線程對象,而值對應線程的變量副本。
1 public class TestNum {
2 // ①通過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值
3 private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
4 public Integer initialValue() {
5 return 0;
6 }
7 };
8
9 // ②獲取下一個序列值
10 public int getNextNum() {
11 seqNum.set(seqNum.get() + 1);
12 return seqNum.get();
13 }
14
15 public static void main(String[] args) {
16 TestNum sn = new TestNum();
17 // ③ 3個線程共享sn,各自產生序列號
18 TestClient t1 = new TestClient(sn);
19 TestClient t2 = new TestClient(sn);
20 TestClient t3 = new TestClient(sn);
21 t1.start();
22 t2.start();
23 t3.start();
24 }
25
26 private static class TestClient extends Thread {
27 private TestNum sn;
28
29 public TestClient(TestNum sn) {
30 this.sn = sn;
31 }
32
33 public void run() {
34 for (int i = 0; i < 3; i++) {
35 // ④每個線程打出3個序列值
36 System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["
37 + sn.getNextNum() + "]");
38 }
39 }
40 }
41 }
我們發現每個線程所產生的序號雖然都共享同一個TestNum實例,但它們并沒有發生相互干擾的情況,而是各自產生獨立的序列號,這是因為我們通過ThreadLocal為每一個線程提供了單獨的副本。
Thread同步機制的比較:
ThreadLocal和線程同步機制相比有什么優勢呢?ThreadLocal和線程同步機制都是為了解決多線程中相同變量的訪問沖突問題。
在同步機制中,通過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機制要求程序慎密地分析什么時候對變量進行讀寫,什么時候需要鎖定某個對象,什么時候釋放對象鎖等繁雜的問題,程序設計和編寫難度相對較大。
而ThreadLocal則從另一個角度來解決多線程的并發訪問。ThreadLocal會為每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問沖突。因為每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的變量封裝進ThreadLocal。
由于ThreadLocal中可以持有任何類型的對象,低版本JDK所提供的get()返回的是Object對象,需要強制類型轉換。但JDK 5.0通過泛型很好的解決了這個問題,在一定程度地簡化ThreadLocal的使用,代碼清單 9 2就使用了JDK 5.0新的ThreadLocal<T>版本。
概括起來說,對于多線程資源共享的問題,同步機制采用了“以時間換空間”的方式,而ThreadLocal采用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。
Spring使用ThreadLocal解決線程安全問題我們知道在一般情況下,只有無狀態的Bean才可以在多線程環境下共享,在Spring中,絕大部分Bean都可以聲明為singleton作用域。就是因為Spring對一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全狀態采用ThreadLocal進行處理,讓它們也成為線程安全的狀態,因為有狀態的Bean就可以在多線程中共享了。
一般的Web應用劃分為展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過接口向上層開放功能調用。在一般情況下,從接收請求到返回響應所經過的所有程序調用都同屬于一個線程,如圖9?2所示:
同一線程貫通三層這樣你就可以根據需要,將一些非線程安全的變量以ThreadLocal存放,在同一次請求響應的調用線程中,所有關聯的對象引用到的都是同一個變量。
下面的實例能夠體現Spring對有狀態Bean的改造思路:
非線程安全代碼:
1 public class TestDao {
2 private Connection conn;// ①一個非線程安全的變量
3
4 public void addTopic() throws SQLException {
5 Statement stat = conn.createStatement();// ②引用非線程安全變量
6 // …
7 }
8 }
線程安全代碼:
1 public class TestDaoNew {
2 // ①使用ThreadLocal保存Connection變量
3 private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
4
5 public static Connection getConnection() {
6 // ②如果connThreadLocal沒有本線程對應的Connection創建一個新的Connection,
7 // 并將其保存到線程本地變量中。
8 if (connThreadLocal.get() == null) {
9 Connection conn = getConnection();
10 connThreadLocal.set(conn);
11 return conn;
12 } else {
13 return connThreadLocal.get();// ③直接返回線程本地變量
14 }
15 }
16
17 public void addTopic() throws SQLException {
18 // ④從ThreadLocal中獲取線程對應的Connection
19 Statement stat = getConnection().createStatement();
20 }
21 }
不同的線程在使用TopicDao時,先判斷connThreadLocal.get()是否是null,如果是null,則說明當前線程還沒有對應的Connection對象,這時創建一個Connection對象并添加到本地線程變量中;如果不為null,則說明當前的線程已經擁有了Connection對象,直接使用就可以了。這樣,就保證了不同的線程使用線程相關的Connection,而不會使用其它線程的Connection。因此,這個TopicDao就可以做到singleton共享了。
當然,這個例子本身很粗糙,將Connection的ThreadLocal直接放在DAO只能做到本DAO的多個方法共享Connection時不發生線程安全問題,但無法和其它DAO共用同一個Connection,要做到同一事務多DAO共享同一Connection,必須在一個共同的外部類使用ThreadLocal保存Connection。
1 public class ConnectionManager {
2
3 private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
4 @Override
5 protected Connection initialValue() {
6 Connection conn = null;
7 try {
8 conn = DriverManager.getConnection(
9 "jdbc:mysql://localhost:3306/test", "username",
10 "password");
11 } catch (SQLException e) {
12 e.printStackTrace();
13 }
14 return conn;
15 }
16 };
17
18 public static Connection getConnection() {
19 return connectionHolder.get();
20 }
21
22 public static void setConnection(Connection conn) {
23 connectionHolder.set(conn);
24 }
25 }
還有一個經典實例,在HibernateUtil中,用于session的管理:
1 public class HibernateUtil {
2 private static Log log = LogFactory.getLog(HibernateUtil.class);
3 private static final SessionFactory sessionFactory; //定義SessionFactory
4
5 static {
6 try {
7 // 通過默認配置文件hibernate.cfg.xml創建SessionFactory
8 sessionFactory = new Configuration().configure().buildSessionFactory();
9 } catch (Throwable ex) {
10 log.error("初始化SessionFactory失敗!", ex);
11 throw new ExceptionInInitializerError(ex);
12 }
13 }
14
15 //創建線程局部變量session,用來保存Hibernate的Session
16 public static final ThreadLocal session = new ThreadLocal();
17
18 /**
19 * 獲取當前線程中的Session
20 * @return Session
21 * @throws HibernateException
22 */
23 public static Session currentSession() throws HibernateException {
24 Session s = (Session) session.get();
25 // 如果Session還沒有打開,則新開一個Session
26 if (s == null) {
27 s = sessionFactory.openSession();
28 session.set(s); //將新開的Session保存到線程局部變量中
29 }
30 return s;
31 }
32
33 public static void closeSession() throws HibernateException {
34 //獲取線程局部變量,并強制轉換為Session類型
35 Session s = (Session) session.get();
36 session.set(null);
37 if (s != null)
38 s.close();
39 }
40 }
總結:
ThreadLocal使用場合主要解決多線程中數據數據因并發產生不一致問題。ThreadLocal為每個線程的中并發訪問的數據提供一個副本,通過訪問副本來運行業務,這樣的結果是耗費了內存,單大大減少了線程同步所帶來性能消耗,也減少了線程并發控制的復雜度。
ThreadLocal不能使用原子類型,只能使用Object類型。ThreadLocal的使用比synchronized要簡單得多。
ThreadLocal和Synchonized都用于解決多線程并發訪問。但是ThreadLocal與synchronized有本質的區別。synchronized是利用鎖的機制,使變量或代碼塊在某一時該只能被一個線程訪問。而ThreadLocal為每一個線程都提供了變量的副本,使得每個線程在某一時間訪問到的并不是同一個對象,這樣就隔離了多個線程對數據的數據共享。而 Synchronized卻正好相反,它用于在多個線程間通信時能夠獲得數據共享。
Synchronized用于線程間的數據共享,而ThreadLocal則用于線程間的數據隔離。
當然ThreadLocal并不能替代synchronized,它們處理不同的問題域。Synchronized用于實現同步機制,比ThreadLocal更加復雜。
參考文檔:
JDK 官方文檔
http://blog.csdn.net/lufeng20/article/details/24314381
http://lavasoft.blog.51cto.com/62575/51926/
</div> </div>