12.23 為什麼Netty的FastThreadLocal速度快

my.oschina.net/OutOfMemory/blog/3117161

前言

最近在看netty源碼的時候發現了一個叫FastThreadLocal的類,jdk本身自帶了ThreadLocal類,所以可以大致想到此類比jdk自帶的類速度更快,主要快在什麼地方,以及為什麼速度更快,下面做一個簡單的分析;

性能測試

ThreadLocal主要被用在多線程環境下,方便的獲取當前線程的數據,使用者無需關心多線程問題,方便使用;為了能說明問題,分別對兩個場景進行測試,分別是:多個線程操作同一個ThreadLocal,單線程下的多個ThreadLocal,下面分別測試:

1.多個線程操作同一個ThreadLocal

分別對ThreadLocal和FastThreadLocal使用測試代碼,部分代碼如下:

<code>publicstaticvoidtest2()throwsException{
CountDownLatchcdl=newCountDownLatch(10000);
ThreadLocal<string>threadLocal=newThreadLocal<string>();
longstarTime=System.currentTimeMillis();
for(inti=0;i<10000;i++){
newThread(newRunnable(){

@Override
publicvoidrun(){
threadLocal.set(Thread.currentThread().getName());
for(intk=0;k<100000;k++){
threadLocal.get();
}
cdl.countDown();
}
},"Thread"+(i+1)).start();
}
cdl.await();
System.out.println(System.currentTimeMillis()-starTime+"ms");
}
/<string>/<string>/<code>

以上代碼創建了10000個線程,同時往ThreadLocal設置,然後get十萬次,然後通過CountDownLatch來計算總的時間消耗,運行結果為:1000ms左右;

下面再對FastThreadLocal進行測試,代碼類似:

<code>publicstaticvoidtest2()throwsException{
CountDownLatchcdl=newCountDownLatch(10000);
FastThreadLocal<string>threadLocal=newFastThreadLocal<string>();
longstarTime=System.currentTimeMillis();
for(inti=0;i<10000;i++){
newFastThreadLocalThread(newRunnable(){

@Override
publicvoidrun(){
threadLocal.set(Thread.currentThread().getName());
for(intk=0;k<100000;k++){
threadLocal.get();
}
cdl.countDown();
}
},"Thread"+(i+1)).start();
}

cdl.await();
System.out.println(System.currentTimeMillis()-starTime);
}
/<string>/<string>/<code>

運行之後結果為:1000ms左右;可以發現在這種情況下兩種類型的ThreadLocal在性能上並沒有什麼差距,下面對第二種情況進行測試;

2.單線程下的多個ThreadLocal

分別對ThreadLocal和FastThreadLocal使用測試代碼,部分代碼如下:

<code>publicstaticvoidtest1()throwsInterruptedException{
intsize=10000;
ThreadLocal<string>tls[]=newThreadLocal[size];

for(inti=0;i<size>tls[i]=newThreadLocal<string>();
}

newThread(newRunnable(){
@Override
publicvoidrun(){
longstarTime=System.currentTimeMillis();
for(inti=0;i<size>tls[i].set("value"+i);
}
for(inti=0;i<size>for(intk=0;k<100000;k++){
tls[i].get();
}
}
System.out.println(System.currentTimeMillis()-starTime+"ms");
}
}).start();
}
/<size>/<size>/<string>/<size>/<string>/<code>

以上代碼創建了10000個ThreadLocal,然後使用同一個線程對ThreadLocal設值,同時get十萬次,運行結果:2000ms左右;

下面再對FastThreadLocal進行測試,代碼類似:

<code>publicstaticvoidtest1(){
intsize=10000;
FastThreadLocal<string>tls[]=newFastThreadLocal[size];
for(inti=0;i<size>tls[i]=newFastThreadLocal<string>();
}

newFastThreadLocalThread(newRunnable(){

@Override
publicvoidrun(){
longstarTime=System.currentTimeMillis();
for(inti=0;i<size>tls[i].set("value"+i);
}
for(inti=0;i<size>for(intk=0;k<100000;k++){
tls[i].get();
}
}
System.out.println(System.currentTimeMillis()-starTime+"ms");
}
}).start();
}
/<size>/<size>/<string>/<size>/<string>/<code>

運行結果:30ms左右;可以發現性能達到兩個數量級的差距,當然這是在大量訪問次數的情況下才有的效果;下面重點分析一下ThreadLocal的機制,以及FastThreadLocal為什麼比ThreadLocal更快;

ThreadLocal的機制

因為我們常用的就是set和get方法,分別看一下對應的源碼:

<code>publicvoidset(Tvalue){
Threadt=Thread.currentThread();
ThreadLocalMapmap=getMap(t);
if(map!=null)
map.set(this,value);
else
createMap(t,value);
}

ThreadLocalMapgetMap(Threadt){
returnt.threadLocals;
}
/<code>

以上代碼大致意思:首先獲取當前線程,然後獲取當前線程中存儲的threadLocals變量,此變量其實就是ThreadLocalMap,最後看此ThreadLocalMap是否為空,為空就創建一個新的Map,不為空則以當前的ThreadLocal為key,存儲當前value;可以進一步看一下ThreadLocalMap中的set方法:

<code>privatevoidset(ThreadLocal>key,Objectvalue){

//Wedon'tuseafastpathaswithget()becauseitisat
//leastascommontouseset()tocreatenewentriesas
//itistoreplaceexistingones,inwhichcase,afast
//pathwouldfailmoreoftenthannot.

Entry[]tab=table;
intlen=tab.length;
inti=key.threadLocalHashCode&(len-1);


for(Entrye=tab[i];
e!=null;
e=tab[i=nextIndex(i,len)]){
ThreadLocal>k=e.get();

if(k==key){
e.value=value;
return;
}

if(k==null){
replaceStaleEntry(key,value,i);
return;
}
}

tab[i]=newEntry(key,value);
intsz=++size;
if(!cleanSomeSlots(i,sz)&&sz>=threshold)
rehash();
}
/<code>

大致意思:ThreadLocalMap內部使用一個數組來保存數據,類似HashMap;每個ThreadLocal在初始化的時候會分配一個threadLocalHashCode,然後和數組的長度進行取模操作,所以就會出現hash衝突的情況,在HashMap中處理衝突是使用數組+鏈表的方式,而在ThreadLocalMap中,可以看到直接使用nextIndex,進行遍歷操作,明顯性能更差;下面再看一下get方法:

<code>publicTget(){
Threadt=Thread.currentThread();
ThreadLocalMapmap=getMap(t);
if(map!=null){
ThreadLocalMap.Entrye=map.getEntry(this);
if(e!=null){
@SuppressWarnings("unchecked")
Tresult=(T)e.value;
returnresult;
}
}
returnsetInitialValue();
}

/<code>

同樣是先獲取當前線程,然後獲取當前線程中的ThreadLocalMap,然後以當前的ThreadLocal為key,到ThreadLocalMap中獲取value:

<code>privateEntrygetEntry(ThreadLocal>key){
inti=key.threadLocalHashCode&(table.length-1);
Entrye=table[i];
if(e!=null&&e.get()==key)
returne;
else
returngetEntryAfterMiss(key,i,e);
}

privateEntrygetEntryAfterMiss(ThreadLocal>key,inti,Entrye){
Entry[]tab=table;
intlen=tab.length;

while(e!=null){
ThreadLocal>k=e.get();
if(k==key)
returne;
if(k==null)
expungeStaleEntry(i);
else
i=nextIndex(i,len);
e=tab[i];
}
returnnull;
}
/<code>

同set方式,通過取模獲取數組下標,如果沒有衝突直接返回數據,否則同樣出現遍歷的情況;所以通過分析可以大致知道以下幾個問題:

  1. ThreadLocalMap是存放在Thread下面的,ThreadLocal作為key,所以多個線程操作同一個ThreadLocal其實就是在每個線程的ThreadLocalMap中插入的一條記錄,不存在任何衝突問題;
  2. ThreadLocalMap在解決衝突時,通過遍歷的方式,非常影響性能;
  3. FastThreadLocal通過其他方式解決衝突的問題,達到性能的優化;

下面繼續來看一下FastThreadLocal是通過何種方式達到性能的優化。

為什麼Netty的FastThreadLocal速度快

Netty中分別提供了FastThreadLocal和FastThreadLocalThread兩個類,FastThreadLocalThread繼承於Thread,下面同樣對常用的set和get方法來進行源碼分析:

<code>publicfinalvoidset(Vvalue){
if(value!=InternalThreadLocalMap.UNSET){
set(InternalThreadLocalMap.get(),value);
}else{
remove();
}
}

publicfinalvoidset(InternalThreadLocalMapthreadLocalMap,Vvalue){
if(value!=InternalThreadLocalMap.UNSET){
if(threadLocalMap.setIndexedVariable(index,value)){
addToVariablesToRemove(threadLocalMap,this);
}
}else{
remove(threadLocalMap);
}
}
/<code>

此處首先對value進行判定是否為InternalThreadLocalMap.UNSET,然後同樣使用了一個InternalThreadLocalMap用來存放數據:

<code>publicstaticInternalThreadLocalMapget(){
Threadthread=Thread.currentThread();
if(threadinstanceofFastThreadLocalThread){
returnfastGet((FastThreadLocalThread)thread);
}else{

returnslowGet();
}
}

privatestaticInternalThreadLocalMapfastGet(FastThreadLocalThreadthread){
InternalThreadLocalMapthreadLocalMap=thread.threadLocalMap();
if(threadLocalMap==null){
thread.setThreadLocalMap(threadLocalMap=newInternalThreadLocalMap());
}
returnthreadLocalMap;
}
/<code>

可以發現InternalThreadLocalMap同樣存放在FastThreadLocalThread中,不同在於,不是使用ThreadLocal對應的hash值取模獲取位置,而是直接使用FastThreadLocal的index屬性,index在實例化時被初始化:

<code>privatefinalintindex;

publicFastThreadLocal(){
index=InternalThreadLocalMap.nextVariableIndex();
}
/<code>

再進入nextVariableIndex方法中:

<code>staticfinalAtomicIntegernextIndex=newAtomicInteger();

publicstaticintnextVariableIndex(){
intindex=nextIndex.getAndIncrement();
if(index<0){
nextIndex.decrementAndGet();
thrownewIllegalStateException("toomanythread-localindexedvariables");
}
returnindex;
}
/<code>

在InternalThreadLocalMap中存在一個靜態的nextIndex對象,用來生成數組下標,因為是靜態的,所以每個FastThreadLocal生成的index是連續的,再看一下InternalThreadLocalMap中是如何setIndexedVariable的:

<code>publicbooleansetIndexedVariable(intindex,Objectvalue){
Object[]lookup=indexedVariables;

if(index<lookup.length>ObjectoldValue=lookup[index];
lookup[index]=value;
returnoldValue==UNSET;
}else{
expandIndexedVariableTableAndSet(index,value);
returntrue;
}
}
/<lookup.length>/<code>

indexedVariables是一個對象數組,用來存放value;直接使用index作為數組下標進行存放;如果index大於數組長度,進行擴容;get方法直接通過FastThreadLocal中的index進行快速讀取:

<code>publicfinalVget(InternalThreadLocalMapthreadLocalMap){
Objectv=threadLocalMap.indexedVariable(index);
if(v!=InternalThreadLocalMap.UNSET){
return(V)v;
}

returninitialize(threadLocalMap);
}

publicObjectindexedVariable(intindex){
Object[]lookup=indexedVariables;
returnindex<lookup.length>}
/<lookup.length>/<code>

直接通過下標進行讀取,速度非常快;但是這樣會有一個問題,可能會造成空間的浪費;

總結

通過以上分析我們可以知道在有大量的ThreadLocal進行讀寫操作的時候,才可能會遇到性能問題;另外FastThreadLocal通過空間換取時間的方式來達到O(1)讀取數據;還有一個疑問就是內部為什麼不直接使用HashMap(數組+黑紅樹)來代替ThreadLocalMap。


分享到:


相關文章: