前言
Github:https://github.com/yihonglei/thinking-in-concurrent
一 ThreadLocal原理
ThreadLocal的作用就是让每个线程绑定自己的局部变量,用于存储每个线程的私有数据,
在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量传递的复杂性。
1、创建一个ThreadLocal对象
private ThreadLocal myThreadLocal = new ThreadLocal();
实例化了一个ThreadLocal对象。每个线程仅需要实例化一次即可。
虽然不同的线程执行同一段代码时,访问同一个ThreadLocal变量,但是每个线程只能看到私有的ThreadLocal实例。
所以不同的线程在给ThreadLocal对象设置不同的值时,他们也不能看到彼此的修改。
2、访问ThreadLocal对象
一旦创建了一个ThreadLocal对象,你就可以通过以下方式来存储此对象的值:
myThreadLocal.set("A thread local value");
也可以直接读取一个ThreadLocal对象的值:
String threadLocalValue = (String) myThreadLocal.get();
get()方法会返回一个Object对象,而set()方法则依赖一个Object对象参数。
3、ThreadLocal泛型
为了使get()方法返回值不用做强制类型转换,通常可以创建一个泛型化的ThreadLocal对象。
private ThreadLocal<String> myThreadLocal1 = new ThreadLocal();
现在你可以存储一个字符串到ThreadLocal实例里,此外,当你从此ThreadLocal实例中获取值的时候,
就不必要做强制类型转换。
myThreadLocal1.set("Hello ThreadLocal");
String threadLocalValues = myThreadLocal.get();
4、初始化ThreadLocal
由于ThreadLocal对象的set()方法设置的值只对当前线程可见,那有什么方法可以为ThreadLocal对象设置的值对
所有线程都可见。为此,我们可以通过ThreadLocal子类的实现,并覆写initialValue()方法,就可以为ThreadLocal对象
指定一个初始化值。如下所示:
private ThreadLocal myThreadLocal = new ThreadLocal<String>() {
@Override protected String initialValue() {
return "This is the initial value";
}
};
此时,在set()方法调用前,当调用get()方法的时候,所有线程都可以看到同一个初始化值。
二 ThreadLocal实战
1、ThreadLocal类的get方法和null值
package com.jpeony.concurrent.threadlocal;
/**
* ThreadLocal类的get方法和null值
*
* @author yihonglei
*/
public class RunSimpleTest {
public static ThreadLocal tl = new ThreadLocal();
public static void main(String[] args) {
if (tl.get() == null) {
System.out.println("未放入值");
tl.set("放入值");
}
// 获取值
System.out.println(tl.get());
}
}
当第一次调用get方法时,由于没有放入过值,返回null,通过set方法放入值,之后通过get方法可以获取当前线程的私有数据。
2、ThreadLocal隔离线实验
创建一个数据工具类:
package com.jpeony.concurrent.threadlocal;
/**
* @author yihonglei
*/
public class DataTools {
public static ThreadLocal<String> tl = new ThreadLocal<>();
}
创建线程A:
package com.jpeony.concurrent.threadlocal;
/**
* @author yihonglei
*/
public class ThreadA extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
DataTools.tl.set("A_Thread_" + System.currentTimeMillis());
// 获取当前线程私有数据
System.out.println("线程A获取的值:" + DataTools.tl.get());
Thread.sleep(200);
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
创建线程B:
package com.jpeony.concurrent.threadlocal;
/**
* @author yihonglei
*/
public class ThreadB extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
DataTools.tl.set("B_Thread_" + System.currentTimeMillis());
// 获取当前线程私有数据
System.out.println("线程B获取的值:" + DataTools.tl.get());
Thread.sleep(200);
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
测试类:
package com.jpeony.concurrent.threadlocal;
/**
* @author yihonglei
*/
public class RunTest {
public static void main(String[] args) {
try {
ThreadA threadA = new ThreadA();
threadA.start();
Thread.sleep(3000);
ThreadB threadB = new ThreadB();
threadB.start();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
运行结果:
从程序运行结果,线程获取属于自己的私有数据,证明了ThreadLocal实现线程私有数据保证数据的隔离性。
3、如何让get方法第一次获取的值不为null
写一个类继承与ThreadLocal,重写initialValue()方法,设置get的默认值。
package com.jpeony.concurrent.threadlocal;
/**
* 重写ThreadLocal初始化方法,自定义初始值
*
* @author yihonglei
*/
public class ThreadLocalExt extends ThreadLocal {
@Override
protected Object initialValue() {
//return super.initialValue();
return "设置默认值,第一次获取不在为null";
}
}
三 ThreadLocal源码分析
1、ThreadLocal源码结构
1.1 主要结构说明
SuppliedThreadLocal:jdk8的增强,支持lambda表达式;
ThreadLocalMap:ThreadLocal内部数据结构,类似Map,但是key是对应ThreadLocal对象的弱引用;
initilaValue():ThreadLocal初始值;
set(T):往ThreadLocalMap放值;
get():从ThreadLocalMap取值;
1.2 ThreadLocal如何实现线程私有的?
在Thread里面,有一个局部变量ThreadLocal.ThreadLocalMap threadLocals = null;
通过这个局部变量维护线程自己的一份数据,这个局部变量值持续到线程结束。
至于这个ThreadLocalMap是个什么,下面分析它在ThreadLocal中的地位和数据结构。
Thread的局部变量threadLocals是私有的,数据结构是ThreadLocal的ThreadLocalMap,如果一个线程想拥有
多个ThreadLocal,则new多个ThreadLocal对象,根据对象计算key,都存在ThreadLoca.ThreadLocalMap数据结构里面。
2、set过程源码分析
2.1 set(T value)
set值
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 判断是否已经存在ThreadLocalMap
if (map != null)
// 添加值到ThreadLocalMap
map.set(this, value);
else
// 创建ThreadLocalMap并添加值
createMap(t, value);
}
2.2 getMap(Thread t)
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
threadLocals为ThreadLocal的内部类成员变量ThreadLocalMap,第一次添加值获取的是null,这个时候还没有初始化。
ThreadLocal.ThreadLocalMap threadLocals = null;
2.3 createMap(Thread t, T firstValue):
void createMap(Thread t, T firstValue) {
// 初始化并set值
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
我们先看下ThreadLocalMap是啥?
2.4 ThreadLocalMap是啥?
ThreadLocalMap是ThreadLocal静态内部类,专为ThreadLocal定制的高效实现,基于弱引用的垃圾清理机制。
ThreadLocalMap源码结构:
ThreadLocalMap存储结构:
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*
* key是对ThreadLocal的弱引用,value是一个Object,存储要放入ThreadLocal的值。
* 当没有引用的时候,Entry自动从table中剔除。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
// 存储要放入ThreadLocal的值
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
结构类似Map,Entry继承WeakReference(弱引用),key跟普通Map的key有区别,这个key存放的是对ThreadLocal的弱引用,
当对key的引用不存在的时候,自动从Entry数组移出,被GC自动回收,弱引用的回收,GC是不判断内存是否还够的,
Object对象存储要放入ThreadLocal的值。debug可以看下这个key长的样子。
ThreadLocalMap成员变量:
/**
* The initial capacity -- MUST be a power of two.
* 初始化容量大小,必须是2的幂次方
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
* table是一个Entry数组
*/
private Entry[] table;
/**
* The number of entries in the table.
* table的元素个数(Entry数组的元素个数)
*/
private int size = 0;
/**
* The next size value at which to resize.
* 重新分配表大小的阀值,默认值是0
*/
private int threshold; // Default to 0
ThreadLocalMap主要方法:
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
* 设置阀值上限,不能超过最坏2/3负载因子,因为超过之后性能极速下降
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* Increment i modulo len.
* 环形意义上的下一个索引
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
* 环形意义上的上一个索引
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
ThreadLocalMap使用线性探测法来解决散列冲突,所以实际上Entry[]数组在程序逻辑上是作为一个环形存在的。
图片源于网络。
ThreadLocalMap维护了Entry环形数组,数组中元素Entry的逻辑上的key为某个ThreadLocal对象(实际上是指向该ThreadLocal对
象的弱引用),value为代码中该线程往该ThreadLoacl变量实际塞入的值。
ThreadLocalMap构造函数:
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
* 构造一个包含firstKey和firstValue的map。
* ThreadLocalMap是惰性构造的,所以只有当至少要往里面放一个元素的时候才会构建它。
*/
hreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化table,设置Entry数组初始容量为16
table = new Entry[INITIAL_CAPACITY];
// firstKey的threadLocalHashCode与初始大小(16-1)取模算entry的下标值
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 初始化节点
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 设置扩容的阀值,当达到这个值的时候,Entry数据要进行扩容处理
setThreshold(INITIAL_CAPACITY);
}
ThreadLocalMap的set方法:
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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)]) {
// 找到对应的entry
ThreadLocal<?> k = e.get();
// 获取对应的value
if (k == key) {
e.value = value;
return;
}
// 替换失效的entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 在table中没有找到对应的key,会基于k-v构建节点,并赋值到Entry对应的位置
tab[i] = new Entry(key, value);
int sz = ++size;
// 从数组中移出空闲的entry
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/**
* Replace a stale entry encountered during a set operation
* with an entry for the specified key. The value passed in
* the value parameter is stored in the entry, whether or not
* an entry already exists for the specified key.
*
* As a side effect, this method expunges all stale entries in the
* "run" containing the stale entry. (A run is a sequence of entries
* between two null slots.)
*
* @param key the key
* @param value the value to be associated with key
* @param staleSlot index of the first stale entry encountered while
* searching for key.
*/
rivate void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
// 向前遍历Entry数组,查找最近一个无效的slot
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
// 向后遍历
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
// 找到key
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
// 找到了key,把新值替换旧值
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
// 如果找到了无效的slot,则这个作为清理的起点,否则,当前的值作为清理的起点
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 从slotToExpunge开始做一次连续段的清理,再做一次启发式清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
// 如果当前的slot已经无效,并且向前扫描过程中没有无效slot,则更新slotToExpunge为当前位置
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
// 如果key在table中不存在,则在原地放一个即可
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
// 在探测过程中如果发现任何无效slot,则做一次清理(连续段清理+启发式清理)
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
/**
* Heuristically scan some cells looking for stale entries.
* This is invoked when either a new element is added, or
* another stale one has been expunged. It performs a
* logarithmic number of scans, as a balance between no
* scanning (fast but retains garbage) and a number of scans
* proportional to number of elements, that would find all
* garbage but would cause some insertions to take O(n) time.
*
* @param i a position known NOT to hold a stale entry. The
* scan starts at the element after i.
*
* @param n scan control: {@code log2(n)} cells are scanned,
* unless a stale entry is found, in which case
* {@code log2(table.length)-1} additional cells are scanned.
* When called from insertions, this parameter is the number
* of elements, but when from replaceStaleEntry, it is the
* table length. (Note: all this could be changed to be either
* more or less aggressive by weighting n instead of just
* using straight log n. But this version is simple, fast, and
* seems to work well.)
*
* @return true if any stale entries have been removed.
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
// i是刚构建的元素下标,所以不会是一个无效slot,所以从下一个开始判断
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
// 扩大扫描控制因子
n = len;
removed = true;
// 清理一个连续段
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
/**
* Re-pack and/or re-size the table. First scan the entire
* table removing stale entries. If this doesn't sufficiently
* shrink the size of the table, double the table size.
*/
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
/*
* 因为做了一次清理,所以size很可能会变小。
* ThreadLocalMap这里的实现是调低阈值来判断是否需要扩容,
* threshold默认为len*2/3,所以这里的threshold - threshold / 4相当于len/2
*/
if (size >= threshold - threshold / 4)
resize();
}
/**
* Expunge all stale entries in the table.
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
/**
* Double the capacity of the table.
* 扩容,因为需要保证table的容量len为2的幂,所以扩容即扩大2倍
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
// 线性探测来存放Entry
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
ThreadLocalMap的getEntry方法:
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal<?> key) {
// 根据key这个ThreadLocal的ID来获取索引,也即哈希值
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 对应的entry存在且未失效且弱引用指向的ThreadLocal就是key,则命中返回
if (e != null && e.get() == key)
return e;
else
// 因为用的是线性探测,所以往后找还是有可能能够找到目标Entry的。
return getEntryAfterMiss(key, i, e);
}
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 基于线性探测法不断向后探测直到遇到空entry
while (e != null) {
ThreadLocal<?> k = e.get();
// 找到目标
if (k == key)
return e;
if (k == null)
// 该entry对应的ThreadLocal已经被回收,调用expungeStaleEntry来清理无效的entry
expungeStaleEntry(i);
else
// 环形意义下往后面走
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
// 因为entry对应的ThreadLocal已经被回收,value设为null,显式断开强引用
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
3、get过程源码分析
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal<?> key) {
// 根据key,即ThreadLocal的哈希值计算索引
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 如果对应的Entry存在,并且ThreadLocal的弱引用指向key,则key命中返回
if (e != null && e.get() == key)
return e;
else
// 如果直接查找不到,根据当前位置往后继续找
return getEntryAfterMiss(key, i, e);
}
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
// 如果找到,则返回
if (k == key)
return e;
// 如果Entry对应的ThreadLocal被回收,通过expungeStaleEntry清理无效的Entry
if (k == null)
expungeStaleEntry(i);
else
// 获取环形意义上的下一个索引
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
// 设置要清理的位置value为空
tab[staleSlot].value = null;
// 整个Entry置为空,方便GC回收
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
四 ThreadLocal存在哪些问题
1、内存溢出问题(OOM)
ThreadLocal是Thread里面维护的一个局部变量,线程不退出,局部变量的值就一直存在。正常情况下单个线程执行完就退出,
那么就会做清理工作,但是当使用多线程的时候,如果我们在ThreadLocal存的是大对象,并发量高的时候,每个线程都持有大
对象,很容易造成内存溢出,ThreadLocal是以“时间换空间”。
五 ThreadLocal总结
1、ThreadLocal.ThreadLocalMap是Thread的一个局部变量;
2、ThreadLocalMap是类似Map的结构,最大的区别在于key,可以看为是ThreadLocal对象实例的Hash值作为Key来存储value,
采用弱引用内存回收机制。
3、 ThreadLocal实现了线程与线程之间的隔离,因为是线程的局部变量,自己有一份同时,也实现了每个ThreadLocal之间的
隔离,因为key是ThreadLocal对象实例的Hash值,是不一样的。
4、线程退出时,局部变量自动回收内存。
5、ThreaLocal是以空间换时间的做法,就是用内存空间存起来,维持到整个线程结束,但是,线程之间就不需要排队访问,
在自己的线程里面随心所欲。