前言
ThreadLocal 是我们在学习java时必须优先掌握的内容,而且应用场景广泛。比如以前一些项目,会把前台传的一部分参数放入ThreadLocal,随线程流转;又或者我们经常使用的Spring框架的@Transactional注解,也用到了ThreadLocal。
所以学习ThreadLocal,是一个必备,且越早学习越好的基础内容
一、ThreadLocal是什么?
ThreadLocal 是一个类,和线程有关,但并不是一个Thread。
这个类能够提供线程局部变量
也就是说这个类的很多属性都被包含在线程实例中,比如下图就是Thread的字段展示,我们可以看到有一个threadLocals,它的实际类型是ThreadLocal.ThreadLocalMap
当然,他和普通的变量有所不同。它本身是唯一的对象,你可以把它new出来,但每个线程去访问或者设置它的时候,读取和设置的并不是这个对象,而是本线程内,这个对象的副本。
这也意味着,这个对象在不同的线程中,副本的值是不一样的
二、如何使用
代码如下:
public class ThreadlocalTest { static ThreadLocal<String> mylocalVar = new ThreadLocal<>(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Runnable() { @Override public void run() { //设置线程1中本地变量的值 mylocalVar.set("ABCDE"); //打印本地变量 System.out.println("thread1 val: " + mylocalVar.get()); } }); t1.start(); Thread.sleep(1000); System.out.println("main thread val: " + mylocalVar.get()); } }
我们先new了这么一个ThreadLocal 出来,然后使用一个线程去set一个值,然后分别在该线程和主线程内读取这个ThreadLocal,看看结果
答案不出所料,t1 线程设置的值,只有 t1 自己能看到。主线程是看不到的。
这里的要点有二个:
- ThreadLocal 和集合类一样,在创建时需要指定类型,上如图就指定的 String 类型
- ThreadLocal 的读写和设置不是用的等于号 , 而是要使用该ThreadLocal 的 set 和 get 方法
三、ThreadLocal 实现及图解
1. 源码解析
public class ThreadLocal<T> { public T get() { Thread t = Thread.currentThread(); // 任何一个线程查询值时,都会获取当前线程 // 返回当前线程里的map属性,线程类Thread中,有一个名为threadLocals, // 类型为ThreadLocalMap的成员变量,返回该成员变量 ThreadLocalMap map = getMap(t); if (map != null) { // 这个成员变量实际就是个HsahMap,把本TreadLocal对象传进去, // 即上文的mylocalVar对象,可返回该线程下+该treadlocal对象的存储值 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } } public class Thread implements Runnable { //... // 此处类型为ThreadLocal类下的静态内部类ThreadLocalMap ThreadLocal.ThreadLocalMap threadLocals = null; ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; } public class ThreadLocal<T> { static class ThreadLocalMap { private Entry[] table; //重要,最终的数据是存储在一个Entry数组中 static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); //仅有key值使用了super(),即为key创建弱引用 value = v; } } //... 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); } private void set(ThreadLocal<?> key, Object value) { } 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)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } } } }
2. 结构图解
我们先借用一张很好的图:每个线程里有map,map的结构我们都懂,就是一个个Entry构成的数组。而每一个Entry我们也是知道的,包含key 和 value。所谓的key,就是我们 new 出来的threadlocal对象。
结构解析:
该功能的实现其实由两部分组成,1. 一部分是每一个线程对象里都持有一个map对象,所以不同线程里有不同线程对象,不同对象互不干扰;2. 另一个部分是每定义一个threadlocal对象,这个对象都会作为key用来帮助每个线程在map里定位;
从上面的结构图,我们已经窥见ThreadLocal的核心机制:
- 我们new出来的threadlocal对象,仅仅作为一个key
- 每个Thread线程内部都有一个Map,Map里面存储threadlocal对象(key)和本线程的对该threadlocal对象设定的值(value)
所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
3. 弱引用
ThreadLocalMap的内部类 Entry 被设计为实现了WeakReference,Entry用来存放数据。在构造Entry对象时,将传进来的ThreadLocal (key)对象包装成了弱引用对象,而Entry对象和内部的value对象之间是强引用的
四、内存泄漏
我们上面说了 ThreadLocal 里使用了弱引用,为什么这里要弄出这个特殊设计呢? 我们继续分析
因为目前程序运行大多采用的是线程池模式,线程存在时间很长,如果不断往其中加入线程私有对象而得不到回收,会导致OOM。所以为了减少程序员手动回收,同时兼顾避免OOM,设计了一套弱引用自动回收机制。
当使用 mylocalVal = null 的时候,断绝 ClassA 对象和 mylocalVal 对象的关系,这样由线程持有的mylocalVal 就只有一个来自Entry的虚引用(虚线部分)了,我们知道,仅有虚引用的对象会被自动回收:
但是,需要注意的是,尽管采用了Entry—弱引用—key,来保证当 mylocalVal 对象置空时,回收Entry中的key,但此时 Entry 以及 value仍然存在,依然存在泄露的可能
因此当使用get()、set()这些方法时时,本线程会遍历ThreadLocalMap里面的Entry,把key为Null的Entry及value置为Null,这样下次GC就可以回收掉了,但如果本线程后续没有使用get()、set()怎么办呢?自然是无法回收了。
所以,我们在使用时,如有需要,应该显式的清除ThreadLocal中数据,即 mylocalVar.remove(), 从本线程种清除该entry
另一个点:threadLocalAge一般建议是类变量,成员变量亦可,但可能导致创建多个threadLocalAge实例,不影响使用,因为ThreadLocal变量,以obj.get() obj.set()为方法,所以存取时只要是同一个对象就行,多对象不过多占用了一些内存
另外使用线程池时,线程会复用,使用get()会获取到上次的值。需要特别注意及时清理
五、inheritableThreadLocals
我们上面说的内容其实主要是Threadlocal.ThreadLocalMap 这个类型的结构。然而在Thread中,使用这个map 结构的却有两个字段
ThreadLocal.ThreadLocalMap threadLocals = null; ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
其中我们对threadlocal做设置和访问时,其实使用到的是上述的 threadlocals 属性,那另一个map,即inheritableThreadLocals 字段又是干什么用的呢?
顾名思义inheritable 就是可遗传的,在这里就是说明threadlocals无法遗传,而该inheritableThreadLocals 段的值能被子线程获取
我们把最开始的代码略微改动,new出InheritableThreadLocal,然后使用主线程赋值,子线程也进行读取和改动
public class ThreadlocalTest { static InheritableThreadLocal<String> mylocalVar = new InheritableThreadLocal<>(); public static void main(String[] args) throws InterruptedException { mylocalVar.set("ABCDE"); Thread t1 = new Thread(new Runnable() { @Override public void run() { //打印本地变量 System.out.println("thread1 val: " + mylocalVar.get()); mylocalVar.set("FGHIJ"); System.out.println("thread1 val: " + mylocalVar.get()); } }); t1.start(); Thread.sleep(2000); System.out.println("main thread val: " + mylocalVar.get()); } }
结果没有意外,主线程,和new出来的子线程最开始数据是一样的,相当于一个数据的两个备份,但是要注意的是:除了子线程创建的瞬间,子线程复制了父线程的一个备份,后面父子线程的内容是互不影响的。
当然,能够继承的原理,就在于Thread对象的初始化了,我们用主线程建立其他线程的时候,会在其他线程对象初始化的时候,读取主线程的InheritableThreadLocal,并创建一个新对象,复制父线程的数据到自己的InheritableThreadLocal里