10. 什麼是ThreadLocal ThreadLocal翻譯成中文比較準確的叫法應該是:線程局部變數。或稱為 線程本地變數 這個玩意有什麼用處?先解釋一下,在併發編程的時候,一個單例模式的類的屬性,如果不做任何處理(是否加鎖,或者用原子類)其實是線程不安全的,各個線程都在操作同一個屬性,比如Co ...
10. 什麼是ThreadLocal
ThreadLocal翻譯成中文比較準確的叫法應該是:線程局部變數。或稱為 線程本地變數
這個玩意有什麼用處?先解釋一下,在併發編程的時候,一個單例模式的類的屬性,如果不做任何處理(是否加鎖,或者用原子類)其實是線程不安全的,各個線程都在操作同一個屬性,比如CoreServlet,Servlet是單例模式,所以如果在Servlet中增加一個屬性,那麼就會有多線程訪問這個屬性就會誘發的安全性問題。
這樣顯然是不行的,並且我們也知道volatile這個關鍵字只能保證線程的可見性,不能保證線程安全的。如果加鎖,效率有會有一定程度的降低。
那麼我們需要滿足這樣一個條件:屬性是同一個,但是每個線程都使用同一個初始值,也就是使用同一個變數的一個新的副本。這種情況之下ThreadLocal就非常使用,比如說DAO的資料庫連接,DAO我們在實際項目中都會是單例模式的,那麼他的屬性Connection就不是一個線程安全的變數。而我們每個線程都需要使用他,並且各自使用各自的。這種情況,ThreadLocal就比較好的解決了這個問題。
ThreadLocal的主要作用:
ThreadLocal的作用主要是做數據隔離,填充的數據只屬於當前線程,變數的數據對別的線程而言是相對隔離的,在多線程環境下,如何防止自己的變數被其它線程篡改。
Spring採用Threadlocal的方式,來保證單個線程中的資料庫操作使用的是同一個資料庫連接,同時,採用這種方式可以使業務層使用事務時不需要感知並管理connection對象,通過傳播級別,巧妙地管理多個事務配置之間的切換,掛起和恢復。
Spring框架裡面就是用的ThreadLocal來實現這種隔離,主要是在TransactionSynchronizationManager這個類裡面,代碼如下所示:
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal<>("Current transaction name"); |
Spring的事務主要是ThreadLocal和AOP去做實現的,我這裡提一下,大家知道每個線程自己的Connection conn是靠ThreadLocal保存的就好了。
ThreadLocal結構圖:
當ThreadLocal Ref出棧後,由於ThreadLocalMap中Entry對ThreadLocal只是弱引用,所以ThreadLocal對象會被回收,Entry的key會變成null,然後在每次get/set/remove ThreadLocalMap中的值的時候,會自動清理key為null的value,這樣value也能被回收了。
註意:如果ThreadLocal Ref一直沒有出棧(例如上面的connectionHolder,通常我們需要保證ThreadLocal為單例且全局可訪問,所以設為static),具有跟Thread相同的生命周期,那麼這裡的虛引用便形同虛設了,所以使用完後記得調用ThreadLocal.remove將其對應的value清除。
另外,由於ThreadLocalMap中只對ThreadLocal是弱引用,對value是強引用,如果ThreadLocal因為沒有其他強引用而被回收,之後也沒有調用過get/set,那麼就會產生記憶體泄露,
在使用線程池時,線程會被覆用,那麼裡面保存的ThreadLocalMap同樣也會被覆用,會造成線程之間的資源沒有被隔離,所以線上程歸還回線程池時要記得調用remove方法。
hash衝突
上面提到ThreadLocalMap是自己實現的類似HashMap的功能,當出現Hash衝突(通過兩個key對象的hash值計算得到同一個數組下標)時,它沒有採用鏈表模式,而是採用的線性探測的方法,既當發生衝突後,就線性查找數組中空閑的位置。
當數組較大時,這個性能會很差,所以建議儘量控制ThreadLocal的數量。
ThreadLocal常用方法:
ThreadLocal在案例中一般以static形式存在的。
initialValue方法
此方法為ThreadLocal保存的數據類型指定的一個初始化值,在ThreadLocal中預設返回null。但可以重寫initialValue()方法進行數據初始化。
如果使用的是Java8提供的Supplier函數介面更加簡化:
set(T value)方法
get方法
get()用於返回當前線程ThreadLocal中數據備份,當前線程的數據都存在一個ThreadLocalMap的數據結構中。
remomve()刪除值
小結
initialValue() : 初始化ThreadLocal中的value屬性值。
set():獲取當前線程,根據當前線程從ThreadLocals中獲取ThreadLocalMap數據結構,
如果ThreadLocalmap的數據結構沒創建,則創建ThreadLocalMap,key為當前ThreadLocal實例,存入數據為當前value。ThreadLocal會創建一個預設長度為16Entry節點,並將k-v放入i位置(i位置計算方式和hashmap相似,當前線程的hashCode&(entry預設長度-1)),並設置閾值(預設為0)為Entry預設長度的2/3。
如果ThreadLocalMap存在。就會遍歷整個Map中的Entry節點,如果entry中的key和本線程ThreadLocal相同,將數據(value)直接覆蓋,並返回。如果ThreadLoca為null,驅除ThreadLocal為null的Entry,並放入Value,這也是記憶體泄漏的重點地區。
get()
get()方法比較簡單。就是根據Thread獲取ThreadLocalMap。通過ThreadLocal來獲得數據value。註意的是:如果ThreadLocalMap沒有創建,直接進入創建過程。初始化ThreadLocalMap。並直接調用和set方法一樣的方法。
11 案例:
基本案例1:
案例0:
package com.hy.threadlocal01;
public class ThreadLocalDemo0 { public static ThreadLocal<Integer> tl0 = new ThreadLocal<Integer>();
public static void main(String[] args) { System.out.println(Thread.currentThread().getName() + ":" + tl0.get()); // main:null
tl0.set(1000);
System.out.println(Thread.currentThread().getName() + ":" + tl0.get()); //main:1000 } }
|
補充案例:匿名類
public static void main(String[] args) { Emp fbb = new Emp(1, "fbb", "fbb", 40); fbb.run();
Emp lbb = new Emp(2, "lbb", "lbb", 50) { @Override public void run() { super.run(); // 調用父類的run方法 } }; lbb.run();
//new了一個類的對象,這個類是一個匿名類,但是我知道這個類繼承/實現了Emp類 Emp zjb = new Emp(3, "zjm", "zjm", 18) { @Override // 重寫父類 run方法 public void run() { System.out.println(super.getEname() + "," + super.getAge() + ",run..."); } };
zjb.run();
//和下麵這案例,不能說完全相同,只能說一模一樣 //new了一個匿名類該匿名類實現了Runnable介面 Thread t1 = new Thread(new Runnable() { @Override public void run() {
} });
//lambda表達式寫法 Thread t2 = new Thread(()-> {
}); } |
案例00:
package com.hy.threadlocal01;
public class ThreadLocalDemo00 { public static ThreadLocal<Integer> tl00 = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return 100; }; };
public static void main(String[] args) { System.out.println(Thread.currentThread().getName()+":"+tl00.get()); } } |
案例001:
package com.hy.threadlocal01;
public class ThreadLocalDemo001 { public static ThreadLocal<Integer> tl001 = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return 100; }; };
public static void main(String[] args) { tl001.set(200); System.out.println(Thread.currentThread().getName()+":"+tl001.get()); } } |
案例01:
package com.hy.threadlocal01;
public class ThreadLocalDemo01 { public static ThreadLocal<Integer> tl01 = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { System.out.println("=======begin"); return 100; }; };
public static void main(String[] args) { System.out.println(Thread.currentThread().getName() + ": ->get -> init:" + tl01.get()); tl01.set(200); // main線程改成200; System.out.println(Thread.currentThread().getName() + ": ->set -> get:" + tl01.get()); tl01.remove(); System.out.println(Thread.currentThread().getName() + ": -> remove -> get->init:" + tl01.get()); tl01.get(); System.out.println(Thread.currentThread().getName() + ": -> get:" + tl01.get()); } } |
案例011
package com.hy.threadlocal01;
public class ThreadLocalDemo011 { public static ThreadLocal<Integer> tl01 = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { System.out.println("=======begin"); return 100; }; };
public static void main(String[] args) { System.out.println(Thread.currentThread().getName()+":"+tl01.get()); tl01.set(200); //main線程改成200; System.out.println(Thread.currentThread().getName()+":"+tl01.get());
System.out.println("***********************"); new Thread() { @Override public void run() { System.out.println(Thread.currentThread().getName()+":"+tl01.get()); }; }.start(); } } |
案例0111:
package com.hy.threadlocal01;
public class ThreadLocalDemo0111 { public static ThreadLocal<Object> tl01 = new ThreadLocal<Object>() { @Override protected Object initialValue() { return new Object(); }; };
public static void main(String[] args) { final Object o1 = tl01.get(); System.out.println(Thread.currentThread().getName() + ":" + o1);
new Thread() { @Override public void run() { Object o2 = tl01.get(); System.out.println(Thread.currentThread().getName() + ":" + o2);
System.out.println(o1 == o2); }; }.start(); } }
|
案例2:
public class ThreadLocalTest05 { public static String dateToStr(int millisSeconds) { Date date = new Date(millisSeconds); SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get(); return simpleDateFormat.format(date); }
private static final ExecutorService executorService = Executors.newFixedThreadPool(100);
public static void main(String[] args) { for (int i = 0; i < 3000; i++) { int j = i; executorService.execute(() -> { String date = dateToStr(j * 1000); // 從結果中可以看出是線程安全的,時間沒有重覆的。 System.out.println(date); }); } executorService.shutdown(); } } class ThreadSafeFormatter { public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); } }; // java8的寫法,裝逼神器 // public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = // ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")); }
|
基本案例2:
案例02:
package com.hy.threadlocal02;
public class ThreadLocalDemo02 { private static ThreadLocal<Integer> tl02 = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return 0; } };
private static void add() { for (int i = 0; i < 5; i++) { // 從當前線程的ThreadLocal中獲取預設值 Integer n = tl02.get(); n += 1; // 往當前線程的ThreadLocal中設置值 tl02.set(n); System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n); } }
public static void main(String[] args) {
for (int i = 0; i < 3; i++) { new Thread(new Runnable() { @Override public void run() { add(); } }).start(); } } } |
保證每個線程都能遍歷完成,並且數據正確,其他線程不會影響當前線程的數據。
典型場景1:
通常用於保存線程不安全的工具類,典型的需要使用的類就是 SimpleDateFormat。
場景介紹
在這種場景下,每個 Thread 內都有自己的實例副本,且該副本只能由當前 Thread 訪問到並使用,相當於每個線程內部的本地變數,這也是 ThreadLocal 命名的含義。因為每個線程獨享副本,而不是公用的,所以不存在多線程間共用的問題。
我們來做一個比喻,比如飯店要做一道菜,但是有 5 個廚師一起做,這樣的話就很亂了,因為如果一個廚師已經放過鹽了,假如其他廚師都不知道,於是就都各自放了一次鹽,導致最後的菜很咸。這就好比多線程的情況,線程不安全。我們用了 ThreadLocal 之後,相當於每個廚師只負責自己的一道菜,一共有 5 道菜,這樣的話就非常清晰明瞭了,不會出現問題。
典型場景2
使用ThreadLocal的好處, 無非就是, 同一個線程無需通過方法參數傳遞變數, 因為變數是線程持有的, 所以想用就可以直接用。
業務場景的例子
一個request請求進入tomcat容器, 進入controller, 再進入service, 再進入dao, 可能還會向自定義線程池發一個非同步任務
在這麼多的類的方法中我想用某些共用的變數怎麼辦?
以 userId 為例:
- service 方法用 userId _id 判斷用戶許可權
- dao 方法用 userId 在表中存儲數據修改人的信息
- 非同步調用另一個服務 B 的時候, 讓 B 知道是誰調用了他
- 所有方法列印的 log, 我想統一加上 userId ,否則不知道是誰調用的, 但是這麼多方法改起來是是很崩潰的
以上所有方法, 如果都加上 String userId 作為參數有多醜陋不用我說大家也能想到, 即使你都加上了, 那麼以後又多了一個欄位你咋辦? 再全改一遍嗎 ?
spring的例子:
TransactionSynchronizationManager
spring的事務是可以嵌套的, 可能是10個service方法屬於一個事務, 如果沒有這個機制那麼所有方法簽名都要加上 Connection connection 作為參數
RequestContextHolder
在任何地方都可以得到 request 請求的參數, 但是這個容易濫用, 導致不同層的代碼耦合在一起, 如果你在 service 方法中用了他, 那麼你的 service 方法就無法很方便的單元測試, 因為你耦合了 http 請求的一些東西, 這本身應該是 controller 關註的
以上例子都是在一個 Thread 內是ok的, 如果新生成一個 Thread, 這些變數咋帶過去呢? 不帶過去不就失聯了嗎?
比如非同步調用發簡訊服務, 簡訊服務想知道user_id是誰, 那麼加方法參數依然是醜陋的
好在 jdk 給我們解決了一部分也就是, 如果用的是InheritableThreadLocal 那麼在new Thread()的時候會複製這些變數到新線程, 但是如果你用的線程池就搞不定了
因為線程池中的線程初期是 new Thread 可以將變數帶過去, 後期就不會 new Thread了, 而是從 pool 中直接拿一個 thread, 也就觸發不了這一步了, 因此需要用到阿裡開源的一個框架 transmittable-thread-local 來改造線程池來支持tl的變數傳遞。
=====================================================
每個線程內需要保存類似於全局變數的信息(例如在攔截器中獲取的用戶信息),可以讓不同方法直接使用,避免參數傳遞的麻煩卻不想被多線程共用(因為不同線程獲取到的用戶信息不一樣)。
例如,用 ThreadLocal 保存一些業務內容(用戶許可權信息、從用戶系統獲取到的用戶名、用戶ID 等),這些信息在同一個線程內相同,但是不同的線程使用的業務內容是不相同的。
線上程生命周期內,都通過這個靜態 ThreadLocal 實例的 get() 方法取得自己 set 過的那個對象,避免了將這個對象(如 user 對象)作為參數傳遞的麻煩
ThreadLocal的其他使用場景場景(面試加分項)
除了源碼裡面使用到ThreadLocal的場景,你自己有使用他的場景麽?一般你會怎麼用呢?
之前我們項目上線後發現部分用戶的日期居然不對了,排查下來是SimpleDataFormat的鍋,當時我們使用SimpleDataFormat的parse()方法,內部有一個Calendar對象,調用SimpleDataFormat的parse()方法會先調用Calendar.clear(),然後調用Calendar.add(),如果一個線程先調用了add()然後另一個線程又調用了clear(),這時候parse()方法解析的時間就不對了。 其實要解決這個問題很簡單,讓每個線程都new 一個自己的 SimpleDataFormat就好了,但是1000個線程難道new1000個SimpleDataFormat? 所以當時我們使用了線程池加上ThreadLocal包裝SimpleDataFormat,再調用initialValue讓每個線程有一個SimpleDataFormat的副本,從而解決了線程安全的問題,也提高了性能。 |
新建DBManager
package com.hy.db;
import java.sql.Connection; import java.sql.DriverManager;
public class DBManager { private static final String URL = "jdbc:mysql://localhost:3306/jspdb07?characterEncoding=utf8"; private static final String USER = "root"; private static final String PWD = "root";
public static Connection getConn() throws Exception { Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(URL, USER, PWD);
return conn; }
public static void main(String[] args) throws Exception { System.out.println(DBManager.getConn()); } } |
新建TransactionManagerFilter
package com.hy.filter;
import java.io.IOException;
import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter;
@WebFilter("*.do") public class TransactionManagerFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException {
}
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
}
@Override public void destroy() {
} } |
事務管理過濾器中要寫如下的代碼:開啟事務,提交事務,回滾事務
try{
conn.setAutoCommit(false); //開啟事務
chain.doFilter(req,resp);// 放行();
conn.commit(); //提交事務
}catch(Exception ex){
conn.rollback(); //回滾事務
}
將其封裝成一個類 TransactionManager
package com.hy.utils;
public class TransactionManager { // 開啟事務 public static void beginTrans() { }
// 提交事務 public static void commit() { }
// 回滾事務 public static void rollback() { } } |
現在問題的焦點來到了,如何在TranscationManager中獲取Connection對象,當然可以在方法中傳遞Connection對象,但是這是面向對象的方式。
package com.hy.utils;
import java.sql.Connection;
import com.hy.db.DBManager;
public class TranscationManager { private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
// 開啟事務 public void beginTrans() throws Exception { // 獲取Connection對象 Connection conn = threadLocal.get();
if (conn == null) { // 重新獲取connecton對象 conn = DBManager.getConn(); // 將Connection對象放在ThreadLocal操作的map中。 threadLocal.set(conn); }
// 設置不自動提交 conn.setAutoCommit(false); }
// 提交事務 public void commit() throws Exception { // 獲取Connection對象 Connection conn = threadLocal.get();
if (conn == null) { // 重新獲取connecton對象 conn = DBManager.getConn(); // 將Connection對象放在ThreadLocal操作的map中。 threadLocal.set(conn); }
conn.commit(); }
// 回滾事務 public void rollback() throws Exception { // 獲取Connection對象 Connection conn = threadLocal.get();
if (conn == null) { // 重新獲取connecton對象 conn = DBManager.getConn(); // 將Connection對象放在ThreadLocal操作的map中。 threadLocal.set(conn); } conn.rollback(); } } |
大家會發現,在這三個方法中,黃色代碼部分都是一樣的。這個代碼的目的就是獲取Connection對象。所以要想辦法將這幾句代碼放入到DBManager當中。
新DBManager
package com.hy.db;
import java.sql.Connection; import java.sql.DriverManager;
public class DBManager { private static final String URL = "jdbc:mysql://localhost:3306/jspdb07?characterEncoding=utf8"; private static final String USER = "root"; private static final String PWD = "root";
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
private static Connection createConn() throws Exception { Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(URL, USER, PWD);
return conn; }
public static Connection getConn() throws Exception { Connection conn = threadLocal.get();
if(conn == null) { conn = createConn();
threadLocal.set(conn); }
return threadLocal.get(); }
public static void closeConn() throws SQLException { Connection conn = threadLocal.get();
if(conn == null) { return; }
if(!conn.isClosed()) { conn.close(); threadLocal.set(null); } } public static void main(String[] args) throws Exception { System.out.println(DBManager.getConn()); } } |
TranscationManager
package com.hy.utils;
import com.hy.db.DBManager;
public class TranscationManager { // 開啟事務 public void beginTrans() throws Exception { DBManager.getConn().setAutoCommit(false); }
// 提交事務 public void commit() throws Exception { DBManager.getConn().commit(); }
// 回滾事務 public void rollback() throws Exception { DBManager.getConn().rollback(); } } |
新TransactionManager
package com.hy.utils;
import com.hy.db.DBManager;
public class TransactionManager { // 開啟事務 public static void beginTrans() throws Exception { DBManager.getConn().setAutoCommit(false); }
// 提交事務 public static void commit() throws Exception { DBManager.getConn().commit(); DBManager.closeConn(); }
// 回滾事務 public static void rollback() throws Exception { DBManager.getConn().rollback(); DBManager.closeConn(); } } |
部分源碼:
ThreadLocal解析:
是ThreadLocal的類圖結構,從圖中可知:Thread類中有兩個變數threadLocals和inheritableThreadLocals,二者都是ThreadLocal內部類ThreadLocalMap類型的屬性。
我們通過查看內部內ThreadLocalMap可以發現實際上它類似於一個HashMap。
在預設情況下,每個線程對象都有兩個屬性,但是這兩個屬性量都為null
只有當線程第一次調用ThreadLocal的set或者get方法的時候才會創建他們(後面我們會查看這兩個方法的源碼)。
除此之外,和我所想的不同的是,每個線程的本地變數的值 不是存放在ThreadLocal對象中,而是放在調用的線程對象的threadLocals屬性裡面(前面也說過,threadLocals是Thread類的屬性)。也就是說,
ThreadLocal類 其實相當於一個 管家一樣(所謂的工具人),只是用來 存值/取值 的,但是 存的值/取的值都來自於 當前線程對象里 threadLocals屬性,而這個屬性是一個類似於Map的結構。
我們通過調用ThreadLocal的set方法將value值 添加到調用線程的threadLocals中,
通過調用ThreadLocal的get方法,它能夠從它的當前線程的threadLocals中取出該值。
如果調用線程一直不終止,那麼這個值(本地變數的值)將會一直存放在當前線程對象的threadLocals中。
當不使用本地變數的時候(也就是那個值時),需要只調用工具人ThreadLocal的 remove方法將其從當前線程對象的threadLocals中刪除即可。
下麵我們通過查看ThreadLocal的set、get以及remove方法來查看ThreadLocal具體實怎樣工作的
1、解析:
每個線程內部有一個名為threadLocals的屬性,該屬性的類型為ThreadLocal.ThreadLocalMap類型(類似於一個HashMap),其中的key為當前定義的ThreadLocal變數的this引用,value為我們使用set方法設置的值。每個線程的本地變數存放在自己的本地記憶體變數threadLocals中,如果當前線程一直不消亡,那麼這些本地變數就會一直存在(所以可能會導致記憶體溢出),因此使用完畢需要將其remove掉。
2、set方法源碼
public void set(T value) { //(1)獲取當前線程(調用者線程) Thread t = Thread.currentThread(); //(2)以當前線程作為key值,去查找對應的線程變數,找到對應的map ThreadLocalMap map = getMap(t); //(3)如果map不為null,就直接添加本地變數,key為當前定義的ThreadLocal變數的this引用,值為添加的本地變數值 if (map != null) map.set(this, value); //(4)如果map為null,說明首次添加,需要首先創建出對應的map else createMap(t, value); } |
在上面的代碼中,(2)處調用getMap方法獲得當前線程對應的threadLocals(參照上面的圖示和文字說明),該方法代碼如下
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //獲取線程自己的變數threadLocals,並綁定到當前調用線程的成員變數threadLocals上
}
如果調用getMap方法返回值不為null,就直接將value值設置到threadLocals中(key為當前線程引用,值為本地變數);如果getMap方法返回null說明是第一次調用set方法(前面說到過,threadLocals預設值為null,只有調用set方法的時候才會創建map),這個時候就需要調用createMap方法創建threadLocals,該方法如下所示