ThreadLocal 內部實現、應用場景和記憶體洩漏
一、什麼是ThreadLocal
首先明確一個概念,那就是ThreadLocal並不是用來併發控制訪問一個共同物件,而是為了給每個執行緒分配一個只屬於該執行緒的變數,顧名思義它是local variable(執行緒區域性變數)。它的功用非常簡單,就是為每一個使用該變數的執行緒都提供一個變數值的副本,是每一個執行緒都可以獨立地改變自己的副本,而不會和其它執行緒的副本衝突,實現執行緒間的資料隔離。從執行緒的角度看,就好像每一個執行緒都完全擁有該變數。
set和get方法是ThreadLocal類中最常用的兩個方法。,接下來 我們來看下ThreadLocal的內部實現:
set方法實現原始碼如下:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//Thread類裡預設threadLocals為null
class Thread implements Runnable{
ThreadLocal.ThreadLocalMap threadLocals = null;
}
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super (k);
value = v;
}
}
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
Thread.currentThread得到當前執行緒,如果當前執行緒存在threadLocals這個變數不為空,那麼根據當前的ThreadLocal例項作為key尋找在map中位置,然後用新的value值來替換舊值。
在ThreadLocal這個類中比較引人注目的應該是ThreadLocal->ThreadLocalMap->Entry這個類。這個類繼承自WeakReference。
get方法實現原始碼如下:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
首先我們通過Thread.currentThread得到當前執行緒,然後獲取當前執行緒的threadLocals變數,這個變數就是ThreadLocalMap型別的,如果這個變數map不為空,再獲取ThreadLocalMap.Entry e,如果e不為空,則獲取value值返回,否則在Map中初始化Entry,並返回初始值null。如果map為空,則建立並初始化map,並返回初始值null。
二、ThreadLocal應用場景
1、資料庫連線池實現
jdbc連線資料庫,如下所示:
Class.forName("com.mysql.jdbc.Driver");
java.sql.Connection conn = DriverManager.getConnection(jdbcUrl);
注意:一次Drivermanager.getConnection(jdbcurl)獲得只是一個connection,並不能滿足高併發情況。因為connection不是執行緒安全的,一個connection對應的是一個事物。
每次獲得connection都需要浪費cpu資源和記憶體資源,是很浪費資源的。所以誕生了資料庫連線池。資料庫連線池實現原理如下:
pool.getConnection(),都是先從threadlocal裡面拿的,如果threadlocal裡面有,則用,保證執行緒裡的多個dao操作,用的是同一個connection,以保證事務。如果新執行緒,則將新的connection放在threadlocal裡,再get給到執行緒。
將connection放進threadlocal裡的,以保證每個執行緒從連線池中獲得的都是執行緒自己的connection。
Hibernate的資料庫連線池原始碼實現:
public class ConnectionPool implements IConnectionPool {
// 連線池配置屬性
private DBbean dbBean;
private boolean isActive = false; // 連線池活動狀態
private int contActive = 0;// 記錄建立的總的連線數
// 空閒連線
private List<Connection> freeConnection = new Vector<Connection>();
// 活動連線
private List<Connection> activeConnection = new Vector<Connection>();
// 將執行緒和連線繫結,保證事務能統一執行
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
public ConnectionPool(DBbean dbBean) {
super();
this.dbBean = dbBean;
init();
cheackPool();
}
// 初始化
public void init() {
try {
Class.forName(dbBean.getDriverName());
for (int i = 0; i < dbBean.getInitConnections(); i++) {
Connection conn;
conn = newConnection();
// 初始化最小連線數
if (conn != null) {
freeConnection.add(conn);
contActive++;
}
}
isActive = true;
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
// 獲得當前連線
public Connection getCurrentConnecton(){
// 預設執行緒裡面取
Connection conn = threadLocal.get();
if(!isValid(conn)){
conn = getConnection();
}
return conn;
}
// 獲得連線
public synchronized Connection getConnection() {
Connection conn = null;
try {
// 判斷是否超過最大連線數限制
if(contActive < this.dbBean.getMaxActiveConnections()){
if (freeConnection.size() > 0) {
conn = freeConnection.get(0);
if (conn != null) {
threadLocal.set(conn);
}
freeConnection.remove(0);
} else {
conn = newConnection();
}
}else{
// 繼續獲得連線,直到從新獲得連線
wait(this.dbBean.getConnTimeOut());
conn = getConnection();
}
if (isValid(conn)) {
activeConnection.add(conn);
contActive ++;
}
} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return conn;
}
// 獲得新連線
private synchronized Connection newConnection()
throws ClassNotFoundException, SQLException {
Connection conn = null;
if (dbBean != null) {
Class.forName(dbBean.getDriverName());
conn = DriverManager.getConnection(dbBean.getUrl(),
dbBean.getUserName(), dbBean.getPassword());
}
return conn;
}
// 釋放連線
public synchronized void releaseConn(Connection conn) throws SQLException {
if (isValid(conn)&& !(freeConnection.size() > dbBean.getMaxConnections())) {
freeConnection.add(conn);
activeConnection.remove(conn);
contActive --;
threadLocal.remove();
// 喚醒所有正待等待的執行緒,去搶連線
notifyAll();
}
}
// 判斷連線是否可用
private boolean isValid(Connection conn) {
try {
if (conn == null || conn.isClosed()) {
return false;
}
} catch (SQLException e) {
e.printStackTrace();
}
return true;
}
// 銷燬連線池
public synchronized void destroy() {
for (Connection conn : freeConnection) {
try {
if (isValid(conn)) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
for (Connection conn : activeConnection) {
try {
if (isValid(conn)) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
isActive = false;
contActive = 0;
}
// 連線池狀態
@Override
public boolean isActive() {
return isActive;
}
// 定時檢查連線池情況
@Override
public void cheackPool() {
if(dbBean.isCheakPool()){
new Timer().schedule(new TimerTask() {
@Override
public void run() {
// 1.對執行緒裡面的連線狀態
// 2.連線池最小 最大連線數
// 3.其他狀態進行檢查,因為這裡還需要寫幾個執行緒管理的類,暫時就不添加了
System.out.println("空線池連線數:"+freeConnection.size());
System.out.println("活動連線數::"+activeConnection.size());
System.out.println("總的連線數:"+contActive);
}
},dbBean.getLazyCheck(),dbBean.getPeriodCheck());
}
}
}
2、有時候ThreadLocal也可以用來避免一些引數傳遞,通過ThreadLocal來訪問物件。
比如一個方法呼叫另一個方法時傳入了8個引數,通過逐層呼叫到第N個方法,傳入了其中一個引數,此時最後一個方法需要增加一個引數,第一個方法變成9個引數是自然的,但是這個時候,相關的方法都會受到牽連,使得程式碼變得臃腫不堪。這時候就可以將要新增的引數設定成執行緒本地變數,來避免參數傳遞。
上面提到的是ThreadLocal一種亡羊補牢的用途,不過也不是特別推薦使用的方式,它還有一些類似的方式用來使用,就是在框架級別有很多動態呼叫,呼叫過程中需要滿足一些協議,雖然協議我們會盡量的通用,而很多擴充套件的引數在定義協議時是不容易考慮完全的以及版本也是隨時在升級的,但是在框架擴充套件時也需要滿足介面的通用性和向下相容,而一些擴充套件的內容我們就需要ThreadLocal來做方便簡單的支援。
簡單來說,ThreadLocal是將一些複雜的系統擴充套件變成了簡單定義,使得相關引數牽連的部分變得非常容易。
3、在某些情況下提升效能和安全。
用SimpleDateFormat這個物件,進行日期格式化。因為建立這個物件本身很費時的,而且我們也知道SimpleDateFormat本身不是執行緒安全的,也不能快取一個共享的SimpleDateFormat例項,為此我們想到使用ThreadLocal來給每個執行緒快取一個SimpleDateFormat例項,提高效能。同時因為每個Servlet會用到不同pattern的時間格式化類,所以我們對應每一種pattern生成了一個ThreadLocal例項。
public interface DateTimeFormat {
String DATE_PATTERN = "yyyy-MM-dd";
ThreadLocal<DateFormat> DATE_FORMAT = ThreadLocal.withInitial(() -> {
return new SimpleDateFormat("yyyy-MM-dd");
});
String TIME_PATTERN = "HH:mm:ss";
ThreadLocal<DateFormat> TIME_FORMAT = ThreadLocal.withInitial(() -> {
return new SimpleDateFormat("HH:mm:ss");
});
String DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
ThreadLocal<DateFormat> DATE_TIME_FORMAT = ThreadLocal.withInitial(() -> {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
});
}
為什麼SimpleDateFormat不安全,可以參考此篇博文:
假如我們把SimpleDateFormat定義成static成員變數,那麼多個thread之間會共享這個sdf物件, 所以Calendar物件也會共享。
假定執行緒A和執行緒B都進入了parse(text, pos) 方法, 執行緒B執行到calendar.clear()後,執行緒A執行到calendar.getTime(), 那麼就會有問題。
如果不用static修飾,將SimpleDateFormat定義成區域性變數:
每呼叫一次方法就會建立一個SimpleDateFormat物件,方法結束又要作為垃圾回收。加鎖效能較差,每次都要等待鎖釋放後其他執行緒才能進入。那麼最好的辦法就是:使用ThreadLocal: 每個執行緒都將擁有自己的SimpleDateFormat物件副本。
附-SimpleDateFormat關鍵原始碼:
public class SimpleDateFormat extends DateFormat {
public Date parse(String text, ParsePosition pos){
calendar.clear(); // Clears all the time fields
// other logic ...
Date parsedDate = calendar.getTime();
}
}
abstract class DateFormat{
// other logic ...
protected Calendar calendar;
public Date parse(String source) throws ParseException{
ParsePosition pos = new ParsePosition(0);
Date result = parse(source, pos);
if (pos.index == 0)
throw new ParseException("Unparseable date: \"" + source + "\"" ,
pos.errorIndex);
return result;
}
}
三、記憶體洩漏問題
在上面提到過,每個thread中都存在一個map, map的型別是ThreadLocal.ThreadLocalMap. Map中的key為一個threadlocal例項. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每個key都弱引用指向threadlocal. 當把threadlocal例項置為null以後,沒有任何強引用指向threadlocal例項,所以threadlocal將會被gc回收. 但是,我們的value卻不能回收,因為存在一條從current thread連線過來的強引用. 只有當前thread結束以後, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將全部被GC回收。
所以得出一個結論就是隻要這個執行緒物件被gc回收,就不會出現記憶體洩露,但在threadLocal設為null和執行緒結束這段時間不會被回收的,就發生了我們認為的記憶體洩露。其實這是一個對概念理解的不一致,也沒什麼好爭論的。最要命的是執行緒物件不被回收的情況,這就發生了真正意義上的記憶體洩露。比如使用執行緒池的時候,執行緒結束是不會銷燬的,會再次使用的。就可能出現記憶體洩露。
