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 }
總結: