學(xué)習(xí)目標(biāo)
公司主營(yíng)業(yè)務(wù):成都網(wǎng)站建設(shè)、成都網(wǎng)站設(shè)計(jì)、移動(dòng)網(wǎng)站開(kāi)發(fā)等業(yè)務(wù)。幫助企業(yè)客戶真正實(shí)現(xiàn)互聯(lián)網(wǎng)宣傳,提高企業(yè)的競(jìng)爭(zhēng)能力。成都創(chuàng)新互聯(lián)公司是一支青春激揚(yáng)、勤奮敬業(yè)、活力青春激揚(yáng)、勤奮敬業(yè)、活力澎湃、和諧高效的團(tuán)隊(duì)。公司秉承以“開(kāi)放、自由、嚴(yán)謹(jǐn)、自律”為核心的企業(yè)文化,感謝他們對(duì)我們的高要求,感謝他們從不同領(lǐng)域給我們帶來(lái)的挑戰(zhàn),讓我們激情的團(tuán)隊(duì)有機(jī)會(huì)用頭腦與智慧不斷的給客戶帶來(lái)驚喜。成都創(chuàng)新互聯(lián)公司推出大寧免費(fèi)做網(wǎng)站回饋大家。
/**
* 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).
*
* 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.
*
* 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();
* }
* }
*
* 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 {
...
? 從Java官方文檔中的描述:ThreadLocal類用來(lái)提供線程內(nèi)部的局部變量。這種變量在多線程環(huán)境下訪問(wèn)(通過(guò)get和set方法訪問(wèn))時(shí)能保證各個(gè)線程的變量相對(duì)獨(dú)立于其他線程內(nèi)的變量。ThreadLocal實(shí)例通常來(lái)說(shuō)都是private static類型的,用于關(guān)聯(lián)線程和線程上下文。
我們可以得知 ThreadLocal 的作用是:提供線程內(nèi)的局部變量,不同的線程之間不會(huì)相互干擾,這種變量在線程的生命周期內(nèi)起作用,減少同一個(gè)線程內(nèi)多個(gè)函數(shù)或組件之間一些公共變量傳遞的復(fù)雜度。
總結(jié):
1. 線程并發(fā): 在多線程并發(fā)的場(chǎng)景下
2. 傳遞數(shù)據(jù): 我們可以通過(guò)ThreadLocal在同一線程,不同組件中傳遞公共變量
3. 線程隔離: 每個(gè)線程的變量都是獨(dú)立的,不會(huì)相互影響
? 在使用之前,我們先來(lái)認(rèn)識(shí)幾個(gè)ThreadLocal的常用方法
方法聲明 | 描述 |
---|---|
ThreadLocal() | 創(chuàng)建ThreadLocal對(duì)象 |
public void set( T value) | 設(shè)置當(dāng)前線程綁定的局部變量 |
public T get() | 獲取當(dāng)前線程綁定的局部變量 |
public void remove() | 移除當(dāng)前線程綁定的局部變量 |
我們來(lái)看下面這個(gè)案例
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() + "的數(shù)據(jù)");
System.out.println("-----------------------");
System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
}
});
thread.setName("線程" + i);
thread.start();
}
}
}
打印結(jié)果:
? 從結(jié)果可以看出多個(gè)線程在訪問(wèn)同一個(gè)變量的時(shí)候出現(xiàn)的異常,線程間的數(shù)據(jù)沒(méi)有隔離。下面我們來(lái)看下采用 ThreadLocal 的方式來(lái)解決這個(gè)問(wèn)題的例子。
public class MyDemo {
private static ThreadLocal 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() + "的數(shù)據(jù)");
System.out.println("-----------------------");
System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
}
});
thread.setName("線程" + i);
thread.start();
}
}
}
打印結(jié)果:
?
從結(jié)果來(lái)看,這樣很好的解決了多線程之間數(shù)據(jù)隔離的問(wèn)題,十分方便。
? 這里可能有的朋友會(huì)覺(jué)得在上述例子中我們完全可以通過(guò)加鎖來(lái)實(shí)現(xiàn)這個(gè)功能。我們首先來(lái)看一下用synchronized代碼塊實(shí)現(xiàn)的效果:
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() + "的數(shù)據(jù)");
System.out.println("-------------------------------------");
String content = demo02.getContent();
System.out.println(Thread.currentThread().getName() + "--->" + content);
}
}
};
t.setName("線程" + i);
t.start();
}
}
}
打印結(jié)果:
?
? 從結(jié)果可以發(fā)現(xiàn), 加鎖確實(shí)可以解決這個(gè)問(wèn)題,但是在這里我們強(qiáng)調(diào)的是線程數(shù)據(jù)隔離的問(wèn)題,并不是多線程共享數(shù)據(jù)的問(wèn)題, 在這個(gè)案例中使用synchronized關(guān)鍵字是不合適的。
? 雖然ThreadLocal模式與synchronized關(guān)鍵字都用于處理多線程并發(fā)訪問(wèn)變量的問(wèn)題, 不過(guò)兩者處理問(wèn)題的角度和思路不同。
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步機(jī)制采用'以時(shí)間換空間'的方式, 只提供了一份變量,讓不同的線程排隊(duì)訪問(wèn) | ThreadLocal采用'以空間換時(shí)間'的方式, 為每一個(gè)線程都提供了一份變量的副本,從而實(shí)現(xiàn)同時(shí)訪問(wèn)而相不干擾 |
側(cè)重點(diǎn) | 多個(gè)線程之間訪問(wèn)資源的同步性 | 多線程中讓每個(gè)線程之間的數(shù)據(jù)相互隔離 |
總結(jié): 在剛剛的案例中,雖然使用ThreadLocal和synchronized都能解決問(wèn)題,但是使用ThreadLocal更為合適,因?yàn)檫@樣可以使程序擁有更高的并發(fā)性。
? 通過(guò)以上的介紹,我們已經(jīng)基本了解ThreadLocal的特點(diǎn)。但是它具體的應(yīng)用是在哪里呢? 現(xiàn)在讓我們一起來(lái)看一個(gè)ThreadLocal的經(jīng)典運(yùn)用場(chǎng)景: 事務(wù)。
? 這里我們先構(gòu)建一個(gè)簡(jiǎn)單的轉(zhuǎn)賬場(chǎng)景: 有一個(gè)數(shù)據(jù)表account,里面有兩個(gè)用戶Jack和Rose,用戶Jack 給用戶Rose 轉(zhuǎn)賬。
? 案例的實(shí)現(xiàn)就簡(jiǎn)單的用mysql數(shù)據(jù)庫(kù),JDBC 和 C3P0 框架實(shí)現(xiàn)。以下是詳細(xì)代碼 :
? (1) 項(xiàng)目結(jié)構(gòu)
? (2) 數(shù)據(jù)準(zhǔn)備
-- 使用數(shù)據(jù)庫(kù)
use test;
-- 創(chuàng)建一張賬戶表
create table account(
id int primary key auto_increment,
name varchar(20),
money double
);
-- 初始化數(shù)據(jù)
insert into account values(null, 'Jack', 1000);
insert into account values(null, 'Rose', 1000);
? (3) C3P0配置文件和工具類
com.mysql.jdbc.Driver
jdbc:mysql://localhost:3306/test
root
1234
5
10
3000
? (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 數(shù)據(jù)庫(kù)連接池對(duì)象屬性
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){
//提交事務(wù)
conn.commit();
//釋放連接
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void rollbackAndClose(Connection conn) {
try {
if(conn != null){
//回滾事務(wù)
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 {
// 轉(zhuǎn)出
ad.out(outUser, money);
// 轉(zhuǎn)入
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) {
// 模擬數(shù)據(jù) : Jack 給 Rose 轉(zhuǎn)賬 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("轉(zhuǎn)賬失敗!");
} else {
System.out.println("轉(zhuǎn)賬成功!");
}
}
}
? 案例中的轉(zhuǎn)賬涉及兩個(gè)DML操作: 一個(gè)轉(zhuǎn)出,一個(gè)轉(zhuǎn)入。這些操作是需要具備原子性的,不可分割。不然就有可能出現(xiàn)數(shù)據(jù)修改異常情況。
public class AccountService {
public boolean transfer(String outUser, String inUser, int money) {
AccountDao ad = new AccountDao();
try {
// 轉(zhuǎn)出
ad.out(outUser, money);
// 模擬轉(zhuǎn)賬過(guò)程中的異常
int i = 1/0;
// 轉(zhuǎn)入
ad.in(inUser, money);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
}
? 所以這里就需要操作事務(wù),來(lái)保證轉(zhuǎn)出和轉(zhuǎn)入操作具備原子性,要么同時(shí)成功,要么同時(shí)失敗。
(1) JDBC中關(guān)于事務(wù)的操作的api
Connection接口的方法 | 作用 |
---|---|
void setAutoCommit(false) | 禁用事務(wù)自動(dòng)提交(改為手動(dòng)) |
void commit(); | 提交事務(wù) |
void rollback(); | 回滾事務(wù) |
(2) 開(kāi)啟事務(wù)的注意點(diǎn):
為了保證所有的操作在一個(gè)事務(wù)中,案例中使用的連接必須是同一個(gè): service層開(kāi)啟事務(wù)的connection需要跟dao層訪問(wèn)數(shù)據(jù)庫(kù)的connection保持一致
線程并發(fā)情況下, 每個(gè)線程只能操作各自的 connection
基于上面給出的前提, 大家通常想到的解決方案是 :
以下是代碼實(shí)現(xiàn)修改的部分:
? (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();
//線程并發(fā)情況下,為了保證每個(gè)線程使用各自的connection,故加鎖
synchronized (AccountService.class) {
Connection conn = null;
try {
conn = JdbcUtils.getConnection();
//開(kāi)啟事務(wù)
conn.setAutoCommit(false);
// 轉(zhuǎn)出
ad.out(conn, outUser, money);
// 模擬轉(zhuǎn)賬過(guò)程中的異常
// int i = 1/0;
// 轉(zhuǎn)入
ad.in(conn, inUser, money);
//事務(wù)提交
JdbcUtils.commitAndClose(conn);
} catch (Exception e) {
e.printStackTrace();
//事務(wù)回滾
JdbcUtils.rollbackAndClose(conn);
return false;
}
return true;
}
}
}
? (2) AccountDao 類 (這里需要注意的是: connection不能在dao層釋放,要在service層,不然在dao層釋放,service層就無(wú)法使用了)
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中傳遞過(guò)來(lái)的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);
}
}
上述方式我們看到的確按要求解決了問(wèn)題,但是仔細(xì)觀察,會(huì)發(fā)現(xiàn)這樣實(shí)現(xiàn)的弊端:
直接從service層傳遞connection到dao層, 造成代碼耦合度提高
加鎖會(huì)造成線程失去并發(fā)性,程序性能降低
像這種需要在項(xiàng)目中進(jìn)行數(shù)據(jù)傳遞和線程隔離的場(chǎng)景,我們不妨用ThreadLocal來(lái)解決:
? (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對(duì)象 : 將connection綁定在當(dāng)前線程中
private static final ThreadLocal tl = new ThreadLocal();
// c3p0 數(shù)據(jù)庫(kù)連接池對(duì)象屬性
private static final ComboPooledDataSource ds = new ComboPooledDataSource();
// 獲取連接
public static Connection getConnection() throws SQLException {
//取出當(dāng)前線程綁定的connection對(duì)象
Connection conn = tl.get();
if (conn == null) {
//如果沒(méi)有,則從連接池中取出
conn = ds.getConnection();
//再將connection對(duì)象綁定到當(dāng)前線程中
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();
//提交事務(wù)
conn.commit();
//解除綁定
tl.remove();
//釋放連接
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void rollbackAndClose() {
try {
Connection conn = getConnection();
//回滾事務(wù)
conn.rollback();
//解除綁定
tl.remove();
//釋放連接
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
? (2) AccountService類的修改:不需要傳遞connection對(duì)象
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();
//開(kāi)啟事務(wù)
conn.setAutoCommit(false);
// 轉(zhuǎn)出 : 這里不需要傳參了 !
ad.out(outUser, money);
// 模擬轉(zhuǎn)賬過(guò)程中的異常
// int i = 1 / 0;
// 轉(zhuǎn)入
ad.in(inUser, money);
//事務(wù)提交
JdbcUtils.commitAndClose();
} catch (Exception e) {
e.printStackTrace();
//事務(wù)回滾
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);
}
}
從上述的案例中我們可以看到, 在一些特定場(chǎng)景下,ThreadLocal方案有兩個(gè)突出的優(yōu)勢(shì):
傳遞數(shù)據(jù) : 保存每個(gè)線程綁定的數(shù)據(jù),在需要的地方可以直接獲取, 避免參數(shù)直接傳遞帶來(lái)的代碼耦合問(wèn)題
線程隔離 : 各線程之間的數(shù)據(jù)相互隔離卻又具備并發(fā)性,避免同步方式帶來(lái)的性能損失
? 通過(guò)以上的學(xué)習(xí),我們對(duì)ThreadLocal的作用有了一定的認(rèn)識(shí)?,F(xiàn)在我們一起來(lái)看一下ThreadLocal的內(nèi)部結(jié)構(gòu),探究它能夠?qū)崿F(xiàn)線程數(shù)據(jù)隔離的原理。
? 通常,如果我們不去看源代碼的話,我猜ThreadLocal
是這樣子設(shè)計(jì)的:每個(gè)ThreadLocal
類都創(chuàng)建一個(gè)Map
,然后用線程的ID threadID
作為Map
的key
,要存儲(chǔ)的局部變量作為Map
的value
,這樣就能達(dá)到各個(gè)線程的局部變量隔離的效果。這是最簡(jiǎn)單的設(shè)計(jì)方法,JDK最早期的ThreadLocal
就是這樣設(shè)計(jì)的。
? 但是,JDK后面優(yōu)化了設(shè)計(jì)方案,現(xiàn)時(shí)JDK8 ThreadLocal
的設(shè)計(jì)是:每個(gè)Thread
維護(hù)一個(gè)ThreadLocalMap
哈希表,這個(gè)哈希表的key
是ThreadLocal
實(shí)例本身,value
才是真正要存儲(chǔ)的值Object
。
? (1) 每個(gè)Thread線程內(nèi)部都有一個(gè)Map (ThreadLocalMap)
? (2) Map里面存儲(chǔ)ThreadLocal對(duì)象(key)和線程的變量副本(value)
? (3)Thread內(nèi)部的Map是由ThreadLocal維護(hù)的,由ThreadLocal負(fù)責(zé)向map獲取和設(shè)置線程的變量值。
? (4)對(duì)于不同的線程,每次獲取副本值時(shí),別的線程并不能獲取到當(dāng)前線程的副本值,形成了副本的隔離,互不干擾。
? 這個(gè)設(shè)計(jì)與我們一開(kāi)始說(shuō)的設(shè)計(jì)剛好相反,這樣設(shè)計(jì)有如下兩個(gè)優(yōu)勢(shì):
(1) 這樣設(shè)計(jì)之后每個(gè)Map
存儲(chǔ)的Entry
數(shù)量就會(huì)變少,因?yàn)橹暗拇鎯?chǔ)數(shù)量由Thread
的數(shù)量決定,現(xiàn)在是由ThreadLocal
的數(shù)量決定。
(2) 當(dāng)Thread
銷毀之后,對(duì)應(yīng)的ThreadLocalMap
也會(huì)隨之銷毀,能減少內(nèi)存的使用。
? 基于ThreadLocal的內(nèi)部結(jié)構(gòu),我們繼續(xù)探究一下ThreadLocal的核心方法源碼,更深入的了解其操作原理。
除了構(gòu)造之外, ThreadLocal對(duì)外暴露的方法有以下4個(gè):
方法聲明 | 描述 |
---|---|
protected T initialValue() | 返回當(dāng)前線程局部變量的初始值 |
public void set( T value) | 設(shè)置當(dāng)前線程綁定的局部變量 |
public T get() | 獲取當(dāng)前線程綁定的局部變量 |
public void remove() | 移除當(dāng)前線程綁定的局部變量 |
其實(shí)get,set和remove邏輯是比較相似的,我們要研究清楚其中一個(gè),其他也就明白了。
(1 ) 源碼和對(duì)應(yīng)的中文注釋
/**
* 返回當(dāng)前線程中保存ThreadLocal的值
* 如果當(dāng)前線程沒(méi)有此ThreadLocal變量,
* 則它會(huì)通過(guò)調(diào)用{@link #initialValue} 方法進(jìn)行初始化值
*
* @return 返回當(dāng)前線程對(duì)應(yīng)此ThreadLocal的值
*/
public T get() {
// 獲取當(dāng)前線程對(duì)象
Thread t = Thread.currentThread();
// 獲取此線程對(duì)象中維護(hù)的ThreadLocalMap對(duì)象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以當(dāng)前的ThreadLocal 為 key,調(diào)用getEntry獲取對(duì)應(yīng)的存儲(chǔ)實(shí)體e
ThreadLocalMap.Entry e = map.getEntry(this);
// 找到對(duì)應(yīng)的存儲(chǔ)實(shí)體 e
if (e != null) {
@SuppressWarnings("unchecked")
// 獲取存儲(chǔ)實(shí)體 e 對(duì)應(yīng)的 value值
// 即為我們想要的當(dāng)前線程對(duì)應(yīng)此ThreadLocal的值
T result = (T)e.value;
return result;
}
}
// 如果map不存在,則證明此線程沒(méi)有維護(hù)的ThreadLocalMap對(duì)象
// 調(diào)用setInitialValue進(jìn)行初始化
return setInitialValue();
}
/**
* set的變樣實(shí)現(xiàn),用于初始化值initialValue,
* 用于代替防止用戶重寫set()方法
*
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
// 調(diào)用initialValue獲取初始化的值
T value = initialValue();
// 獲取當(dāng)前線程對(duì)象
Thread t = Thread.currentThread();
// 獲取此線程對(duì)象中維護(hù)的ThreadLocalMap對(duì)象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null)
// 存在則調(diào)用map.set設(shè)置此實(shí)體entry
map.set(this, value);
else
// 1)當(dāng)前線程Thread 不存在ThreadLocalMap對(duì)象
// 2)則調(diào)用createMap進(jìn)行ThreadLocalMap對(duì)象的初始化
// 3)并將此實(shí)體entry作為第一個(gè)值存放至ThreadLocalMap中
createMap(t, value);
// 返回設(shè)置的值value
return value;
}
/**
* 獲取當(dāng)前線程Thread對(duì)應(yīng)維護(hù)的ThreadLocalMap
*
* @param t the current thread 當(dāng)前線程
* @return the map 對(duì)應(yīng)維護(hù)的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
*創(chuàng)建當(dāng)前線程Thread對(duì)應(yīng)維護(hù)的ThreadLocalMap
*
* @param t 當(dāng)前線程
* @param firstValue 存放到map中第一個(gè)entry的值
*/
void createMap(Thread t, T firstValue) {
//這里的this是調(diào)用此方法的threadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
(2 ) 代碼執(zhí)行流程
? A. 首先獲取當(dāng)前線程
? B. 根據(jù)當(dāng)前線程獲取一個(gè)Map
? C. 如果獲取的Map不為空,則在Map中以ThreadLocal的引用作為key來(lái)在Map中獲取對(duì)應(yīng)的value e,否則轉(zhuǎn)到E
? D. 如果e不為null,則返回e.value,否則轉(zhuǎn)到E
? E. Map為空或者e為空,則通過(guò)initialValue函數(shù)獲取初始值value,然后用ThreadLocal的引用和value作為firstKey和firstValue創(chuàng)建一個(gè)新的Map
總結(jié): 先獲取當(dāng)前線程的 ThreadLocalMap 變量,如果存在則返回值,不存在則創(chuàng)建并返回初始值。
(1 ) 源碼和對(duì)應(yīng)的中文注釋
/**
* 設(shè)置當(dāng)前線程對(duì)應(yīng)的ThreadLocal的值
*
* @param value 將要保存在當(dāng)前線程對(duì)應(yīng)的ThreadLocal的值
*/
public void set(T value) {
// 獲取當(dāng)前線程對(duì)象
Thread t = Thread.currentThread();
// 獲取此線程對(duì)象中維護(hù)的ThreadLocalMap對(duì)象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null)
// 存在則調(diào)用map.set設(shè)置此實(shí)體entry
map.set(this, value);
else
// 1)當(dāng)前線程Thread 不存在ThreadLocalMap對(duì)象
// 2)則調(diào)用createMap進(jìn)行ThreadLocalMap對(duì)象的初始化
// 3)并將此實(shí)體entry作為第一個(gè)值存放至ThreadLocalMap中
createMap(t, value);
}
(2 ) 代碼執(zhí)行流程
? A. 首先獲取當(dāng)前線程,并根據(jù)當(dāng)前線程獲取一個(gè)Map
? B. 如果獲取的Map不為空,則將參數(shù)設(shè)置到Map中(當(dāng)前ThreadLocal的引用作為key)
? C. 如果Map為空,則給該線程創(chuàng)建 Map,并設(shè)置初始值
(1 ) 源碼和對(duì)應(yīng)的中文注釋
/**
* 刪除當(dāng)前線程中保存的ThreadLocal對(duì)應(yīng)的實(shí)體entry
*/
public void remove() {
// 獲取當(dāng)前線程對(duì)象中維護(hù)的ThreadLocalMap對(duì)象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null)
// 存在則調(diào)用map.remove
// 以當(dāng)前ThreadLocal為key刪除對(duì)應(yīng)的實(shí)體entry
m.remove(this);
}
(2 ) 代碼執(zhí)行流程
? A. 首先獲取當(dāng)前線程,并根據(jù)當(dāng)前線程獲取一個(gè)Map
? B. 如果獲取的Map不為空,則移除當(dāng)前ThreadLocal對(duì)象對(duì)應(yīng)的entry
/**
* 返回當(dāng)前線程對(duì)應(yīng)的ThreadLocal的初始值
* 此方法的第一次調(diào)用發(fā)生在,當(dāng)線程通過(guò){@link #get}方法訪問(wèn)此線程的ThreadLocal值時(shí)
* 除非線程先調(diào)用了 {@link #set}方法,在這種情況下,
* {@code initialValue} 才不會(huì)被這個(gè)線程調(diào)用。
* 通常情況下,每個(gè)線程最多調(diào)用一次這個(gè)方法。
*
* 這個(gè)方法僅僅簡(jiǎn)單的返回null {@code null};
* 如果程序員想ThreadLocal線程局部變量有一個(gè)除null以外的初始值,
* 必須通過(guò)子類繼承{@code ThreadLocal} 的方式去重寫此方法
* 通常, 可以通過(guò)匿名內(nèi)部類的方式實(shí)現(xiàn)
*
* @return 當(dāng)前ThreadLocal的初始值
*/
protected T initialValue() {
return null;
}
? 此方法的作用是 返回該線程局部變量的初始值。
(1) 這個(gè)方法是一個(gè)延遲調(diào)用方法,從上面的代碼我們得知,在set方法還未調(diào)用而先調(diào)用了get方法時(shí)才執(zhí)行,并且僅執(zhí)行1次。
(2)這個(gè)方法缺省實(shí)現(xiàn)直接返回一個(gè)null
。
(3)如果想要一個(gè)除null之外的初始值,可以重寫此方法。(備注: 該方法是一個(gè)protected
的方法,顯然是為了讓子類覆蓋而設(shè)計(jì)的)
? ThreadLocalMap是ThreadLocal的內(nèi)部類,沒(méi)有實(shí)現(xiàn)Map接口,用獨(dú)立的方式實(shí)現(xiàn)了Map的功能,其內(nèi)部的Entry也是獨(dú)立實(shí)現(xiàn)。
(1) 成員變量
/**
* 初始容量 —— 必須是2的整次冪
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 存放數(shù)據(jù)的table,Entry類的定義在下面分析
* 同樣,數(shù)組長(zhǎng)度必須是2的冥。
*/
private Entry[] table;
/**
* 數(shù)組里面entrys的個(gè)數(shù),可以用于判斷table當(dāng)前使用量是否超過(guò)負(fù)因子。
*/
private int size = 0;
/**
* 進(jìn)行擴(kuò)容的閾值,表使用量大于它的時(shí)候進(jìn)行擴(kuò)容。
*/
private int threshold; // Default to 0
/**
* 閾值設(shè)置為長(zhǎng)度的2/3
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
(2) 存儲(chǔ)結(jié)構(gòu) - Entry
// 在ThreadLocalMap中,也是用Entry來(lái)保存K-V結(jié)構(gòu)數(shù)據(jù)的。但是Entry中key只能是ThreadLocal對(duì)象,這點(diǎn)被Entry的構(gòu)造方法已經(jīng)限定死了
// 另外,Entry繼承WeakReference,使用弱引用,可以將ThreadLocal對(duì)象的生命周期和線程生命周期解綁,持有對(duì)ThreadLocal的弱引用,可以使得ThreadLocal在沒(méi)有其他強(qiáng)引用的時(shí)候被回收掉,這樣可以避免因?yàn)榫€程得不到銷毀導(dǎo)致ThreadLocal對(duì)象無(wú)法被回收
static class Entry extends WeakReference {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
ThreadLocal使用的是自定義的ThreadLocalMap,接下來(lái)我們來(lái)探究一下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);
}
可以看出來(lái)線程中的ThreadLocalMap使用的是延遲初始化,在第一次調(diào)用get()或者set()方法的時(shí)候才會(huì)進(jìn)行初始化。
(2) 下面來(lái)看看構(gòu)造函數(shù)ThreadLocalMap(ThreadLocal> firstKey, Object firstValue)
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
//初始化table
table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
//計(jì)算索引
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//設(shè)置值
table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
size = 1;
//設(shè)置閾值
setThreshold(INITIAL_CAPACITY);
}
主要說(shuō)一下計(jì)算索引,firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
。
& (INITIAL_CAPACITY - 1)
,這是取模的一種方式,對(duì)于2的冪作為模數(shù)取模,用此代替%(2^n)
,這也就是為啥容量必須為2的冥,在這個(gè)地方也得到了解答。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;
? 這里定義了一個(gè)AtomicInteger類型,每次獲取當(dāng)前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647
,這個(gè)值和斐波那契散列
有關(guān)(這是一種乘數(shù)散列法,只不過(guò)這個(gè)乘數(shù)比較特殊,是32位整型上限2^32-1乘以黃金分割比例0.618....的值2654435769,用有符號(hào)整型表示就是-1640531527,去掉符號(hào)后16進(jìn)制表示為0x61c88647),其主要目的就是為了讓哈希碼能均勻的分布在2的n次方的數(shù)組里, 也就是Entry[] table
中,這樣做可以盡量避免hash沖突。
(3) ThreadLocalMap中的set()
? ThreadLocalMap使用開(kāi)發(fā)地址-線性探測(cè)法
來(lái)解決哈希沖突,線性探測(cè)法的地址增量di = 1, 2, ... 其中,i為探測(cè)次數(shù)。該方法一次探測(cè)下一個(gè)地址,直到有空的地址后插入,若整個(gè)空間都找不到空余的地址,則產(chǎn)生溢出。假設(shè)當(dāng)前table長(zhǎng)度為16,也就是說(shuō)如果計(jì)算出來(lái)key的hash值為14,如果table[14]上已經(jīng)有值,并且其key與當(dāng)前key不一致,那么就發(fā)生了hash沖突,這個(gè)時(shí)候?qū)?4加1得到15,取table[15]進(jìn)行判斷,這個(gè)時(shí)候如果還是沖突會(huì)回到0,取table[0],以此類推,直到可以插入。
按照上面的描述,可以把table看成一個(gè)環(huán)形數(shù)組
。
先看一下線性探測(cè)相關(guān)的代碼,從中也可以看出來(lái)table實(shí)際是一個(gè)環(huán):
/**
* 獲取環(huán)形數(shù)組的下一個(gè)索引
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* 獲取環(huán)形數(shù)組的上一個(gè)索引
*/
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;
//計(jì)算索引,上面已經(jīng)有說(shuō)過(guò)。
int i = key.threadLocalHashCode & (len-1);
/**
* 根據(jù)獲取到的索引進(jìn)行循環(huán),如果當(dāng)前索引上的table[i]不為空,在沒(méi)有return的情況下,
* 就使用nextIndex()獲取下一個(gè)(上面提到到線性探測(cè)法)。
*/
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal> k = e.get();
//table[i]上key不為空,并且和當(dāng)前key相同,更新value
if (k == key) {
e.value = value;
return;
}
/**
* table[i]上的key為空,說(shuō)明被回收了
* 這個(gè)時(shí)候說(shuō)明改table[i]可以重新使用,用新的key-value將其替換,并刪除其他無(wú)效的entry
*/
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}