ThreadLocal置信大家都有用过的,个别用作存取一些全局的信息。比方用户信息,流程信息,甚至在Spring框架外面通过事务注解Transactional去获取数据库连贯的实现上,也有它的一份功绩。
ThreadLocal作为一个进阶必会知识点,而且还是面试高频考点。网上博客对它的解读也必然不会少,然而网上博客解读程度参差不齐,看多了难免会绕。不如本人亲自再梳理一遍,顺便记录下本人的解读。
ThreadLocal的线程隔离性Demo
先来看一个小的demo
static ThreadLocal<Student> threadLocal = new ThreadLocal<Student>();

public static void main(String[] args) {

threadLocalTest1();

}

private static void threadLocalTest1() {

new Thread(new Runnable() {    public void run() {        try {            TimeUnit.SECONDS.sleep(3);        } catch (InterruptedException e) {            e.printStackTrace();        }    System.out.println(threadLocal.get());    }}).start();new Thread(new Runnable() {    public void run() {        try {            TimeUnit.SECONDS.sleep(1);        } catch (InterruptedException e) {            e.printStackTrace();        }        threadLocal.set(new Student("zhangsan"));    }}).start();

}
复制代码
如代码所示,开启两个线程。
第一个线程3秒之后去取在动态变量threadLocal里的变量。
第二个线程1秒之后去设置threadLocal里的变量。
这段代码运行的后果就是,第一个线程永远获取不到第二个线程给动态变量threadLocal里设置的变量。
论断:不同的线程操作同一个threadLocal对象,能够实现线程信息之间的隔离。
猜测:看到set办法和get办法,大胆猜测threadLocal对象外面有个Map,key为以后线程,value为ThreadLocal泛型里的对象,这样就实现了在空间上的线程安全性。
但事实并不是这样,答案不在猜测中,而在源码中。
ThreadLocal.set()办法源码解读
public void set(T value) {

// 获取以后线程Thread t = Thread.currentThread();// 获取以后线程的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null)    // 把以后ThreadLocal对象作为key,调用set办法的入参对象作为value,放入以后线程的ThreadLocalMap    map.set(this, value);else    // 通过以后线程和调用set办法的入参对象去结构Map    createMap(t, value);

}
复制代码
ThreadLocal.get()办法源码解读
public T get() {

// 获取以后线程Thread t = Thread.currentThread();// 获取以后线程的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {// 通过以后ThreadLocal对象去取EntryThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {    @SuppressWarnings("unchecked")    // 获取Entry的value返回    T result = (T)e.value;    return result;}}return setInitialValue();

}
复制代码
那么ThreadLocalMap又是什么呢?
再点进源码,你会发现,ThreadLocalMap是ThreadLocal的一个动态外部类,同时在Thread类下有一个成员变量ThreadLocals
ThreadLocal.ThreadLocalMap threadLocals = null
复制代码
那咱们顺着逻辑再上来看看ThreadLocalMap的set办法用来干嘛的。
ThreadLocalMap.set()办法源码解读
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;    }}// 设置Entrytab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)    rehash();

}
复制代码
其实咱们在晓得了HashMap的底层原理实现的根底下来了解上述代码并不难,取Entry数组长度,哈希,与运算取模,设置Entry零打碎敲。
然而这里须要留神的是,也是ThreadLocal最有特色的一点,是这个Entry并不是一般了解里的Entry,而是ThreadLocalMap外面的一个动态外部类并且继承了WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {

Object value;Entry(ThreadLocal<?> k, Object v) {    super(k);    value = v;}

}
复制代码
到此,是不是对几个带有Thread的名词弄的有点晕,劳动一下,咱们先来看张图梳理一下ThreadLocal,ThreadLocalMap,与Thread的关系,如图:

层层深刻,这时候问题又来了,弱援用是啥?啥是弱援用?弱什么?什么援用?
WeakReference演示
public static void main(String[] args) {

wrTest();

}

private static void wrTest() {

WeakReference<Student> weakReference = new WeakReference<Student>(new Student("aaaa"));System.out.println(weakReference.get());System.gc();System.out.println(weakReference.get());

}
复制代码
输入的后果为:
Student@1b6d3586
null
复制代码
在这个demo外面,咱们能够很清晰的看到弱援用的个性:当JVM进行垃圾回收时,无论内存是否短缺,都会回收被弱援用关联的对象。
当垃圾回收器执行一次之后,原来的弱援用关联的对象就为null了,它领有这样的特质,又有什么用呢?
这就得引出内存透露的问题了。
内存透露
应用ThreadLocal哪些状况会产生内存透露?
调用ThreadLocal的set办法设置某个对象进去,起初这个对象回收不了。长此以往,影响程序运行速度,最终造成OOM。
为什么会回收不了?是因为垃圾回收器执行之后,CurrentThread仍然运行的前提下,Entry[]始终存在,然而其中有些key因为是继承了WeakReference,在GC之后其get办法返回值就是null了,导致取不到Entry外面key.get()为null的KEY所对应的value,而这块value永远也拜访不到了。如图:

应用ThreadLocal如何防止内存透露?
把ThreadLocal定义为static,放弃单例,不被回收。
用完ThreadLocal,须要手动擦除对应的Entry节点信息,记得调用ThreadLocal的remove办法。
特地是在理论我的项目的场景下,大多数状况下线程都是交给线程池在治理。一个线程工作跑完,通常不会立刻销毁,而是放在线程池外面期待下次工作的降临(有种说法说是在把线程放回线程池的过程中会擦除Thread下的ThreadLocal.ThreadLocalMap threadLocals信息,当然,这是线程池帮咱们做的)无论线程池是否帮咱们擦除,咱们用完ThreadLocal手动remove总是平安的。
附上remove的源码。
public void remove() {

// 获取以后线程的ThreadLocalMapThreadLocalMap m = getMap(Thread.currentThread());if (m != null){    m.remove(this);}

}

private void remove(ThreadLocal<?> key) {

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)]) {     // 匹配key     if (e.get() == key) {     // 革除一个Entry元素     e.clear();     expungeStaleEntry(i);     return;    }}

}
复制代码
总结
解读完ThreadLocal的源码,再回归到它的命名,了解又深了一个档次:
Thread + Local = 线程 + 本地 = 线程本地变量 = 把某个对象放在了线程本地。
文理不分家,无妨借用理科的思维打比方去了解它:
ThreadLocal对象就像一个具体的主观的对象,能够是某个话题,某部电影,某本书,甚至某个人。
而每个Thread就像一个人,读者,旁观者。
Thread对ThreadLocal的set操作和get操作,就别离对应是一个人对某个主观的对象进行设置主观印象和获取主观印象。即使是同一个对象,不同的人会对其有不同的主观印象并记录在本人的脑海里,在每个人来看这些印象都是正当的,无论你处在哪个上下文,总能疾速获取到这个印象信息,而不会错乱。
在空间上保留对象的正本,通过空间换工夫的思维,也就实现了ThreadLocal的线程安全性。