目錄
ThreadLocal 與 Synchronized 同步機制的比較
第03課 線程本地 ThreadLocal 的介紹與使用
ThreadLocal 概述
我們通過上兩篇的學習,我們已經知道了變量值的共享可以使用public static
變量的形式,所有的線程都使用同一個被public static
修飾的變量。
那麼如果我們想實現每一個線程都有自己的共享變量該如何解決呢?JDK 提供的 ThreadLocal 正是爲了解決這樣的問題的。
ThreadLocal 主要解決的就是每個線程綁定自己的值,可以將 ThreadLocal 類比喻成全局存放數據的盒子,盒子中可以存儲每個線程的私有變量。
先舉個例子:
public class ThreadLocalDemo {
public static ThreadLocal<List<String>> threadLocal = new ThreadLocal<>();
public void setThreadLocal(List<String> values) {
threadLocal.set(values);
}
public void getThreadLocal() {
System.out.println(Thread.currentThread().getName());
threadLocal.get().forEach(name -> System.out.println(name));
}
public static void main(String[] args) throws InterruptedException {
final ThreadLocalDemo threadLocal = new ThreadLocalDemo();
new Thread(() -> {
List<String> params = new ArrayList<>(3);
params.add("張三");
params.add("李四");
params.add("王五");
threadLocal.setThreadLocal(params);
threadLocal.getThreadLocal();
}).start();
new Thread(() -> {
try {
Thread.sleep(1000);
List<String> params = new ArrayList<>(2);
params.add("Chinese");
params.add("English");
threadLocal.setThreadLocal(params);
threadLocal.getThreadLocal();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
運行結果:
Thread-0
張三
李四
王五
Thread-1
Chinese
English
可以,看出雖然多個線程對同一個變量進行訪問,但是由於threadLocal
變量由ThreadLocal
修飾,則不同的線程訪問的就是該線程設置的值,這裏也就體現出來ThreadLocal
的作用。
當使用ThreadLocal
維護變量時,ThreadLocal
爲每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。
ThreadLocal 與 Synchronized 同步機制的比較
在同步機制中,通過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機制要求程序慎密地分析什麼時候對變量進行讀寫,什麼時候需要鎖定某個對象,什麼時候釋放對象鎖等繁雜的問題,程序設計和編寫難度相對較大。
ThreadLocal 是線程局部變量,是一種多線程間併發訪問變量的解決方案。和 Synchronized 等加鎖的方式不同,ThreadLocal 完全不提供鎖,而使用以空間換時間的方式,爲每個線程提供變量的獨立副本,以保證線程的安全。
如何實現一個簡單的 ThreadLocal
public class SimpleThreadLocal<T> {
/**
* Key爲線程對象,Value爲傳入的值對象
*/
private static Map<Thread, T> valueMap = Collections.synchronizedMap(new HashMap<Thread, T>());
/**
* 設值
* @param value Map鍵值對的value
*/
public void set(T value) {
valueMap.put(Thread.currentThread(), value);
}
/**
* 取值
* @return
*/
public T get() {
Thread currentThread = Thread.currentThread();
//返回當前線程對應的變量
T t = valueMap.get(currentThread);
//如果當前線程在Map中不存在,則將當前線程存儲到Map中
if (t == null && !valueMap.containsKey(currentThread)) {
t = initialValue();
valueMap.put(currentThread, t);
}
return t;
}
public void remove() {
valueMap.remove(Thread.currentThread());
}
public T initialValue() {
return null;
}
public static void main(String[] args) {
SimpleThreadLocal<List<String>> threadLocal = new SimpleThreadLocal<>();
new Thread(() -> {
List<String> params = new ArrayList<>(3);
params.add("張三");
params.add("李四");
params.add("王五");
threadLocal.set(params);
System.out.println(Thread.currentThread().getName());
threadLocal.get().forEach(param -> System.out.println(param));
}).start();
new Thread(() -> {
try {
Thread.sleep(1000);
List<String> params = new ArrayList<>(2);
params.add("Chinese");
params.add("English");
threadLocal.set(params);
System.out.println(Thread.currentThread().getName());
threadLocal.get().forEach(param -> System.out.println(param));
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
運行結果:
雖然上面的代碼清單中的這個 ThreadLocal 實現版本顯得比較簡單粗糙,但其目的主要在於呈現 JDK 中所提供的 ThreadLocal 類在實現上的思路。
關於如何設計 ThreadLocal 的思路以及其原理會在後文中詳細介紹,這裏只做一個簡單的預熱。
ThreadLocal 的應用
MyBatis 的使用
SqlSessionManager 類部分代碼如下:
private ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<SqlSession>();
@Override
public Connection getConnection() {
final SqlSession sqlSession = localSqlSession.get();
if (sqlSession == null) {
throw new SqlSessionException("Error: Cannot get connection. No managed session is started.");
}
return sqlSession.getConnection();
}
@Override
public void commit() {
final SqlSession sqlSession = localSqlSession.get();
if (sqlSession == null) {
throw new SqlSessionException("Error: Cannot commit. No managed session is started.");
}
sqlSession.commit();
}
@Override
public void rollback() {
final SqlSession sqlSession = localSqlSession.get();
if (sqlSession == null) {
throw new SqlSessionException("Error: Cannot rollback. No managed session is started.");
}
sqlSession.rollback();
}
從上圖可能看出,在 MyBatis 中,SqlSessionManager 類不但實現了 SqlSession 接口,同時也實現了 SqlSessionFactory 接口。而我們平時使用到的最多的就是 DefaultSqlSession,實現了 SqlSession,SqlSession 接口的實現如下:
SqlSessionManager 的作用如下:
-
SqlSessionFactoryBuilder 負責接收 mybatis-config.xml 的輸入流,創建 DefaultSqlSessionFactory 實例。
-
DefaultSqlSessionFactory 實現了 SqlSessionFactory 接口。
-
SqlSessionManager 實現了 SqlSessionFactory 接口,又封裝了 DefaultSqlSessionFactory。
拿出 SqlSessionManager 的一個方法 getConnection 解釋一下:
private ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<SqlSession>();
@Override
public Connection getConnection() {
final SqlSession sqlSession = localSqlSession.get();
if (sqlSession == null) {
throw new SqlSessionException("Error: Cannot get connection. No managed session is started.");
}
return sqlSession.getConnection();
}
可以看出 localSqlSession 是一個 ThreadLocal 變量,是每一個線程私有的,當有一個線程請求獲取 Connection 的時候,會首先獲取當前線程 ThreadLocal 中的 SqlSession,然後由 SqlSession 獲取 Connection 對象,一個 ThreadLocal 的簡單使用。
數據庫主從複製時讀寫分離的 ThreadLocal 使用
其實,在 MyBatis 中對 ThreadLocal 的使用主要體現在數據庫連接這塊,我們不僅聯想到我們在實現主從複製讀寫分離的時候,我們是否也是用到了 ThreadLocal,先看示例:
上述簡單的實現了數據源的 Handler 類 DataSourceHandler,在下邊的類中會實現讀寫數據庫的切換:
根據 AOP 切面編程獲取方法類型,根據方法的類型判斷是讀庫還是寫庫,如果是讀庫的話就爲當前線程設置訪問讀庫的數據庫信息,詳細數據庫主從複製讀寫分離的 AOP 實現案例,可以參考代碼: