netty在內存分配中大量使用了線程本地緩存,并對ThreadLocal進行了擴展。
## 9.3.1 java的ThreadLocal
**使用**
ThreadLocal為每個線程存儲了其對應的副本,下面的例子中,我們啟動多個線程,每個線程保存不同的數據。
```
public class JavaThreadLocalDemo2 {
public static void main(String[] args) {
Task task = new Task();
Thread tasks[] = new Thread[10];
for(int i=0;i<10;i++) {
tasks[i] = new Thread(task);
}
for(int i=0;i<10;i++) {
tasks[i].start();
}
}
static class Task implements Runnable{
ThreadLocal<Float> local = new ThreadLocal<Float>();
@Override
public void run() {
Float process = new Random().nextFloat();
local.set(process);
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() +" "+local.get() +" "+ (local.get()==process));
}
}
}
```
**實現**
每個Thread實例的內部有一個ThreadLocal.ThreadLocalMap對象的實例threadLocals,內部有一個Entry[] table用來保存threadLocal和其對應的Object;每個ThreadLocal有一個唯一的threadLocalHashCode,每次調用set時,先獲取當前線程的ThreadLocalMap,然后使用map的set方法將ThreadLocal和value設置進去。
```
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
```
map的set中,首先根據ThreadLocal的哈希值獲取其在table中的位置,如果該位置內容為null,保存到這個元素上即可;如果該位置元素不為null且key為相同ThreadLocal,替換value;如果該位置元素不為null且key為null(出現null的原因是由于Entry的key是繼承了軟引用,在下一次GC時不管它有沒有被引用都會被回收掉,Value不會被回收)。當出現null時,會調用replaceStaleEntry()方法接著循環尋找相同的key,如果存在,直接替換舊值。如果不存在,則在當前位置上重新創建新的Entry。如果該位置元素不為相同ThreadLocal且不為null,說明其他ThreadLocal已經使用,遍歷鏈表查找其他位置。
保存到table之后,cleanSomeSlots會查詢是否有過期的元素,如果有并且大于閥值(超過2/3),執行rehash()
```
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = 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] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
```
map的get方法中,獲取當前線程的ThreadLocalMap后,在getEntry中獲取ThreadLocal對于的值。getEntry方法會根據index查找,由于可能發生沖突,會調用getEntryAfterMiss遍歷數組。
```
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
```
通過對java的ThreadLocal實現的了解,我們可以看到,在set時會發生沖突,會遍歷尋找合適的位置,而在get時如果首次未命中,也會遍歷尋找ThreadLocal對應的值。在Netty高并發中,會有頻繁的讀取,因此Netty自己實現了ThreadLocal以提高效率。
# Netty的ThreadLocal
首先,Netty將線程分為了兩種,一種是Netty實現的可快速存取本地緩存的FastThreadLocalThread,一種是普通的Thread。這點在線程池創建過程中使用的`DefaultThreadFactory`中可以看到,線程池創建的線程都是FastThreadLocalThread。FastThreadLocalThread中有一個變量InternalThreadLocalMap保存本地的相關數據。
在線程中,netty講線程的本地變量都保存在InternalThreadLocalMap中,這個對象看名字是個Map,但其實里面由數組indexedVariables保存設置的本地變量。數組indexedVariables的大小默認為32,每個線程內部都有一個InternalThreadLocalMap的實例,可以設置多個ThreadLocal,這個ThreadLocal不是Java默認的ThreadLocal,而是使用FastThreadLocal,每個FastThreadLocal有一個唯一的Id保存在index中,對應著FastThreadLocalThread內部的InternalThreadLocalMap的位置。因此,netty相比Java的ThreadLocal來說,每個key都有固定的index,這樣不會發生沖突,從而提高了效率。
netty中如果使用的是普通線程的ThreadLocal,那么會在Thread的ThreadLocalMap中保存InternalThreadLocalMap,FastThreadLocal存放在其index對應的位置上。

# 測試
CompareJavaTL使用java的ThreadLocal,寫/讀時間為338ms和248ms
CompareNettyTL使用Netty的FastThreadLocal,寫/讀時間為283ms和8ms
可見,Netty的ThreadLocal機制在讀寫時效率都要比java的高,根據之前的分析,
```
public class CompareJavaTL {
static int count = 100000000;
static int times = 100000000;
public static void main(String[] args) {
ThreadLocal<String> tl = new ThreadLocal<String>();
long begin = System.currentTimeMillis();
for(int i=0;i<count;i++) {
tl.set("javatl");
}
System.out.println(System.currentTimeMillis()-begin);
begin = System.currentTimeMillis();
for(int i=0;i<times;i++) {
tl.get();
}
System.out.println(System.currentTimeMillis()-begin);
}
}
```
```
public class CompareNettyTL {
static int count = 100000000;
static int times = 100000000;
public static void main(String[] args) {
new FastThreadLocalThread(new Runnable() {
@Override
public void run() {
FastThreadLocal<String> tl = new FastThreadLocal<String>();
long begin = System.currentTimeMillis();
for(int i=0;i<count;i++) {
tl.set("javatl");
}
System.out.println(System.currentTimeMillis()-begin);
begin = System.currentTimeMillis();
for(int i=0;i<times;i++) {
tl.get();
}
System.out.println(System.currentTimeMillis()-begin);
}
}).start();
}
}
```