大綱
1.ThreadLocal的特點介紹
2.ThreadLocal的使用案例
3.ThreadLocal的內部結構
4.ThreadLocal的核心方法源碼
5.ThreadLocalMap的核心方法源碼
6.ThreadLocalMap的原理總結
1.ThreadLocal的特點介紹
(1)ThreadLocal的注釋說明
(2)ThreadLocal的常用方法
(3)ThreadLocal的使用案例
(4)ThreadLocal類與synchronized關鍵字對比
(1)ThreadLocal的注釋說明
//This class provides thread-local variables.
//These variables differ from their normal counterparts in that
//each thread that accesses one (via its get or set method) has its own,
//independently initialized copy of the variable.
//ThreadLocal instances are typically private static fields in classes that
//wish to associate state with a thread (e.g., a user ID or Transaction ID).
public class ThreadLocal<T> {......
}
ThreadLocal類可以提供線程內部的局部變量,這種變量在多線程環境下訪問(通過get和set方法訪問)時,能保證各個線程的變量相對獨立于其他線程內的變量。ThreadLocal實例通常是private static類型,用于關聯線程和線程上下文。
由此可知,ThreadLocal的作用是:提供線程內的局部變量,不同的線程之間不會相互干擾。由于這種變量只會在線程的生命周期內起作用,所以可以減少同一個線程內,在多個函數或者組件之間傳遞數據的復雜度。
ThreadLocal的總結如下:
一.線程并發:適用于多線程并發的場景
二.傳遞數據:可通過ThreadLocal在同一線程,不同組件中傳遞公共變量
三.線程隔離:每個線程的變量都是獨立的,互相之間不會影響
(2)ThreadLocal的常用方法
ThreadLocal的常用方法如下:
(3)ThreadLocal的使用案例
一.沒用ThreadLocal時共享變量在線程間不隔離
//需求:線程隔離
//在多線程并發的場景下, 每個線程中的變量都是相互獨立
//線程A:設置(變量1) 獲取(變量1)
//線程B:設置(變量2) 獲取(變量2)
public class MyDemo {//變量private String content;private String getContent() {return content;}private void setContent(String content) {this.content = content;}public static void main(String[] args) {MyDemo demo = new MyDemo();for (int i = 0; i < 5; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {//每個線程: 先存一個變量, 過一會再取出這個變量demo.setContent(Thread.currentThread().getName() + "的數據");System.out.println("-----------------------");System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());}});thread.setName("線程" + i);thread.start();//啟動線程}}
}
運行后打印結果如下,從結果可以看出多個線程在訪問同一個變量的時候出現了異常,有些線程取出了其他線程的數據,線程之間的數據沒有實現隔離。
-----------------------
-----------------------
線程0--->線程3的數據
-----------------------
-----------------------
線程2--->線程4的數據
-----------------------
線程4--->線程4的數據
線程3--->線程3的數據
線程1--->線程4的數據
二.使用ThreadLocal實現共享變量在線程間隔離
//需求:線程隔離
//在多線程并發的場景下, 每個線程中的變量都是相互獨立
//線程A:設置(變量1) 獲取(變量1)
//線程B:設置(變量2) 獲取(變量2)
//ThreadLocal:
//1.set(): 將變量綁定到當前線程中
//2.get(): 獲取當前線程綁定的變量
public class MyDemo {private static ThreadLocal<String> tl = new ThreadLocal<>();//變量private String content;private String getContent() {return tl.get();}private void setContent(String content) {//變量content綁定到當前線程tl.set(content);}public static void main(String[] args) {MyDemo demo = new MyDemo();for (int i = 0; i < 5; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {demo.setContent(Thread.currentThread().getName() + "的數據");System.out.println("-----------------------");System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());}});thread.setName("線程" + i);thread.start();//啟動線程}}
}
運行后打印結果如下,從結果看,很好地解決了多線程之間數據隔離的問題。
-----------------------
線程0--->線程0的數據
-----------------------
線程1--->線程1的數據
-----------------------
線程2--->線程2的數據
-----------------------
線程3--->線程3的數據
-----------------------
線程4--->線程4的數據
(4)ThreadLocal類與synchronized關鍵字對比
一.synchronized同步方式
上述線程隔離的效果完全可以通過加synchronized鎖來實現。
//需求:線程隔離
//在多線程并發的場景下, 每個線程中的變量都是相互獨立
//線程A:設置(變量1) 獲取(變量1)
//線程B:設置(變量2) 獲取(變量2)
public class MyDemo {//變量private String content;private String getContent() {return content;}private void setContent(String content) {this.content = content;}public static void main(String[] args) {MyDemo demo = new MyDemo();for (int i = 0; i < 5; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {//每個線程: 存一個變量, 過一會再取出這個變量synchronized (MyDemo.class){demo.setContent(Thread.currentThread().getName() + "的數據");System.out.println("-----------------------");System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());}}});thread.setName("線程" + i); //線程0~4thread.start();}}
}
運行后打印結果如下:
-----------------------
線程0--->線程0的數據
-----------------------
線程1--->線程1的數據
-----------------------
線程2--->線程2的數據
-----------------------
線程3--->線程3的數據
-----------------------
線程4--->線程4的數據
從結果可以發現,加鎖確實可以解決這個問題。但是這里強調的是多線程數據隔離的問題,并不是多線程共享數據的問題。在這個案例中使用synchronized關鍵字是不合適的,降低了并發性。
二.ThreadLocal與synchronized的區別
雖然ThreadLocal模式與synchronized關鍵字,都可以用于處理多線程并發訪問變量的問題,不過兩者處理問題的角度和思路不同。
三.總結
雖然使用ThreadLocal和synchronized都能解決線程間數據隔離的問題,但是ThreadLocal更為合適,因為它可以使程序擁有更高的并發性。
2.ThreadLocal的使用案例
(1)轉賬案例的場景
(2)轉賬案例引入事務
(3)常規方案解決引入事務后的問題
(4)ThreadLocal方案解決引入事務后的問題
(5)ThreadLocal的應用場景總結
(1)轉賬案例的場景
有一個數據表account,里面有兩個用戶Jack和Rose,Jack給Rose轉賬。案例的實現使用了MySQL數據庫、JDBC和C3P0框架,以下是詳細的代碼。
一.數據準備
--使用數據庫
use test;
--創建一張賬戶表
create table account(id int primary key auto_increment,name varchar(20),money double
);
-- 初始化數據
insert into account values(null, 'Jack', 1000);
insert into account values(null, 'Rose', 0);
二.C3P0配置文件和工具類
<c3p0-config><!-- 使用默認的配置讀取連接池對象 --><default-config><!-- 連接參數 --><property name="driverClass">com.mysql.jdbc.Driver</property><property name="jdbcUrl">jdbc:mysql://localhost:3306/test</property><property name="user">root</property><property name="password">123456</property><!-- 連接池參數 --><property name="initialPoolSize">5</property><property name="maxPoolSize">10</property><property name="checkoutTimeout">3000</property></default-config>
</c3p0-config>
三.JdbcUtils工具類
public class JdbcUtils {//c3p0數據庫連接池對象屬性private static final ComboPooledDataSource ds = new ComboPooledDataSource();//從數據庫連接池中獲取一個連接public static Connection getConnection() throws SQLException {return ds.getConnection();}//釋放資源public static void release(AutoCloseable... ios) {for (AutoCloseable io : ios) {if (io != null) {try {io.close();} catch (Exception e) {e.printStackTrace();}}}} public static void commitAndClose(Connection conn) {try {if (conn != null) {//提交事務conn.commit();//釋放連接conn.close();}} catch (SQLException e) {e.printStackTrace();}}public static void rollbackAndClose(Connection conn) {try {if (conn != null) {//回滾事務conn.rollback();//釋放連接conn.close();}} catch (SQLException e) {e.printStackTrace();}}
}
四.Dao層代碼:AccountDao
public class AccountDao {public void out(String outUser, int money) throws SQLException {String sql = "update account set money = money - ? where name = ?";Connection conn = JdbcUtils.getConnection();PreparedStatement pstm = conn.prepareStatement(sql);pstm.setInt(1,money);pstm.setString(2,outUser);pstm.executeUpdate();JdbcUtils.release(pstm,conn);}public void in(String inUser, int money) throws SQLException {String sql = "update account set money = money + ? where name = ?";Connection conn = JdbcUtils.getConnection();PreparedStatement pstm = conn.prepareStatement(sql);pstm.setInt(1,money);pstm.setString(2,inUser);pstm.executeUpdate();JdbcUtils.release(pstm,conn);}
}
五.Service層代碼:AccountService
public class AccountService {public boolean transfer(String outUser, String inUser, int money) {AccountDao ad = new AccountDao();try {//轉出ad.out(outUser, money);//轉入ad.in(inUser, money);} catch (Exception e) {e.printStackTrace();return false;}return true;}
}
六.Web層代碼:AccountWeb
public class AccountWeb {public static void main(String[] args) {//模擬數據 : Jack給Rose轉賬100String outUser = "Jack";String inUser = "Rose";int money = 100;AccountService as = new AccountService();boolean result = as.transfer(outUser, inUser, money);if (result == false) {System.out.println("轉賬失敗!");} else {System.out.println("轉賬成功!");}}
}
(2)轉賬案例引入事務
案例中的轉賬涉及兩個DML操作:一個轉出,一個轉入。這兩個操作需要具備原子性,否則可能會出現數據修改異常。所以需要引入事務來保證轉出和轉入操作具備原子性,也就是要么都同時成功,要么都同時失敗。
引入事務改造前:
public class AccountService {public boolean transfer(String outUser, String inUser, int money) {AccountDao ad = new AccountDao();try {//轉出ad.out(outUser, money);//模擬轉賬過程中的異常:轉出成功,轉入失敗int i = 1/0;//轉入ad.in(inUser, money);} catch (Exception e) {e.printStackTrace();return false;}return true;}
}
引入事務改造如下:
public class AccountService {public boolean transfer(String outUser, String inUser, int money) {AccountDao ad = new AccountDao();Connection conn = null;try {//1.開啟事務conn = JdbcUtils.getConnection();conn.setAutoCommit(false);//禁用事務自動提交(改為手動)//轉出ad.out(conn, outUser, money);//模擬轉賬過程中的異常:轉出成功,轉入失敗int i = 1/0;//轉入ad.in(conn, inUser, money);//2.事務提交JdbcUtils.commitAndClose(conn);} catch (Exception e) {e.printStackTrace();//3.失敗時事務回滾JdbcUtils.rollbackAndClose(conn);return false;}return true;}
}
JDBC開啟事務的注意點:
為了保證所有操作在一個事務中,轉賬時使用的Connection必須是同一個。Service層開啟事務的Connection要和Dao層訪問數據庫的Connection一致。線程并發情況下,每個線程只能操作各自從JdbcUtils中獲取的Connection。
(3)常規方案解決引入事務后的問題
一.常規方案的實現
基于上面給出的前提, 常規方案是:傳參 + 加鎖。
傳參:從Service層將Connection對象向Dao層傳遞
加鎖:防止多個線程并發操作從JdbcUtils中獲取的是同一個Connection
AccountService類修改如下:
//事務的使用注意點:
//1.Service層和Dao層的連接對象保持一致
//2.每個線程的Connection對象必須前后一致, 線程隔離
//常規的解決方案
//1.傳參: 將Service層的Connection對象直接傳遞到Dao層
//2.加鎖: 防止多個線程并發操作從JdbcUtils中獲取的同一個Connection
//常規解決方案的弊端:
//1.代碼耦合度高
//2.降低程序性能
public class AccountService {public boolean transfer(String outUser, String inUser, int money) {AccountDao ad = new AccountDao();//線程并發情況下,為了保證每個線程使用各自的Connection,避免不同的線程修改同一個Connection,故加鎖synchronized (AccountService.class) {Connection conn = null;try {//1.開啟事務conn = JdbcUtils.getConnection();//從線程池中獲取一個Connection對象conn.setAutoCommit(false);//禁用事務自動提交(改為手動)//轉出ad.out(conn, outUser, money);//模擬轉賬過程中的異常:轉出成功,轉入失敗int i = 1/0;//轉入ad.in(conn, inUser, money);//2.事務提交JdbcUtils.commitAndClose(conn);} catch (Exception e) {e.printStackTrace();//3.失敗時事務回滾JdbcUtils.rollbackAndClose(conn);return false;}return true;}}
}
AccountDao類修改如下:
注意:Connection不能在Dao層釋放,否則Service層就無法使用了。
public class AccountDao {public void out(Connection conn, String outUser, int money) throws SQLException{String sql = "update account set money = money - ? where name = ?";//注釋從連接池獲取連接的代碼,使用從Service中傳遞過來的Connection//Connection conn = JdbcUtils.getConnection();PreparedStatement pstm = conn.prepareStatement(sql);pstm.setInt(1,money);pstm.setString(2,outUser);pstm.executeUpdate();//連接不能在這里釋放,Service層中還需要使用//JdbcUtils.release(pstm,conn);JdbcUtils.release(pstm);}public void in(Connection conn, String inUser, int money) throws SQLException {String sql = "update account set money = money + ? where name = ?";//Connection conn = JdbcUtils.getConnection();PreparedStatement pstm = conn.prepareStatement(sql);pstm.setInt(1,money);pstm.setString(2,inUser);pstm.executeUpdate();//連接不能在這里釋放,Service層中還需要使用//JdbcUtils.release(pstm,conn);JdbcUtils.release(pstm);}
}
二.常規方案的弊端
上述的確按要求解決了問題,但是存在如下弊端:
第一.直接從Service層傳遞Connection到Dao層,代碼耦合度比較高
第二.加鎖會降低程序并發性,程序性能下降
(4)ThreadLocal方案解決引入事務后的問題
一.ThreadLocal方案的實現
像這種需要進行數據傳遞和線程隔離的場景,可以使用ThreadLocal來解決。
JdbcUtils工具類加入ThreadLocal:
public class JdbcUtils {//ThreadLocal對象: 將Connection綁定在當前線程中private static final ThreadLocal<Connection> tl = new ThreadLocal();//c3p0數據庫連接池對象屬性private static final ComboPooledDataSource ds = new ComboPooledDataSource();//獲取連接//原來: 直接從連接池中獲取連接//現在: //1.直接獲取當前線程綁定的連接對象//2.如果連接對象是空的,再去連接池中獲取連接,并將此連接對象跟當前線程進行綁定public static Connection getConnection() throws SQLException {//取出當前線程綁定的connection對象Connection conn = tl.get();if (conn == null) {//如果沒有,則從連接池中取出conn = ds.getConnection();//再將connection對象綁定到當前線程中tl.set(conn);}return conn;}//釋放資源public static void release(AutoCloseable... ios) {for (AutoCloseable io : ios) {if (io != null) {try {io.close();} catch (Exception e) {e.printStackTrace();}}}}public static void commitAndClose() {try {Connection conn = getConnection();//提交事務conn.commit();//解除綁定tl.remove();//釋放連接conn.close();} catch (SQLException e) {e.printStackTrace();}}public static void rollbackAndClose() {try {Connection conn = getConnection();//回滾事務conn.rollback();//解除綁定tl.remove();//釋放連接conn.close();} catch (SQLException e) {e.printStackTrace();}}
}
AccountService類不需要傳遞Connection對象:
public class AccountService {public boolean transfer(String outUser, String inUser, int money) {AccountDao ad = new AccountDao();try {//1.開啟事務conn = JdbcUtils.getConnection();conn.setAutoCommit(false);//禁用事務自動提交(改為手動)//轉出:這里不需要傳參conn了ad.out(outUser, money);//模擬轉賬過程中的異常:轉出成功,轉入失敗int i = 1/0;//轉入ad.in(inUser, money);//2.事務提交JdbcUtils.commitAndClose(conn);} catch (Exception e) {e.printStackTrace();//3.失敗時事務回滾JdbcUtils.rollbackAndClose(conn);return false;}return true;}
}
AccountDao類去掉Connection參數:
public class AccountDao {public void out(String outUser, int money) throws SQLException {String sql = "update account set money = money - ? where name = ?";Connection conn = JdbcUtils.getConnection();PreparedStatement pstm = conn.prepareStatement(sql);pstm.setInt(1,money);pstm.setString(2,outUser);pstm.executeUpdate();JdbcUtils.release(pstm);}public void in(String inUser, int money) throws SQLException {String sql = "update account set money = money + ? where name = ?";Connection conn = JdbcUtils.getConnection();PreparedStatement pstm = conn.prepareStatement(sql);pstm.setInt(1,money);pstm.setString(2,inUser);pstm.executeUpdate();JdbcUtils.release(pstm);}
}
二.ThreadLocal方案的好處
從上述可以看到,ThreadLocal方案有兩個優勢:
優勢一:傳遞數據
保存每個線程綁定的數據,在需要的地方可以直接獲取,避免參數直接傳遞帶來的代碼耦合問題。
優勢二:線程隔離
各線程之間的數據相互隔離卻又具備并發性,避免同步方式帶來的性能損失。
(5)ThreadLocal的典型應用場景
場景一:
在TransactionSynchronizationManager類中(這是Spring-JDBC的類),會通過ThreadLocal來保證數據庫連接和事務資源的隔離性,從而避免了不同線程之間事務和連接混亂的問題。
場景二:
在實際開發中,當用戶登錄之后,攔截器會獲得用戶的基本信息。這些信息在后續的方法中會用到,如果設置到HttpServletRequest中,則不是很靈活,而且還依賴服務器對象,這時就可以用ThreadLocal。
3.ThreadLocal的內部結構
(1)早期的設計
(2)現在的設計
(3)現在的設計的優勢
(1)早期的設計
如果不去看源碼,可能會猜測ThreadLocal是如下這樣設計的:
首先每個ThreadLocal都創建一個Map,然后用線程作為Map的key,線程的變量副本作為Map的value,這樣就能讓各個線程的變量副本實現數據隔離的效果。
這是最簡單的設計方法,JDK最早期的ThreadLocal確實是這樣設計的。
(2)現在的設計
JDK后面優化了設計方案,在JDK8中ThreadLocal的設計是:
首先讓每個Thread創建一個ThreadLocalMap,這個Map的key是ThreadLocal實例本身,value才是真正要存儲的局部變量。
具體設計如下:
一.每個Thread線程內部都有一個Map(ThreadLocalMap)
二.Map里面存儲ThreadLocal對象(key)和線程的變量副本(value)
三.Thread內部的Map由ThreadLocal維護,ThreadLocal會向Map獲取和設置線程的變量副本
四.其他線程不能獲取當前線程的變量副本,從而實現了數據隔離
(3)現在的設計的優勢
優勢一:
每個Map存儲的Entry數量變少,可以降低Hash沖突發生的概率。因為之前的存儲數量由Thread的數量決定,現在由ThreadLocal的數量決定。在實際中,ThreadLocal的數量往往要少于Thread的數量。
優勢二:
當Thread銷毀后,對應的ThreadLocalMap也隨之銷毀。讓線程變量副本的生命周期跟隨著線程的生命周期,可以減少內存的占用。
4. ThreadLocal的核心方法源碼
(1)ThreadLocal的整體設計原理
(2)ThreadLocal的set()方法源碼
(3)ThreadLocal的get()方法源碼
(4)ThreadLocal的initialValue()方法源碼
(1)ThreadLocal的整體設計原理
ThreadLocal為實現多個線程對同一個共享變量進行set操作時線程隔離,每個線程都有一個與ThreadLocal關聯的容器來存儲共享變量的初始化副本。當線程對變量副本更新時,只更新存儲在當前線程關聯容器中的數據副本。
如下圖示,在每個線程中都會維護一個成員變量ThreadLocalMap。其中key是一個指向ThreadLocal實例的弱引用,而value表示ThreadLocal的初始化值或者當前線程執行set()方法設置的值。
假設定義了3個不同功能的ThreadLocal共享變量,而在Thread1中分別用到了這3個ThreadLocal進行操作,那么這3個ThreadLocal都會存儲到Thread1的ThreadLocalMap中。
如果Thread2也想用這3個ThreadLocal共享變量,那么在Thread2中也會維護一個ThreadLocalMap,把這3個ThreadLocal共享變量保存到該ThreadLocalMap中。
如果Thread1想要對local1進行運算,則將local1實例作為key,從Thread1的ThreadLocalMap中獲取對應的value值,進行運算即可。
(2)ThreadLocal的set()方法源碼
該方法會在當前線程的成員變量ThreadLocalMap中設置一個值。
具體的執行流程如下:
一.首先通過Thread的currentThread()方法獲取當前線程。
二.然后通過ThreadLocal的getMap()方法獲取當前線程的成員變量ThreadLocalMap。
三.如果獲取的ThreadLocalMap為空,于是調用createMap()方法初始化當前線程的成員變量ThreadLocalMap。
四.如果獲取的ThreadLocalMap不為空,則更新ThreadLocalMap中key為當前ThreadLocal對象所對應的value。
public class ThreadLocal<T> {...//在當前線程中設置一個值,并保存在該線程的ThreadLocalMap中public void set(T value) {//首先通過Thread.currentThread()獲取當前線程Thread t = Thread.currentThread();//然后獲取當前線程的成員變量ThreadLocalMapThreadLocalMap map = getMap(t);//判斷當前線程的成員變量ThreadLocalMap是否為空if (map != null) {//如果不為空,則調用map.set()更新ThreadLocalMap中key為當前ThreadLocal對象所對應的valuemap.set(this, value);} else {//如果為空,則調用createMap()方法初始化當前線程的成員變量ThreadLocalMapcreateMap(t, value);}}//獲取線程Thread的成員變量ThreadLocalMap ThreadLocalMap getMap(Thread t) {return t.threadLocals;}//初始化線程Thread的成員變量ThreadLocalMapvoid createMap(Thread t, T firstValue) {//這里的this是調用此方法的threadLocal對象//初始化ThreadLocalMap的第一個元素,key為調用此方法的threadLocal對象,value為傳入的firstValuet.threadLocals = new ThreadLocalMap(this, firstValue);}...
}public class Thread implements Runnable {...//每個線程都有一個ThreadLocalMap類型的成員變量,叫threadLocalsThreadLocal.ThreadLocalMap threadLocals = null;...
}
(3)ThreadLocal的get()方法源碼
該方法會從當前線程的成員變量ThreadLocalMap中獲取一個值。
具體的執行流程如下:
一.首先通過Thread的currentThread()方法獲取當前線程。
二.然后通過ThreadLocal的getMap()方法獲取當前線程的成員變量ThreadLocalMap。
三.如果獲取的ThreadLocalMap不為空,而且key為當前ThreadLocal對象所對應的value值也不為空,則返回該value。
四.否則就調用setInitialValue()方法,初始化當前線程的成員變量ThreadLocalMap,或者初始化ThreadLocalMap中key為當前ThreadLocal對象所對應的value值。
public class ThreadLocal<T> {...//從當前線程的成員變量ThreadLocalMap中獲取一個值//如果當前線程的成員變量ThreadLocalMap為空,則調用setInitialValue()方法進行初始化public T get() {//獲取當前線程Thread t = Thread.currentThread();//獲取當前線程的成員變量ThreadLocalMapThreadLocalMap map = getMap(t);//如果當前線程的成員變量ThreadLocalMap不為空if (map != null) {//以當前ThreadLocal對象為key,//調用當前線程的成員變量ThreadLocalMap的getEntry()方法,來獲取對應的Entry對象ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//如果當前線程的成員變量ThreadLocalMap為空,//或者ThreadLocalMap中key為當前ThreadLocal對象所對應的value為空//那么就需要進行初始化return setInitialValue();}//進行初始化并返回key為當前ThreadLocal對象所對應的value//該方法通過initialValue()方法獲取初始值來初始化當前線程的成員變量ThreadLocalMap并賦值//如下兩種情況需要進行初始化://第一種情況: map不存在,表示當前線程的成員變量ThreadLocalMap還沒初始化//第二種情況: map存在, 但是key為當前ThreadLocal對象所對應的value為空private T setInitialValue() {//調用initialValue()方法獲取初始化的值T value = initialValue();//獲取當前線程對象Thread t = Thread.currentThread();//獲取當前線程的成員變量ThreadLocalMapThreadLocalMap map = getMap(t);//判斷當前線程的成員變量ThreadLocalMap是否為空if (map != null) {//如果不為空,則調用map.set()更新ThreadLocalMap中key為當前ThreadLocal對象所對應的valuemap.set(this, value);} else {//如果為空,則調用createMap()方法初始化當前線程的成員變量ThreadLocalMapcreateMap(t, value);}//返回初始化的valuereturn value;}...
}
(4)ThreadLocal的initialValue()方法源碼
在set()方法還未調用而先調用get()方法時,就會執行initialValue()方法。該方法會返回當前線程的成員變量ThreadLocalMap中,key為當前ThreadLocal對象所對應的value的初始值。initialValue()方法默認情況下會返回一個null,但可以重寫覆蓋此方法。
public class ThreadLocal<T> {...//返回當前線程的成員變量ThreadLocalMap中,key為當前ThreadLocal對象所對應的value的初始值protected T initialValue() {return null;}...
}
5.ThreadLocalMap的核心方法源碼
(1)ThreadLocalMap的初始化方法
(2)ThreadLocalMap的set()方法
(3)ThreadLocalMap的弱引用
(1)ThreadLocalMap的初始化方法
ThreadLocal的createMap()方法會初始化一個ThreadLocalMap集合。
ThreadLocalMap的構造方法主要會進行如下處理:首先初始化一個長度為16的Entry數組,然后通過對firstKey的hashCode進行位運算取模來得到一個數組下標i,接著根據firstKey和firstValue封裝一個Entry對象,最后將這個Entry對象保存到Entry數組的下標為i的位置中。
public class ThreadLocal<T> {...//初始化線程Thread的成員變量ThreadLocalMapvoid createMap(Thread t, T firstValue) {//這里的this是調用此方法的threadLocal對象//初始化ThreadLocalMap的第一個元素,key為調用此方法的threadLocal對象,value為傳入的firstValuet.threadLocals = new ThreadLocalMap(this, firstValue);}//ThreadLocalMap is a customized hash map suitable only for maintaining thread local values.//No operations are exported outside of the ThreadLocal class. //The class is package private to allow declaration of fields in class Thread.//To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. //However, since reference queues are not used, //stale entries are guaranteed to be removed only when the table starts running out of space.static class ThreadLocalMap {//The entries in this hash map extend WeakReference, //using its main ref field as the key (which is always a ThreadLocal object).//Note that null keys (i.e. entry.get() == null) mean that the key is no longer referenced, //so the entry can be expunged from table.//Such entries are referred to as "stale entries" in the code that follows.static class Entry extends WeakReference<ThreadLocal<?>> {//The value associated with this ThreadLocal.Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}//The initial capacity -- MUST be a power of two.private static final int INITIAL_CAPACITY = 16;//The table, resized as necessary.//table.length MUST always be a power of two.private Entry[] table;//The number of entries in the table.private int size = 0;ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//初始化一個長度為16的Entry數組table = new Entry[INITIAL_CAPACITY];//通過對firstKey這個ThreadLocal實例對象的hashCode,進行位運算取模,來得到一個數組下標iint i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//將firstKey和firstValue封裝成一個Entry對象,保存到數組的下標為i的位置table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}...}...
}public class Thread implements Runnable {...//每個線程都有一個ThreadLocalMap類型的成員變量,叫threadLocalsThreadLocal.ThreadLocalMap threadLocals = null;...
}
(2)ThreadLocalMap的set()方法
在ThreadLocal的set()方法中,如果發現當前線程的成員變量ThreadLocalMap已經初始化了,那么就會調用ThreadLocalMap的set()方法來保存要設置的值。
在ThreadLocalMap的set(key, value)方法中,首先根據ThreadLocal對象的hashCode和數組長度進行位與運算(即取模),來獲取set()方法要設置的元素,應該放置在數組的哪個位置(即數組下標i)。
如果數組下標i的位置不存在Entry對象,則直接將key和value封裝成一個新的Entry對象然后存儲到數組的下標為i的這個位置。
如果數組下標i的位置存在Entry對象,則使用for循環從數組下標i位置開始往后遍歷(線性探索解決Hash沖突)。
如果根據key計算出來的數組下標i已經存在其他的value,且該位置的key和要設置的key不同,則繼續尋找i + 1的位置進行存儲。
如果根據要設置的key找出的數組對應位置的Entry元素的key為null,則調用replaceStaleEntry()方法來進行替換和清理。
因為Entry元素中的key是弱引用,有可能ThreadLocal實例被回收了導致Entry元素中的key為null。
最后統計數組的元素個數,如果元素個數超出閾值則進行擴容。
public class ThreadLocal<T> {...static class ThreadLocalMap {//The entries in this hash map extend WeakReference, //using its main ref field as the key (which is always a ThreadLocal object).//Note that null keys (i.e. entry.get() == null) mean that the key is no longer referenced, //so the entry can be expunged from table.//Such entries are referred to as "stale entries" in the code that follows.static class Entry extends WeakReference<ThreadLocal<?>> {//The value associated with this ThreadLocal.Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}//The initial capacity -- MUST be a power of two.private static final int INITIAL_CAPACITY = 16;//The table, resized as necessary.//table.length MUST always be a power of two.private Entry[] table;//The number of entries in the table.private int size = 0;ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//初始化一個長度為16的Entry數組table = new Entry[INITIAL_CAPACITY];//通過對firstKey這個ThreadLocal實例對象的hashCode,進行位運算取模,來得到一個數組下標iint i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//將firstKey和firstValue封裝成一個Entry對象,保存到數組的下標為i的位置table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}//Set the value associated with key.//@param key the thread local object//@param value the value to be setprivate void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;//首先根據ThreadLocal對象的hashCode和數組長度進行位與運算(即取模),來獲取元素放置的位置(即數組下標)int i = key.threadLocalHashCode & (len-1);//然后從i開始往后遍歷到數組最后一個Entry(線性探索)for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {//獲取Entry元素中的keyThreadLocal<?> k = e.get();//如果key相等,則覆蓋valueif (k == key) {e.value = value;return;}//如果key為null,則用新key、value覆蓋//同時清理key = null的陳舊數據(弱引用)if (k == null) {replaceStaleEntry(key, value, i);return;}}//如果數組下標i的位置不存在數據,則直接將key和value封裝成Entry對象存儲到該位置tab[i] = new Entry(key, value);int sz = ++size;//如果超過閾值,就需要擴容了,cleanSomeSlots()方法會清理數組中的無效的keyif (!cleanSomeSlots(i, sz) && sz >= threshold) {rehash();//擴容}}private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}...}...
}
(3)ThreadLocalMap的弱引用
ThreadLocalMap的數組table中的Entry元素的key什么時候會為空?
當線程已經退出 + ThreadLocal已失去了線程的引用 + 垃圾回收時,key便會為空。也就是如果ThreadLocal對象被回收了,那么ThreadLocalMap中的key就會為空。
public class ThreadLocal<T> {...static class ThreadLocalMap {private Entry[] table;static class Entry extends WeakReference<ThreadLocal<?>> {//The value associated with this ThreadLocal.Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}...}
}
在JVM進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。如下代碼中,定義了一個Object對象和一個引用了Object對象的弱引用對象。當把object變量設置為null后,也就是object變量不再引用這個Object對象了,那么gc()方法便會對這個Object對象進行回收,盡管它被弱引用對象引用著。
public class WeakReferneceExample {//object變量引用了新創建的Object對象static Object object = new Object();public static void main(String[] args) {//objectWeakReference變量引用了新創建的WeakReference對象//但WeakReference對象是通過傳入object變量創建的,所以objectWeakReference變量其實引用了Object對象WeakReference<Object> objectWeakReference = new WeakReference<>(object);object = null;//object變量不再引用Object對象,但此時objectWeakReference還在引用Object對象System.gc();//此時垃圾回收,會回收還在被弱引用引用著的Object對象System.out.println("gc之后" + objectWeakReference.get());//會輸出null}
}
如果在強引用的情況下,執行gc()方法時,Object對象是不會被回收的。
public class StrongReferenceExample {static Object object = new Object();//object變量引用了新創建的Object對象public static void main(String[] args) {Object strongRef = object;//strongRef變量也引用新創建的Object對象object = null;//object變量不再引用Object對象,但此時strongRef變量還在引用新創建的Object對象System.gc();//此時垃圾回收,不會回收還在被強引用引用著的Object對象System.out.println("gc之后" + strongRef);//會輸出Object對象}
}
6.ThreadLocalMap的原理總結
(1)ThreadLocalMap的基本結構
(2)ThreadLocalMap的成員變量
(3)ThreadLocalMap的存儲結構Entry
(4)弱引用和內存泄漏
(5)如果ThreadLocalMap的key使用強引用
(6)如果ThreadLocalMap的key使用弱引用
(7)使用ThreadLocal出現內存泄漏的原因
(8)ThreadLocalMap為什么使用弱引用
(9)使用線性探測法解決Hash沖突
(1)ThreadLocalMap的基本結構
ThreadLocalMap是ThreadLocal的內部類,它沒有實現Map接口,而是用獨立的方式實現了Map的功能,它內部的Entry也是用獨立的方式實現的。
(2)ThreadLocalMap的成員變量
table是一個Entry類型的數組,用于存儲數據。
size代表table數組中的元素個數。
threshold代表需要擴容時size的閾值。
public class ThreadLocal<T> {...static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}//初始容量 —— 必須是2的整次冪private static final int INITIAL_CAPACITY = 16;//存放數據的table,同樣,數組長度必須是2的整次冪private Entry[] table;//數組里面Entry的個數,可以用于判斷table當前使用量是否超過閾值private int size = 0;//進行數組擴容的閾值private int threshold; // Default to 0...}...
}
(3)ThreadLocalMap的存儲結構Entry
在ThreadLocalMap中,也是用Entry來保存K-V結構數據的,不過Entry中的key只能是ThreadLocal類型的對象。
另外Entry繼承自WeakReference,也就是key(ThreadLocal對象)是弱引用,使用弱引用的目的是將ThreadLocal對象的生命周期和線程的生命周期進行解綁。
public class ThreadLocal<T> {...static class ThreadLocalMap {//Entry繼承WeakReference,并且用ThreadLocal作為key.//如果key為null(entry.get() == null),意味著key不再被引用,此時Entry也可以從table中清除static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}...}...
}
(4)弱引用和內存泄漏
有人在使用ThreadLocal的過程中會發現有內存泄漏的情況,就猜測內存泄漏與Entry中使用了弱引用有關,這個理解其實是不對的。
一.內存泄漏和內存溢出
Memory Overflow內存溢出:指的是沒有足夠的內存提供申請者使用。
Memory Leak內存泄漏:指的是程序中己動態分配的堆內存由于某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等后果,內存泄漏的堆積終將導致內存溢出。
二.弱引用和強引用
強引用:就是最常見的普通對象引用,只要還有強引用指向一個對象,就能表明對象還活著,垃圾回收器就不會回收這種對象。
弱引用:GC垃圾回收器一旦發現了只具有弱引用的對象,不管當前內存空間是否足夠,都會將其回收。
(5)如果ThreadLocalMap的key使用強引用
假設ThreadLocalMap中的數組元素Entry對象的key使用了強引用,此時ThreadLocal的內存圖(實線表示強引用)如下:
假設在業務代碼中使用完ThreadLocal ,棧中的ThreadLocalRef被回收了。但是因為ThreadLocalMap對象的Entry元素強引用了ThreadLocal對象,那么就可能會造成ThreadLocal對象無法被回收。
在沒有手動刪除這個Entry對象以及CurrentThread依然運行的情況下,存在這樣的強引用鏈:CurrentThreadRef -> CurrentThread -> ThreadLocalMap -> Entry
于是Entry對象就不會被回收,從而導致Entry的key和value都內存泄露。由此可見:即使Entry對象的key使用強引用, 也無法避免內存泄漏。
(6)如果ThreadLocalMap的key使用弱引用
假設ThreadLocalMap中的數組元素Entry對象的key使用了強引用,此時ThreadLocal的內存圖(實線表示強引用,虛線表示弱引用)如下:
假設在業務代碼中使用完ThreadLocal ,棧中的ThreadLocalRef被回收了。由于ThreadLocalMap只持有ThreadLocal對象的弱引用,此時沒有任何強引用指向Heap堆中的Threadlocal對象,所以ThreadLocal對象就可以順利被GC回收,此時Entry對象中的key = null。
在沒有手動刪除這個Entry對象以及CurrentThread依然運行的情況下,存在這樣的強引用鏈:CurrentThreadRef -> CurrentThread -> ThreadLocalMap -> Entry -> Value
于是即使被key弱引用的ThreadLocal對象被GC回收了(key變為null),但被value強引用的對象不會被回收,從而導致Entry的value存在內存泄漏。由此可見:即使Entry對象的key使用了弱引用, 也有可能內存泄漏。
Entry對象的key可使用弱引用,是因為棧中有變量強引用ThreadLocal對象。Entry對象的value就不能使用弱引用了,因為Value對象只有value引用。否則一旦GC回收Value對象后,而ThreadLocal對象沒被回收就會有問題。
(7)使用ThreadLocal出現內存泄漏的原因
發生內存泄漏與ThreadLocalMap中的key是否使用弱引用是沒有關系的。
發生內存泄漏的的真正原因是:
原因一:沒有手動刪除Entry對象;
原因二:CurrentThread依然運行;
一.手動刪除Entry元素
只要在使用完ThreadLocal后,調用其remove()方法刪除對應的Entry元素,則可避免內存泄漏。
二.使用完ThreadLocal后當前線程隨之結束
由于ThreadLocalMap是Thread的一個屬性,被當前線程所引用,所以ThreadLocalMap的生命周期和Thread線程一樣長。那么在使用完ThreadLocal,如果當前線程Thread隨之執行結束,ThreadLocalMap自然也會被GC回收,這樣就能從根源上避免了內存泄漏。
綜上可知,ThreadLocal內存泄漏的根源是:由于ThreadLocalMap的生命周期和Thread線程一樣長,如果沒有手動刪除對應的Entry對象,那么就會導致內存泄漏。
(8)ThreadLocalMap為什么使用弱引用
ThreadLocalMap的數組中的Entry元素的key,無論使用強引用還是弱引用,都無法完全避免內存泄漏,出現內存泄露與使用弱引用是沒有關系的。
要完全避免內存泄漏只有兩種方式:
一.使用完ThreadLocal后,調用其remove()方法刪除對應的Entry元素
二.使用完ThreadLocal后,當前線程也結束運行
相對第一種方式,第二種方式顯然更不好控制。特別是在使用線程池的時候,核心線程一般不會隨便結束的。也就是說,只要記得在使用完ThreadLocal后及時調用remove()方法刪除Entry,那么無論Entry元素的key是強引用還是弱引用都不會出現內存泄露的問題。
那么為什么Entry元素的key要用弱引用呢?
因為在調用ThreadLocal的set()、get()、remove()方法中,會觸發調用ThreadLocalMap的set()、getEntry()、remove()方法。這些方法會判斷key是否為null,如果為null就設置對應的value也為null。
這就意味著使用完ThreadLocal,CurrentThread依然運行的前提下,就算忘記調用remove()方法刪除Entry元素,弱引用也比強引用多一層保障。
弱引用的ThreadLocal對象會被回收,那么對應的value在下一次調用set()、getEntry()、remove()方法時就會被清除,從而避免內存泄漏。但如果沒有下一次調用set()、getEntry()、remove()中的任一方法,那么還是會存在內存泄露的問題。
public class ThreadLocal<T> {...static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}//初始容量 —— 必須是2的整次冪private static final int INITIAL_CAPACITY = 16;//存放數據的table,同樣,數組長度必須是2的整次冪private Entry[] table;//數組里面Entry的個數,可以用于判斷table當前使用量是否超過閾值private int size = 0;//進行數組擴容的閾值private int threshold; // Default to 0private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;//首先根據ThreadLocal對象的hashCode和數組長度進行位與運算(即取模),來獲取元素放置的位置(即數組下標)int i = key.threadLocalHashCode & (len-1);//然后從i開始往后遍歷到數組最后一個Entry(線性探索)for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {//獲取Entry元素中的keyThreadLocal<?> k = e.get();//如果key相等,則覆蓋valueif (k == key) {e.value = value;return;}//如果key為null,則用新key、value覆蓋//同時清理key = null的陳舊數據(弱引用)if (k == null) {replaceStaleEntry(key, value, i);return;}}//如果數組下標i的位置不存在數據,則直接將key和value封裝成Entry對象存儲到該位置tab[i] = new Entry(key, value);int sz = ++size;//如果超過閾值,就需要擴容了,cleanSomeSlots()方法會清理數組中的無效的keyif (!cleanSomeSlots(i, sz) && sz >= threshold) {rehash();//擴容}}private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key) {return e;} else {return getEntryAfterMiss(key, i, e);}}private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;while (e != null) {ThreadLocal<?> k = e.get();if (k == key) {return e;}if (k == null) {expungeStaleEntry(i);//清理數組中的無效的key} else {i = nextIndex(i, len);}e = tab[i];}return null;}private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {e.clear();expungeStaleEntry(i);//清理數組中的無效的keyreturn;}}}...}...
}
(9)使用線性探測法解決Hash沖突
ThreadLocalMap是使用線性探測法(開放尋址法)來解決Hash沖突的,該方法一次探測下一個位置,直到有空的位置后插入。若整個空間都找不到有空的位置,則產生溢出。
假設當前table長度為16,如果根據當前key計算出來的Hash值為14。此時table[14]上已經有值,且其key與當前key不一致,則發生了Hash沖突。這時就會將14加1得到15,取table[15]進行判斷。如果判斷table[15]時還是Hash沖突,那么就會回到0,取table[0]繼續判斷,直到可以插入為止。