ThreadLocal 理解與應(yīng)用
在并發(fā)編程中,我們主要考慮的問(wèn)題是多個(gè)線(xiàn)程對(duì)于共享數(shù)據(jù)的訪(fǎng)問(wèn),并在訪(fǎng)問(wèn)共享數(shù)據(jù)時(shí)保證線(xiàn)程安全。如果我們希望每個(gè)線(xiàn)程都有一個(gè)共享變量的副本,并且對(duì)這個(gè)副本進(jìn)行讀寫(xiě)時(shí)不影響其他的線(xiàn)程該如何做呢?
JDK 為我們提供了ThreadLocal類(lèi)來(lái)解決線(xiàn)程與數(shù)據(jù)綁定的需求。如果說(shuō)synchronized與volatile關(guān)鍵字保證了線(xiàn)程間的數(shù)據(jù)共享(可見(jiàn)性),那么ThraedLocal類(lèi)就是保證線(xiàn)程間的數(shù)據(jù)隔離。為什么這么說(shuō)呢?
volatile和synchronized保證共享數(shù)據(jù)在不同的線(xiàn)程中是可見(jiàn)的,一個(gè)線(xiàn)程對(duì)共享數(shù)據(jù)的改變其他線(xiàn)程也能觀察到,通過(guò)同步機(jī)制來(lái)保證線(xiàn)程安全。而ThreadLocal類(lèi)則提供了另一個(gè)保證線(xiàn)程安全的處理思路:
每個(gè)線(xiàn)程持有共享變量的一個(gè)副本,并且與線(xiàn)程綁定,這樣每個(gè)線(xiàn)程對(duì)這個(gè)變量的讀寫(xiě)都在自己線(xiàn)程內(nèi)部,對(duì)線(xiàn)程來(lái)說(shuō),這個(gè)變量是屬于線(xiàn)程私有的,不會(huì)對(duì)其他線(xiàn)程有影響,避免出現(xiàn)數(shù)據(jù)不一致的情況,也就是線(xiàn)程間的數(shù)據(jù)是隔離的,互不相干的。
什么是線(xiàn)程局部變量(thread-local variable)?
線(xiàn)程局部變量就是為每一個(gè)使用該變量的線(xiàn)程都提供一個(gè)變量值的副本,該副本是線(xiàn)程私有的,不同的線(xiàn)程持有不同的副本,每個(gè)線(xiàn)程都可以獨(dú)立的改變這個(gè)副本并且不會(huì)和其他的線(xiàn)程起沖突。這是 Java 中較為特殊的線(xiàn)程綁定機(jī)制,從而為多線(xiàn)程環(huán)境中常出現(xiàn)的并發(fā)訪(fǎng)問(wèn)問(wèn)題提供了一種數(shù)據(jù)隔離機(jī)制。
如何使用 ThreadLocal
-
創(chuàng)建 ThreadLocal 實(shí)例
public static ThreadLocal<Integer> threadLocalData = new ThreadLocal<>(); public static void main(String[] args) { System.out.println(threadLocalData.get());//輸出為Null }一般將
ThreadLocal變量聲明為公開(kāi)的靜態(tài)字段,方便線(xiàn)程對(duì)其進(jìn)行訪(fǎng)問(wèn),需要注意的是:直接使用構(gòu)造方法聲明ThreadLocal對(duì)象后,對(duì)象的初始值為 null。 -
ThreadLocal 對(duì)象初始化值
ThreadLocal類(lèi)提供了初始化接口protected T initialValue()我們可以通過(guò)繼承
ThreadLocal類(lèi)并復(fù)寫(xiě)initialValue()方法提供初始值,提供的初始值對(duì)所有線(xiàn)程都是可見(jiàn)的。public static ThreadLocal<Integer> threadLocalData = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return 1; } }; public static void main(String[] args) { System.out.println(threadLocalData.get());//輸出為1 }實(shí)例化 ThreadLocal 對(duì)象并將其初始值設(shè)置為 1。對(duì)所有線(xiàn)程來(lái)講,它們拿到的初始值都是 1。
-
讀取 ThreadLocal 值
get()方法用于獲取當(dāng)前線(xiàn)程的副本變量值
public T get() -
寫(xiě)入 ThreadLocal 值
set()方法用于寫(xiě)入當(dāng)前線(xiàn)程的副本變量值
public void set(T value) -
刪除 ThreadLocal 值
remove()方法移除當(dāng)前前程的副本變量值。每次 remove()之后都會(huì)對(duì)副本變量應(yīng)用一次 initialValue(),恢復(fù)副本的初始值。所以 remove()之后再 get()得到的是初始值。
public void remove() -
example
我們用一個(gè)簡(jiǎn)單的例子來(lái)說(shuō)明這幾個(gè)接口的使用方式。
//初始化ThreadLocal對(duì)象,并將其初始值設(shè)置為1,在Java8中使用Lambda構(gòu)造方式 public static ThreadLocal<Integer> threadLocalData = ThreadLocal.withInitial(() -> 1); public static void main(String[] args) { //先使用remove()移除值,再獲取值 new Thread(() -> { threadLocalData.remove(); System.out.println("remove()&get()" + threadLocalData.get());//輸出remove()&get()1 }).start(); //直接使用get()獲取值 new Thread(() -> System.out.println("get()" + threadLocalData.get())).start();//輸出get()1 //先使用set()設(shè)置值,再獲取 new Thread(() -> { threadLocalData.set(2); System.out.println("set()&get()" + threadLocalData.get());//輸出set()&get()2 }).start(); }
ThreadLocal 使用場(chǎng)景
什么場(chǎng)景適合使用ThreadLocal類(lèi)呢?
ThreadLocal 主要使用在多線(xiàn)程多實(shí)例(并且每個(gè)線(xiàn)程對(duì)應(yīng)一個(gè)實(shí)例狀態(tài))的對(duì)象訪(fǎng)問(wèn),并且不想顯式的為每個(gè)多線(xiàn)程對(duì)象以參數(shù)傳遞的形式來(lái)傳遞這個(gè)共享變量。總結(jié)起來(lái)還是比較繞口,分開(kāi)來(lái)看場(chǎng)景應(yīng)該滿(mǎn)足以下幾點(diǎn):
- 有一個(gè)共享變量,這個(gè)共享變量在應(yīng)用的全局域一般來(lái)說(shuō)只有一個(gè),也就是單例的。一般體現(xiàn)為使用
static final修飾。 - 這個(gè)共享變量是持有狀態(tài)的,也就是說(shuō)這個(gè)共享變量自身有一個(gè)初始值,但是又被多線(xiàn)程訪(fǎng)問(wèn),每個(gè)線(xiàn)程都會(huì)對(duì)這個(gè)共享值進(jìn)行讀取操作,但是又希望每個(gè)線(xiàn)程有自己的獨(dú)立的副本值。
- 要滿(mǎn)足前兩個(gè)條件,可以在
Thread對(duì)象中設(shè)置一個(gè)字段,用來(lái)存儲(chǔ)這個(gè)線(xiàn)程私有的狀態(tài),但是又不會(huì)采取線(xiàn)程類(lèi)成員變量的方式來(lái)實(shí)現(xiàn),那么就使用ThreadLocal類(lèi)來(lái)隱式的持有這個(gè)共享變量的副本。
我們來(lái)舉個(gè)常用的場(chǎng)景來(lái)說(shuō)明下。在 Web 開(kāi)發(fā)中,經(jīng)常需要對(duì)用戶(hù)的請(qǐng)求時(shí)間進(jìn)行格式化,一般服務(wù)端會(huì)生成一個(gè)當(dāng)前時(shí)間對(duì)象,然后將這個(gè)時(shí)間對(duì)象格式化為字符串類(lèi)型記錄到日志中,而格式化方法往往又是一個(gè)工具類(lèi),所有的線(xiàn)程都調(diào)用這個(gè)工具類(lèi)的格式化方法,就像下面這樣。
import javax.annotation.concurrent.NotThreadSafe;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
@NotThreadSafe
public class ErrorDateUtils {
private static final SimpleDateFormat FMT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String format(Date date) {
return FMT.format(date);
}
public static Date parse(String dateStr) throws ParseException {
return FMT.parse(dateStr);
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
Date now = new Date();
String format = format(now);
try {
Date parse = parse(format);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
/**
* 輸出:
* ........
* Exception in thread "Thread-98" java.lang.NumberFormatException: For input string: ""
* at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
* at java.lang.Long.parseLong(Long.java:601)
* ........
* at java.text.DateFormat.parse(DateFormat.java:364)
* ........
*/
由于SimpleDateFormat類(lèi)本身不是線(xiàn)程安全的,在多線(xiàn)程訪(fǎng)問(wèn)的情況下產(chǎn)生了異常,那么我們可以自然的想到使用同步對(duì)parse()和format()方法進(jìn)行處理。
import javax.annotation.concurrent.ThreadSafe;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
@ThreadSafe
public class SafeButSlowDateUtils {
private static final SimpleDateFormat FMT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static synchronized String format(Date date) {
return FMT.format(date);
}
public static synchronized Date parse(String dateStr) throws ParseException {
return FMT.parse(dateStr);
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
Date now = new Date();
String format = format(now);
try {
Date parse = parse(format);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
這樣經(jīng)過(guò)同步處理后確實(shí)不會(huì)發(fā)生線(xiàn)程安全問(wèn)題,但是在大規(guī)模并發(fā)訪(fǎng)問(wèn)時(shí)由于同步的存在每個(gè)用戶(hù)發(fā)起的請(qǐng)求都可能存在鎖競(jìng)爭(zhēng)的情況,拖慢系統(tǒng)的處理速度。當(dāng)然我們可以使用棧封閉,在每個(gè)線(xiàn)程中實(shí)例化SimpleDateFormat對(duì)象,這樣就一勞永逸的解決了線(xiàn)程安全問(wèn)題,例如:
public class SafeButNotGraceParseDateDemo {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
SimpleDateFormat formater = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date now = new Date();
String format = formater.format(now);
try {
Date parse = formater.parse(format);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
嗯,看起來(lái)不錯(cuò),但是這樣做就不再需要DateUtils工具類(lèi)了,并且這個(gè)日期格式轉(zhuǎn)換器每次都要初始化,無(wú)法復(fù)用。
現(xiàn)在看看我們遇到的情況:
- 我們需要全局有一個(gè)共享的
SimpleDateFormat對(duì)象(共享) - 這個(gè)全局共享的
SimpleDateFormat對(duì)象是持有狀態(tài)的,也就是格式化的格式字符串yyyy-MM-dd HH:mm:ss,并且會(huì)對(duì)每個(gè)線(xiàn)程的 Date 對(duì)象應(yīng)用這個(gè)格式化模式字符串進(jìn)行格式化。每個(gè)線(xiàn)程都會(huì)對(duì)這個(gè)共享對(duì)象進(jìn)行讀取操作。(共享對(duì)象有狀態(tài),且線(xiàn)程間狀態(tài)可能不一致) - 我們?yōu)榱藵M(mǎn)足復(fù)用性,不希望在每個(gè)線(xiàn)程中實(shí)例化,
SimpleDateFormat對(duì)象(不希望每個(gè)線(xiàn)程顯式持有狀態(tài))
滿(mǎn)足了以上的條件,我們就可以認(rèn)為,目前的場(chǎng)景非常適合使用ThreadLocal類(lèi)來(lái)解決問(wèn)題,共享變量SimpleDateFormat對(duì)象不變,只需要使用ThreadLocal來(lái)做線(xiàn)程綁定,這樣每個(gè)線(xiàn)程都持有SimpleDateFormat對(duì)象的副本,每個(gè)線(xiàn)程都持有私有的狀態(tài),是獨(dú)立互不干擾的。
import javax.annotation.concurrent.ThreadSafe;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
@ThreadSafe
public class SafeAndGraceDateUtils {
private static final ThreadLocal<SimpleDateFormat> FMT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy" +
"-MM-dd HH:mm:ss"));
public static String format(Date date) {
return FMT.get().format(date);
}
public static Date parse(String str) throws ParseException {
return FMT.get().parse(str);
}
public static void main(String[] args) throws ParseException {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
Date now1 = new Date();
String format = format(now1);
try {
Date parse = parse(format);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
使用ThreadLocal對(duì)SimpleDateFormat封裝后,每個(gè)線(xiàn)程都有一個(gè)獨(dú)立的SimpleDateFormat副本,狀態(tài)隔離,這樣就不會(huì)出現(xiàn)線(xiàn)程安全問(wèn)題了。
ThreadLocal 在 MyBatis 中的應(yīng)用
ThreadLocal 的多線(xiàn)程隔離數(shù)據(jù)副本的特性非常適合在管理數(shù)據(jù)庫(kù)連接中應(yīng)用。例如在 Mybatis 中SqlSessionManager中就使用了ThreadLocal進(jìn)行 session 管理。
我們知道SqlSessionManager負(fù)責(zé)維護(hù)管理SqlSession,SqlSessionManager本身是線(xiàn)程安全的,但是 DefaultSqlSession 卻并不是線(xiàn)程安全的。如果多個(gè)并發(fā)線(xiàn)程同時(shí)從SqlSessionManager獲取到同一個(gè)SqlSession實(shí)例,由于SqlSession實(shí)例中包含了數(shù)據(jù)庫(kù)操作相關(guān)的狀態(tài)信息,多個(gè)并發(fā)線(xiàn)程同時(shí)使用一個(gè)SqlSession實(shí)例對(duì)數(shù)據(jù)庫(kù)進(jìn)行讀寫(xiě)操作則會(huì)引起數(shù)據(jù)不一致錯(cuò)誤。所以 Mybatis 選擇了使用 ThreadLocal 來(lái)維護(hù) session,對(duì)每個(gè)線(xiàn)程存儲(chǔ)一個(gè) session 副本,這樣進(jìn)行了數(shù)據(jù)的隔離,防止出現(xiàn)線(xiàn)程安全問(wèn)題。
注意注釋標(biāo)明了Note that this class is not Thread-Safe.,DefaultSqlSession本身并不是線(xiàn)程安全的。
/**
*
* The default implementation for {@link SqlSession}.
* Note that this class is not Thread-Safe.
*
* @author Clinton Begin
*/
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
private final boolean autoCommit;
private boolean dirty;
private List<Cursor<?>> cursorList;
public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
this.configuration = configuration;
this.executor = executor;
this.dirty = false;
this.autoCommit = autoCommit;
}
在SqlSessionManager使用成員變量localSqlSession來(lái)維護(hù)數(shù)據(jù)庫(kù)會(huì)話(huà)。
/**
* @author Larry Meadors
*/
public class SqlSessionManager implements SqlSessionFactory, SqlSession {
private final SqlSessionFactory sqlSessionFactory;
private final SqlSession sqlSessionProxy;
private final 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 clearCache() {
final SqlSession sqlSession = localSqlSession.get();
if (sqlSession == null) {
throw new SqlSessionException("Error: Cannot clear the cache. No managed session is started.");
}
sqlSession.clearCache();
}
@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();
}
在獲取連接getConnection()等方法中,即使多線(xiàn)程訪(fǎng)問(wèn),也是使用localSqlSession.get()來(lái)獲取線(xiàn)程本地綁定的localSqlSession對(duì)象副本。
ThreadLocal 使用總結(jié)
概括起來(lái)說(shuō),對(duì)于多線(xiàn)程資源共享的問(wèn)題,同步機(jī)制采用了“以時(shí)間換空間”的方式,而 ThreadLocal另辟蹊徑采用了“以空間換時(shí)間”的方式來(lái)實(shí)現(xiàn)了數(shù)據(jù)的隔離。
前者僅提供一份變量,讓不同的線(xiàn)程排隊(duì)訪(fǎng)問(wèn),而后者為每一個(gè)線(xiàn)程都提供了一份變量,因此可以同時(shí)訪(fǎng)問(wèn)而互不影響。