目錄
1. ThreadLocal簡介
1.1 ThreadLocal基礎
1.1.1 ThreadLocal和Thread的關系
1.1.2 變量的生命周期
1.2 可繼承的ThreadLocal
2. ThreadLocal的應用案例
2.1 解決并發(fā)問題
2.1.1 java.lang.ThreadLocalRandom
2.1.2 HDFS中的Statistics:實現(xiàn)高并發(fā)下的統(tǒng)計功能
2.2 解決數(shù)據(jù)存儲問題
2.2.1 Struts2的ActionContext設計原理
2.2.2 Spring中thread scope Bean
3. 總結(jié)
1. ThreadLocal簡介
這篇博客主要對ThreadLocal類的基礎知識和實踐應用進行分析。文章的重點在于應用案例的探究,同時也會對理論基礎作簡單的介紹。
1.1 ThreadLocal基礎
為什么需要ThreadLocal
要理解為什么需要ThreadLocal就不得不從線程安全問題說起。高并發(fā)是很多領域都會遇到的非常棘手的問題,其最核心的問題在于如何平衡高性能和數(shù)據(jù)一致性。當我們說某個類是線程安全的時候,也就意味著該類在多線程環(huán)境下的狀態(tài)保持一致性。
所謂的一致性,就是關聯(lián)數(shù)據(jù)之間的邏輯關系是否正確和完整。
通過下面示例對數(shù)據(jù)一致性問題進行說明:
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
int nThreads = 10;
final Counter counter = new Counter();
ExecutorService exec = Executors.newFixedThreadPool(nThreads);
final CountDownLatch latch = new CountDownLatch(nThreads);
for(int i = 0; i < nThreads; i++){
exec.submit(new Runnable(){
public void run(){
for(int i = 0; i < 10000; i++){
counter.increase();
}
latch.countDown();
}
});
}
latch.await();
System.out.println("Expected:" + nThreads * 10000 + ",Actual:" + counter.count);
}
static class Counter{
int count = 0;
public void increase(){
this.count++;
}
}
輸出:
Expected:100000,Actual:71851
可見最終變量count的狀態(tài)并不符合預期的邏輯。對于并發(fā)問題來說,最簡單的解決辦法就是加鎖,本質(zhì)是并發(fā)訪問到串行訪問的改變。如下:
static class Counter{
int count = 0;
public synchronized void increase(){
this.count++;
}
}
輸出:
Expected:100000,Actual:100000
第一次實驗中,count變量的值之所以出現(xiàn)不正確的情況,是因為其被多個線程同時訪問,而且對某個線程來說,其它線程對變量count的操作結(jié)果,該線程是不一定可見的,這是造成count變量最終數(shù)據(jù)不一致的原因。而用synchronized修飾過后,串行訪問時就不存在不可見的情況。從而保證了count變量的正確性。那么是否可以換個思路:讓變量只能被一個線程訪問,這不就不存在之前談到的線程安全問題了嗎?
讓每個線程都保存一份變量的副本,該副本只會被隸屬的線程操作,這也就不存在線程安全問題了。這就是ThreadLocal的由來。
1.1.1 ThreadLocal和Thread的聯(lián)系
在上面提到了數(shù)據(jù)副本,那么線程如何保存該副本的呢?其實,Thread類中有一個ThreadLocalMap類型的變量threadLocals,定義如下:
public class Thread implements Runnable {
//。。。
ThreadLocal.ThreadLocalMap threadLocals = null;
//。。。
}
ThreadLocalMap是ThreadLocal的一個內(nèi)部類,其作用相當于一個HashMap,用于保存隸屬于該線程的變量副本。下面需要考慮一個問題:ThreadLocalMap的key和value該如何設計呢?
從API角度來說,ThreadLocal的作用是提供給client訪問Thread中threadLocals變量的訪問接口,每個ThreadLocal都對應著一個Thread內(nèi)部的變量副本。所以ThreadLocalMap中的key就是ThreadLocal對象(也就是該對象的hashCode),value也就是變量副本。一個對象默認的hashcode也就是該對象的引用值,這可以保證不同對象的hashcode不同。不過ThreadLocal并沒有使用這一默認值,而是內(nèi)部聲明了一個threadLocalHashCode整型變量用以存儲該對象的hashcode值:
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//。。。
}
變量副本的存儲問題已經(jīng)解決,那么怎么對Thread內(nèi)部的threadLocals變量進行訪問呢?這就要通過ThreadLocal了。下面對ThreadLocal的方法簡單介紹下:
- get()操作
public T get() {
Thread t = Thread.currentThread();//獲取當前Thread對象引用
ThreadLocalMap map = getMap(t);//從Thread對象中獲取ThreadLocalMap變量
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();//如果是第一次訪問,就setInitialValue進行初始化
}
private T setInitialValue() {
//initialValue方法是protected修飾的,默認返回null,所以需要在ThreadLocal子類中進行覆蓋。
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
- set操作
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
和setInitialValue幾乎一致,不同的是:set操作會傳入需要設置的value。而setInitialValue需要通過initialValue()獲取初始值。
- remove操作
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
1.1.2 變量的生命周期
這里所的變量指的是存儲在Thread對象中的變量副本。下面從init-service-destroy三個階段分析下其生命周期:
- Init
第一次調(diào)用get方法的時候完成了初始化過程。這也就是為什么需要覆蓋ThreadLocal的initialValue方法。在setInitialValue方法中的createMap方法如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
Service
只要線程活著且ThreadLocal可訪問即處于Service階段。Destroy
由于threadLocals變量是Thread的成員,那么當Thread對象掛了后,那么其內(nèi)部的所有成員也都被gc了。此外,通過ThreadLocal提供remove方法也可以將threadLocals里的特定副本變量移除。
ThreadLocal變量的生命周期呢?由于ThreadLocal變量通常用private static修飾,也就是屬于類成員
變量。所以其生命周期當然也就和該類一致。
1.2 可繼承的ThreadLocal
首先看個實例:
static class Context {
private static final ThreadLocal<HashMap<String,String>> CONTEXT1 = new ThreadLocal<HashMap<String,String>>(){
protected HashMap<String,String> initialValue(){
return new HashMap<String,String>();
}
};
private static final InheritableThreadLocal<HashMap<String,String>> CONTEXT2 = new InheritableThreadLocal<HashMap<String,String>>(){
protected HashMap<String,String> initialValue(){
return new HashMap<String,String>();
}
};
public static HashMap<String,String> getContext1() {
return CONTEXT1.get();
}
public static HashMap<String,String> getContext2() {
return CONTEXT2.get();
}
}
public static void main(String[] args) throws InterruptedException {
Context.getContext1().put("name", "wqx");
Context.getContext2().put("name", "wqx");
Thread thread = new Thread(new Runnable(){
@Override
public void run() {
System.out.println("name:" + Context.getContext1().get("name"));
System.out.println("name:" + Context.getContext2().get("name"));
}
});
thread.start();
}
輸出:
name:null
name:wqx
字面意思上理解InheritableThreadLocal即為可繼承的ThreadLocal,這里的可繼承的含義指的是子線程在實例化過程中,會查看當前執(zhí)行線程(可以理解為父線程)的inheritableThreadLocals是否為null,如果不為null,則將該變量賦值給子線程的inheritableThreadLocals。下面是Thread類構(gòu)造函數(shù)中的相關片段:
Thread parent = currentThread();//當前線程,也就是執(zhí)行new Thread()的線程
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
2. ThreadLocal的應用案例
2.1 解決并發(fā)問題
2.1.1 java.lang.ThreadLocalRandom
在Java中隨機數(shù)可以用Random類,下面是java.util.Random的生成隨機數(shù)的方法:
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}
可見,其中通過CAS方式保證其線程安全性。這在高并發(fā)的環(huán)境中由于線程間的競爭必然帶來一定的性能損耗。ThreadLocal此時就派上用場了,ThreadLocalRandom是通過ThreadLocal改進的用于隨機數(shù)生成的工具類,每個線程單獨持有一個ThreadLocalRandom對象引用,這就完全杜絕了線程間的競爭問題。
public class ThreadLocalRandom extends Random {
//。。。
private static final ThreadLocal<ThreadLocalRandom> localRandom =
new ThreadLocal<ThreadLocalRandom>() {
protected ThreadLocalRandom initialValue() {
return new ThreadLocalRandom();
}
};
//。。。
}
ThreadLocalRandom能用于全局范圍的隨機數(shù)生成嗎?每個線程都持有一個ThreadLocalRandom對象,生成的隨機數(shù)不會重復嗎?考慮到ThreadLocal的特點,理論上也就不應該將其用于全局范圍,其更適合于線程獨享變量的存儲。But!凡事都有例外,下面看個例外的用法。
2.1.2 HDFS中的Statistics:實現(xiàn)高并發(fā)下的統(tǒng)計功能
Hadoop的分布式文件系統(tǒng)(HDFS)是其生態(tài)的基石,MR任務中涉及到的數(shù)據(jù)輸入輸出都與其密切相關。對于FileSystem來說,對大量的讀寫操作進行統(tǒng)計是非常必要的。這該如何實現(xiàn)呢?
方案一:通過加鎖的方式??紤]到Hadoop處理的數(shù)據(jù)體量及對數(shù)據(jù)操作的頻率,加鎖帶來的性能損耗不可忽視,So。。。PASS!
方案二:ThreadLocal可以嗎?對當前FileSystem進行操作的線程很多,如果只使用ThreadLocal方案的話,只能統(tǒng)計一個線程的操作次數(shù),那么在匯總操作的時候必然要進行同步synchronized處理。這可行嗎?判斷一個方案可不可行,必須要具體業(yè)務邏輯具體分析,在本例中,statistics是用于存儲統(tǒng)計數(shù)據(jù)的對象,那么對FileSystem進行操作(比如:create、mkdir、list、delete等)的同時都會記錄在statistics對象中,也就是對statistics對象進行寫操作,而對于統(tǒng)計數(shù)據(jù)的讀操作比較少。所以Hadoop考慮到寫多讀少的事實,ThreadLocal方案是可以接受的。
下面是Statistics對象的部分實現(xiàn):
public static final class Statistics {
/**
* Statistics data.
* /
public static class StatisticsData {
volatile long bytesRead;
volatile long bytesWritten;
volatile int readOps;
volatile int largeReadOps;
volatile int writeOps;
//。。。
}
//allData保存的是所有線程中StatisticsData對象的引用
private final Set<StatisticsDataReference> allData;
//ThreadLocal變量
private final ThreadLocal<StatisticsData> threadData;
public void incrementBytesWritten(long newBytes) {
getThreadStatistics().bytesWritten += newBytes;
}
public StatisticsData getThreadStatistics() {
StatisticsData data = threadData.get();
if (data == null) { //第一次統(tǒng)計操作時需要進行初始化,并與allData進行關聯(lián)
data = new StatisticsData();
threadData.set(data);
StatisticsDataReference ref =
new StatisticsDataReference(data, Thread.currentThread());
synchronized(this) {
allData.add(ref);
}
}
return data;
}
//。。。
}
下面是DistributedFileSystem中刪除操作的實現(xiàn),可見在每次執(zhí)行刪除操作的時候,都會通過statistics進行記錄。
public class DistributedFileSystem extends FileSystem {
@Override
public boolean delete(Path f, final boolean recursive) throws IOException {
statistics.incrementWriteOps(1);
// 。。。
}
}
如果需要獲取統(tǒng)計數(shù)據(jù)時,就要將所有線程內(nèi)部的統(tǒng)計數(shù)據(jù)進行累加,這肯定需要進行同步處理的。如下所示的是獲取統(tǒng)計數(shù)據(jù)中所有寫操作的次數(shù):
public long getBytesWritten() {
return visitAll(new StatisticsAggregator<Long>() {
private long bytesWritten = 0;
@Override
public void accept(StatisticsData data) {
bytesWritten += data.bytesWritten;
}
public Long aggregate() {
return bytesWritten;
}
});
}
//加鎖處理,保證統(tǒng)計數(shù)據(jù)的正確性
private synchronized <T> T visitAll(StatisticsAggregator<T> visitor) {
visitor.accept(rootData);
for (StatisticsDataReference ref: allData) {
StatisticsData data = ref.getData();
visitor.accept(data);
}
return visitor.aggregate();
}
在寫多讀少的環(huán)境下,這種方案可以有效的解決傳統(tǒng)“加鎖”方案帶來的多線程間的競爭。Brilliant idea!
2.2 解決數(shù)據(jù)存儲問題
2.2.1 Struts2的ActionContext設計原理
Struts2是使用較為廣泛的MVC框架,其關于請求響應流程的設計思路也是很新穎的。當?shù)谝淮谓佑|Struts2的時候,曾一直困惑于一個問題:Action中的每個方法的請求參數(shù)怎么獲得的?處理結(jié)果又是如何返回的?在傳統(tǒng)的Servlet中,我們可以通過函數(shù)入?yún)ttpServletRequest對象獲取請求參數(shù),可以通過入?yún)ttpServletResponse對象向輸出流寫入響應數(shù)據(jù)。而Struts2中自定義的Action的每個方法都沒有入?yún)ⅲ姨幚砗蟮捻憫獢?shù)據(jù)也不是當作返回值返回的。
Struts2的最大亮點也許就是對數(shù)據(jù)流和控制流的解耦。數(shù)據(jù)不再需要作為方法參數(shù)傳入或作為返回值返回。Struts2的返回值僅僅作為控制流的標識(比如:選擇哪個視圖)。Struts2中數(shù)據(jù)載體就是ActionContext。不管是請求參數(shù)亦或是處理后的響應數(shù)據(jù)都被封裝在ActionContext內(nèi)部。開發(fā)者一般常接觸的是ActionContext的子類ServletActionContext。
首先看下Struts2中幾個主要組件的示意圖:

ActionContext作為數(shù)據(jù)載體,與每個組件都會有數(shù)據(jù)交互,如:ActionInvocation、Interceptor、Action、Result等。這幾乎涵蓋了一個請求的整個生命周期。這里說的請求的生命周期可以泛指處理請求的線程的生命周期。ThreadLocal不正適合這種情況嗎?下面看下com.opensymphony.xwork2.ActionContext類的部分結(jié)構(gòu):
public class ActionContext implements Serializable {
//。。。
static ThreadLocal<ActionContext> actionContext = new ThreadLocal<ActionContext>();
private Map<String, Object> context;
public ActionContext(Map<String, Object> context) {
this.context = context;
}
public static ActionContext getContext() {
return actionContext.get();
}
public Map<String, Object> getContextMap() {
return context;
}
//。。。
ActionContext是典型的ThreadLocal使用案例,通過將請求處理過程中涉及到的所有參數(shù)封裝進ActionContext中,從而實現(xiàn)了數(shù)據(jù)流和控制流的分離,這一解耦思路值得好好學習。Another brilliant idea!
2.2.1 Spring中thread scope Bean
在Spring中,如果按照Bean的生命周期對其進行劃分,那么大致可以分為這么幾類:Singleton、Prototype、Request、Session、Thread Scope等。這一節(jié)主要介紹ThreadScope的Bean如何實現(xiàn)。經(jīng)過上面的各種案例分析,這個問題就灰常容
易解決了,只需要將Bean的生命周期與Thread同步就行。ThreadLocal正合適。下面是Spring內(nèi)部已經(jīng)實現(xiàn)的方案SimpleThreadScope:
public class SimpleThreadScope implements Scope {
private final ThreadLocal<Map<String, Object>> threadScope =
new NamedThreadLocal<Map<String, Object>>("SimpleThreadScope") {
@Override
protected Map<String, Object> initialValue() {
return new HashMap<String, Object>();
}
};
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Map<String, Object> scope = this.threadScope.get();
Object object = scope.get(name);
if (object == null) {
object = objectFactory.getObject();
scope.put(name, object);
}
return object;
}
//。。。
}
3. 總結(jié)
上面小節(jié)中分別分析了ThreadLocal的兩個主要的應用領域:1.解決并發(fā)問題。2.解決數(shù)據(jù)存儲問題。其中解決并發(fā)問題的本質(zhì)是一種以空間換時間的思路,時間效率提升了,但是也存在著內(nèi)存使用時的潛在溢出風險。數(shù)據(jù)存儲問題主要指的是:系統(tǒng)中多個組件如何實現(xiàn)數(shù)據(jù)的交互和共享,而作為執(zhí)行者的線程作為數(shù)據(jù)載體再適合不過了。雖然各種組件可以實現(xiàn)數(shù)據(jù)共享,但是數(shù)據(jù)在線程間是隔離的。