ThreadLocal全面決議
學習目標
- 了解ThreadLocal的介紹
- 掌握ThreadLocal的運用場景
- 了解ThreadLocal的內部結構
- 了解ThreadLocal的核心方法原始碼
- 了解ThreadLocalMap的原始碼
1. ThreadLocal介紹
1.1 官方介紹
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code 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).
*
* <p>For example, the class below generates unique identifiers local to each
* thread.
* A thread's id is assigned the first time it invokes {@code ThreadId.get()}
* and remains unchanged on subsequent calls.
* <pre>
* import java.util.concurrent.atomic.AtomicInteger;
*
* public class ThreadId {
* // Atomic integer containing the next thread ID to be assigned
* private static final AtomicInteger nextId = new AtomicInteger(0);
*
* // Thread local variable containing each thread's ID
* private static final ThreadLocal<Integer> threadId =
* new ThreadLocal<Integer>() {
* @Override protected Integer initialValue() {
* return nextId.getAndIncrement();
* }
* };
*
* // Returns the current thread's unique ID, assigning it if necessary
* public static int get() {
* return threadId.get();
* }
* }
* </pre>
* <p>Each thread holds an implicit reference to its copy of a thread-local
* variable as long as the thread is alive and the {@code ThreadLocal}
* instance is accessible; after a thread goes away, all of its copies of
* thread-local instances are subject to garbage collection (unless other
* references to these copies exist).
*
* @author Josh Bloch and Doug Lea
* @since 1.2
*/
public class ThreadLocal<T> {
...
? 從Java官方檔案中的描述:ThreadLocal類用來提供執行緒內部的區域變數,這種變數在多執行緒環境下訪問(通過get和set方法訪問)時能保證各個執行緒的變數相對獨立于其他執行緒內的變數,ThreadLocal實體通常來說都是private static型別的,用于關聯執行緒和執行緒背景關系,
我們可以得知 ThreadLocal 的作用是:提供執行緒內的區域變數,不同的執行緒之間不會相互干擾,這種變數在執行緒的生命周期內起作用,減少同一個執行緒內多個函式或組件之間一些公共變數傳遞的復雜度,
總結:
1. 執行緒并發: 在多執行緒并發的場景下
2. 傳遞資料: 我們可以通過ThreadLocal在同一執行緒,不同組件中傳遞公共變數
3. 執行緒隔離: 每個執行緒的變數都是獨立的,不會相互影響
1.2 基本使用
1.2.1 常用方法
? 在使用之前,我們先來認識幾個ThreadLocal的常用方法
| 方法宣告 | 描述 |
|---|---|
| ThreadLocal() | 創建ThreadLocal物件 |
| public void set( T value) | 設定當前執行緒系結的區域變數 |
| public T get() | 獲取當前執行緒系結的區域變數 |
| public void remove() | 移除當前執行緒系結的區域變數 |
1.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() {
@Override
public void run() {
demo.setContent(Thread.currentThread().getName() + "的資料");
System.out.println("-----------------------");
System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
}
});
thread.setName("執行緒" + i);
thread.start();
}
}
}
列印結果:

