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