? 從結果可以看出多個執行緒在訪問同一個變數的時候出現的例外,執行緒間的資料沒有隔離,下面我們來看下采用 ThreadLocal 的方式來解決這個問題的例子,
public class MyDemo {
private static ThreadLocal<String> tl = new ThreadLocal<>();
private String content;
private String getContent() {
return tl.get();
}
private void setContent(String 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() {
@Override
public void run() {
demo.setContent(Thread.currentThread().getName() + "的資料");
System.out.println("-----------------------");
System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
}
});
thread.setName("執行緒" + i);
thread.start();
}
}
}
列印結果:
? 
從結果來看,這樣很好的解決了多執行緒之間資料隔離的問題,十分方便,
1.3 ThreadLocal類與synchronized關鍵字
1.3.1 synchronized同步方式
? 這里可能有的朋友會覺得在上述例子中我們完全可以通過加鎖來實作這個功能,我們首先來看一下用synchronized代碼塊實作的效果:
public class Demo02 {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
Demo02 demo02 = new Demo02();
for (int i = 0; i < 5; i++) {
Thread t = new Thread(){
@Override
public void run() {
synchronized (Demo02.class){
demo02.setContent(Thread.currentThread().getName() + "的資料");
System.out.println("-------------------------------------");
String content = demo02.getContent();
System.out.println(Thread.currentThread().getName() + "--->" + content);
}
}
};
t.setName("執行緒" + i);
t.start();
}
}
}
列印結果:
? 
? 從結果可以發現, 加鎖確實可以解決這個問題,但是在這里我們強調的是執行緒資料隔離的問題,并不是多執行緒共享資料的問題, 在這個案例中使用synchronized關鍵字是不合適的,
1.3.2 ThreadLocal與synchronized的區別
? 雖然ThreadLocal模式與synchronized關鍵字都用于處理多執行緒并發訪問變數的問題, 不過兩者處理問題的角度和思路不同,
| synchronized | ThreadLocal | |
|---|---|---|
| 原理 | 同步機制采用'以時間換空間'的方式, 只提供了一份變數,讓不同的執行緒排隊訪問 | ThreadLocal采用'以空間換時間'的方式, 為每一個執行緒都提供了一份變數的副本,從而實作同時訪問而相不干擾 |
| 側重點 | 多個執行緒之間訪問資源的同步性 | 多執行緒中讓每個執行緒之間的資料相互隔離 |
總結: 在剛剛的案例中,雖然使用ThreadLocal和synchronized都能解決問題,但是使用ThreadLocal更為合適,因為這樣可以使程式擁有更高的并發性,
2. 運用場景_事務案例
? 通過以上的介紹,我們已經基本了解ThreadLocal的特點,但是它具體的應用是在哪里呢? 現在讓我們一起來看一個ThreadLocal的經典運用場景: 事務,
2.1 轉賬案例
2.1.1 場景構建
? 這里我們先構建一個簡單的轉賬場景: 有一個資料表account,里面有兩個用戶Jack和Rose,用戶Jack 給用戶Rose 轉賬,
? 案例的實作就簡單的用mysql資料庫,JDBC 和 C3P0 框架實作,以下是詳細代碼 :
? (1) 專案結構

? (2) 資料準備
-- 使用資料庫
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', 1000);
? (3) 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">1234</property>
<!-- 連接池引數 -->
<property name="initialPoolSize">5</property>
<property name="maxPoolSize">10</property>
<property name="checkoutTimeout">3000</property>
</default-config>
</c3p0-config>
? (4) 工具類 : JdbcUtils
package com.itheima.transfer.utils;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;
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();
}
}
}
? (5) dao層代碼 : AccountDao
package com.itheima.transfer.dao;
import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
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);
}
}
? (6) service層代碼 : AccountService
package com.itheima.transfer.service;
import com.itheima.transfer.dao.AccountDao;
import java.sql.SQLException;
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;
}
}
? (7) web層代碼 : AccountWeb
package com.itheima.transfer.web;
import com.itheima.transfer.service.AccountService;
public class AccountWeb {
public static void main(String[] args) {
// 模擬資料 : Jack 給 Rose 轉賬 100
String 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.1.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;
}
}
? 所以這里就需要操作事務,來保證轉出和轉入操作具備原子性,要么同時成功,要么同時失敗,
(1) JDBC中關于事務的操作的api
| Connection介面的方法 | 作用 |
|---|---|
| void setAutoCommit(false) | 禁用事務自動提交(改為手動) |
| void commit(); | 提交事務 |
| void rollback(); | 回滾事務 |
(2) 開啟事務的注意點:
-
為了保證所有的操作在一個事務中,案例中使用的連接必須是同一個: service層開啟事務的connection需要跟dao層訪問資料庫的connection保持一致
-
執行緒并發情況下, 每個執行緒只能操作各自的 connection
2.2 常規解決方案
2.2.1 常規方案的實作
基于上面給出的前提, 大家通常想到的解決方案是 :
- 從service層將connection物件向dao層傳遞
- 加鎖
以下是代碼實作修改的部分:
? (1 ) AccountService 類
package com.itheima.transfer.service;
import com.itheima.transfer.dao.AccountDao;
import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;
public class AccountService {
public boolean transfer(String outUser, String inUser, int money) {
AccountDao ad = new AccountDao();
//執行緒并發情況下,為了保證每個執行緒使用各自的connection,故加鎖
synchronized (AccountService.class) {
Connection conn = null;
try {
conn = JdbcUtils.getConnection();
//開啟事務
conn.setAutoCommit(false);
// 轉出
ad.out(conn, outUser, money);
// 模擬轉賬程序中的例外
// int i = 1/0;
// 轉入
ad.in(conn, inUser, money);
//事務提交
JdbcUtils.commitAndClose(conn);
} catch (Exception e) {
e.printStackTrace();
//事務回滾
JdbcUtils.rollbackAndClose(conn);
return false;
}
return true;
}
}
}
? (2) AccountDao 類 (這里需要注意的是: connection不能在dao層釋放,要在service層,不然在dao層釋放,service層就無法使用了)
package com.itheima.transfer.dao;
import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
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();
// JdbcUtils.release(pstm,conn);
JdbcUtils.release(pstm);
}
}
2.2.2 常規方案的弊端
上述方式我們看到的確按要求解決了問題,但是仔細觀察,會發現這樣實作的弊端:
-
直接從service層傳遞connection到dao層, 造成代碼耦合度提高
-
加鎖會造成執行緒失去并發性,程式性能降低
2.3 ThreadLocal解決方案
2.3.1 ThreadLocal方案的實作
像這種需要在專案中進行資料傳遞和執行緒隔離的場景,我們不妨用ThreadLocal來解決:
? (1) 工具類的修改: 加入ThreadLocal
package com.itheima.transfer.utils;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;
public class JdbcUtils {
//ThreadLocal物件 : 將connection系結在當前執行緒中
private static final ThreadLocal<Connection> tl = new ThreadLocal();
// c3p0 資料庫連接池物件屬性
private static final ComboPooledDataSource ds = new ComboPooledDataSource();
// 獲取連接
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();
}
}
}
? (2) AccountService類的修改:不需要傳遞connection物件
package com.itheima.transfer.service;
import com.itheima.transfer.dao.AccountDao;
import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;
public class AccountService {
public boolean transfer(String outUser, String inUser, int money) {
AccountDao ad = new AccountDao();
try {
Connection conn = JdbcUtils.getConnection();
//開啟事務
conn.setAutoCommit(false);
// 轉出 : 這里不需要傳參了 !
ad.out(outUser, money);
// 模擬轉賬程序中的例外
// int i = 1 / 0;
// 轉入
ad.in(inUser, money);
//事務提交
JdbcUtils.commitAndClose();
} catch (Exception e) {
e.printStackTrace();
//事務回滾
JdbcUtils.rollbackAndClose();
return false;
}
return true;
}
}
? (3) AccountDao類的修改:照常使用
package com.itheima.transfer.dao;
import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
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);
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,conn);
JdbcUtils.release(pstm);
}
}
2.3.2 ThreadLocal方案的好處
從上述的案例中我們可以看到, 在一些特定場景下,ThreadLocal方案有兩個突出的優勢:
-
傳遞資料 : 保存每個執行緒系結的資料,在需要的地方可以直接獲取, 避免引數直接傳遞帶來的代碼耦合問題
-
執行緒隔離 : 各執行緒之間的資料相互隔離卻又具備并發性,避免同步方式帶來的性能損失
3. ThreadLocal的內部結構
? 通過以上的學習,我們對ThreadLocal的作用有了一定的認識,現在我們一起來看一下ThreadLocal的內部結構,探究它能夠實作執行緒資料隔離的原理,
3.1 常見的誤解
? 通常,如果我們不去看源代碼的話,我猜ThreadLocal是這樣子設計的:每個ThreadLocal類都創建一個Map,然后用執行緒的ID threadID作為Map的key,要存盤的區域變數作為Map的value,這樣就能達到各個執行緒的區域變數隔離的效果,這是最簡單的設計方法,JDK最早期的ThreadLocal就是這樣設計的,
3.2 核心結構
? 但是,JDK后面優化了設計方案,現時JDK8 ThreadLocal的設計是:每個Thread維護一個ThreadLocalMap哈希表,這個哈希表的key是ThreadLocal實體本身,value才是真正要存盤的值Object,
? (1) 每個Thread執行緒內部都有一個Map (ThreadLocalMap)
? (2) Map里面存盤ThreadLocal物件(key)和執行緒的變數副本(value)
? (3)Thread內部的Map是由ThreadLocal維護的,由ThreadLocal負責向map獲取和設定執行緒的變數值,
? (4)對于不同的執行緒,每次獲取副本值時,別的執行緒并不能獲取到當前執行緒的副本值,形成了副本的隔離,互不干擾,

3.3 這樣設計的好處
? 這個設計與我們一開始說的設計剛好相反,這樣設計有如下兩個優勢:
(1) 這樣設計之后每個Map存盤的Entry數量就會變少,因為之前的存盤數量由Thread的數量決定,現在是由ThreadLocal的數量決定,
(2) 當Thread銷毀之后,對應的ThreadLocalMap也會隨之銷毀,能減少記憶體的使用,
4. ThreadLocal的核心方法原始碼
? 基于ThreadLocal的內部結構,我們繼續探究一下ThreadLocal的核心方法原始碼,更深入的了解其操作原理,
除了構造之外, ThreadLocal對外暴露的方法有以下4個:
| 方法宣告 | 描述 |
|---|---|
| protected T initialValue() | 回傳當前執行緒區域變數的初始值 |
| public void set( T value) | 設定當前執行緒系結的區域變數 |
| public T get() | 獲取當前執行緒系結的區域變數 |
| public void remove() | 移除當前執行緒系結的區域變數 |
其實get,set和remove邏輯是比較相似的,我們要研究清楚其中一個,其他也就明白了,
4.1 get方法
(1 ) 原始碼和對應的中文注釋
/**
* 回傳當前執行緒中保存ThreadLocal的值
* 如果當前執行緒沒有此ThreadLocal變數,
* 則它會通過呼叫{@link #initialValue} 方法進行初始化值
*
* @return 回傳當前執行緒對應此ThreadLocal的值
*/
public T get() {
// 獲取當前執行緒物件
Thread t = Thread.currentThread();
// 獲取此執行緒物件中維護的ThreadLocalMap物件
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以當前的ThreadLocal 為 key,呼叫getEntry獲取對應的存盤物體e
ThreadLocalMap.Entry e = map.getEntry(this);
// 找到對應的存盤物體 e
if (e != null) {
@SuppressWarnings("unchecked")
// 獲取存盤物體 e 對應的 value值
// 即為我們想要的當前執行緒對應此ThreadLocal的值
T result = (T)e.value;
return result;
}
}
// 如果map不存在,則證明此執行緒沒有維護的ThreadLocalMap物件
// 呼叫setInitialValue進行初始化
return setInitialValue();
}
/**
* set的變樣實作,用于初始化值initialValue,
* 用于代替防止用戶重寫set()方法
*
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
// 呼叫initialValue獲取初始化的值
T value = https://www.cnblogs.com/codepaopao/p/initialValue();
// 獲取當前執行緒物件
Thread t = Thread.currentThread();
// 獲取此執行緒物件中維護的ThreadLocalMap物件
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null)
// 存在則呼叫map.set設定此物體entry
map.set(this, value);
else
// 1)當前執行緒Thread 不存在ThreadLocalMap物件
// 2)則呼叫createMap進行ThreadLocalMap物件的初始化
// 3)并將此物體entry作為第一個值存放至ThreadLocalMap中
createMap(t, value);
// 回傳設定的值value
return value;
}
/**
* 獲取當前執行緒Thread對應維護的ThreadLocalMap
*
* @param t the current thread 當前執行緒
* @return the map 對應維護的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
*創建當前執行緒Thread對應維護的ThreadLocalMap
*
* @param t 當前執行緒
* @param firstValue 存放到map中第一個entry的值
*/
void createMap(Thread t, T firstValue) {
//這里的this是呼叫此方法的threadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
(2 ) 代碼執行流程
? A. 首先獲取當前執行緒
? B. 根據當前執行緒獲取一個Map
? C. 如果獲取的Map不為空,則在Map中以ThreadLocal的參考作為key來在Map中獲取對應的value e,否則轉到E
? D. 如果e不為null,則回傳e.value,否則轉到E
? E. Map為慷訓者e為空,則通過initialValue函式獲取初始值value,然后用ThreadLocal的參考和value作為firstKey和firstValue創建一個新的Map
總結: 先獲取當前執行緒的 ThreadLocalMap 變數,如果存在則回傳值,不存在則創建并回傳初始值,
4.2 set方法
(1 ) 原始碼和對應的中文注釋
/**
* 設定當前執行緒對應的ThreadLocal的值
*
* @param value 將要保存在當前執行緒對應的ThreadLocal的值
*/
public void set(T value) {
// 獲取當前執行緒物件
Thread t = Thread.currentThread();
// 獲取此執行緒物件中維護的ThreadLocalMap物件
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null)
// 存在則呼叫map.set設定此物體entry
map.set(this, value);
else
// 1)當前執行緒Thread 不存在ThreadLocalMap物件
// 2)則呼叫createMap進行ThreadLocalMap物件的初始化
// 3)并將此物體entry作為第一個值存放至ThreadLocalMap中
createMap(t, value);
}
(2 ) 代碼執行流程
? A. 首先獲取當前執行緒,并根據當前執行緒獲取一個Map
? B. 如果獲取的Map不為空,則將引數設定到Map中(當前ThreadLocal的參考作為key)
? C. 如果Map為空,則給該執行緒創建 Map,并設定初始值
4.3 remove方法
(1 ) 原始碼和對應的中文注釋
/**
* 洗掉當前執行緒中保存的ThreadLocal對應的物體entry
*/
public void remove() {
// 獲取當前執行緒物件中維護的ThreadLocalMap物件
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null)
// 存在則呼叫map.remove
// 以當前ThreadLocal為key洗掉對應的物體entry
m.remove(this);
}
(2 ) 代碼執行流程
? A. 首先獲取當前執行緒,并根據當前執行緒獲取一個Map
? B. 如果獲取的Map不為空,則移除當前ThreadLocal物件對應的entry
4.4 initialValue方法
/**
* 回傳當前執行緒對應的ThreadLocal的初始值
* 此方法的第一次呼叫發生在,當執行緒通過{@link #get}方法訪問此執行緒的ThreadLocal值時
* 除非執行緒先呼叫了 {@link #set}方法,在這種情況下,
* {@code initialValue} 才不會被這個執行緒呼叫,
* 通常情況下,每個執行緒最多呼叫一次這個方法,
*
* <p>這個方法僅僅簡單的回傳null {@code null};
* 如果程式員想ThreadLocal執行緒區域變數有一個除null以外的初始值,
* 必須通過子類繼承{@code ThreadLocal} 的方式去重寫此方法
* 通常, 可以通過匿名內部類的方式實作
*
* @return 當前ThreadLocal的初始值
*/
protected T initialValue() {
return null;
}
? 此方法的作用是 回傳該執行緒區域變數的初始值,
(1) 這個方法是一個延遲呼叫方法,從上面的代碼我們得知,在set方法還未呼叫而先呼叫了get方法時才執行,并且僅執行1次,
(2)這個方法預設實作直接回傳一個null,
(3)如果想要一個除null之外的初始值,可以重寫此方法,(備注: 該方法是一個protected的方法,顯然是為了讓子類覆寫而設計的)
5. ThreadLocalMap原始碼分析
5.1 基本結構
? ThreadLocalMap是ThreadLocal的內部類,沒有實作Map介面,用獨立的方式實作了Map的功能,其內部的Entry也是獨立實作,

(1) 成員變數
/**
* 初始容量 —— 必須是2的整次冪
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 存放資料的table,Entry類的定義在下面分析
* 同樣,陣列長度必須是2的冥,
*/
private Entry[] table;
/**
* 陣列里面entrys的個數,可以用于判斷table當前使用量是否超過負因子,
*/
private int size = 0;
/**
* 進行擴容的閾值,表使用量大于它的時候進行擴容,
*/
private int threshold; // Default to 0
/**
* 閾值設定為長度的2/3
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
(2) 存盤結構 - Entry
// 在ThreadLocalMap中,也是用Entry來保存K-V結構資料的,但是Entry中key只能是ThreadLocal物件,這點被Entry的構造方法已經限定死了
// 另外,Entry繼承WeakReference,使用弱參考,可以將ThreadLocal物件的生命周期和執行緒生命周期解綁,持有對ThreadLocal的弱參考,可以使得ThreadLocal在沒有其他強參考的時候被回收掉,這樣可以避免因為執行緒得不到銷毀導致ThreadLocal物件無法被回收
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = https://www.cnblogs.com/codepaopao/p/v;
}
}
5.2 hash沖突的解決
ThreadLocal使用的是自定義的ThreadLocalMap,接下來我們來探究一下ThreadLocalMap的hash沖突解決方式,
(1) 先回顧ThreadLocal的set() 方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocal.ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
}
- 代碼很簡單,獲取當前執行緒,并獲取當前執行緒的ThreadLocalMap實體(從getMap(Thread t)中很容易看出來),
- 如果獲取到的map實體不為空,呼叫map.set()方法,否則呼叫建構式 ThreadLocal.ThreadLocalMap(this, firstValue)實體化map,
可以看出來執行緒中的ThreadLocalMap使用的是延遲初始化,在第一次呼叫get()或者set()方法的時候才會進行初始化,
(2) 下面來看看建構式ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化table
table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
//計算索引
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//設定值
table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
size = 1;
//設定閾值
setThreshold(INITIAL_CAPACITY);
}
主要說一下計算索引,firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1),
- 關于
& (INITIAL_CAPACITY - 1),這是取模的一種方式,對于2的冪作為模數取模,用此代替%(2^n),這也就是為啥容量必須為2的冥,在這個地方也得到了解答, - 關于
firstKey.threadLocalHashCode:
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
? 這里定義了一個AtomicInteger型別,每次獲取當前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,這個值和斐波那契散列有關(這是一種乘數散列法,只不過這個乘數比較特殊,是32位整型上限2^32-1乘以黃金分割比例0.618....的值2654435769,用有符號整型表示就是-1640531527,去掉符號后16進制表示為0x61c88647),其主要目的就是為了讓哈希碼能均勻的分布在2的n次方的陣列里, 也就是Entry[] table中,這樣做可以盡量避免hash沖突,
(3) ThreadLocalMap中的set()
? ThreadLocalMap使用開發地址-線性探測法來解決哈希沖突,線性探測法的地址增量di = 1, 2, ... 其中,i為探測次數,該方法一次探測下一個地址,直到有空的地址后插入,若整個空間都找不到空余的地址,則產生溢位,假設當前table長度為16,也就是說如果計算出來key的hash值為14,如果table[14]上已經有值,并且其key與當前key不一致,那么就發生了hash沖突,這個時候將14加1得到15,取table[15]進行判斷,這個時候如果還是沖突會回到0,取table[0],以此類推,直到可以插入,
按照上面的描述,可以把table看成一個環形陣列,
先看一下線性探測相關的代碼,從中也可以看出來table實際是一個環:
/**
* 獲取環形陣列的下一個索引
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* 獲取環形陣列的上一個索引
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
ThreadLocalMap的set()代碼如下:
private void set(ThreadLocal<?> key, Object value) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
//計算索引,上面已經有說過,
int i = key.threadLocalHashCode & (len-1);
/**
* 根據獲取到的索引進行回圈,如果當前索引上的table[i]不為空,在沒有return的情況下,
* 就使用nextIndex()獲取下一個(上面提到到線性探測法),
*/
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//table[i]上key不為空,并且和當前key相同,更新value
if (k == key) {
e.value = https://www.cnblogs.com/codepaopao/p/value;
return;
}
/**
* table[i]上的key為空,說明被回收了
* 這個時候說明改table[i]可以重新使用,用新的key-value將其替換,并洗掉其他無效的entry
*/
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
本文來自博客園,作者:paopaoa,轉載請注明原文鏈接:https://www.cnblogs.com/codepaopao/p/16866269.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/528710.html
標籤:Java
