参照
全文参照视频:全面解析ThreadLocal
一、ThreadLocal说明
1. 作用
你可以理解你要操作的变量依托在了ThreadLocal上,一个变量对应一个ThreadLocal,变量依托在ThreadLocal时,会在每个线程中创建这个变量的副本,这样每个独立的副本作为该线程的全局变量独立使用,从而保证了多线程的安全性。
ThreadLocal<String> threadLocalOld = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return new String("dsadsa");
}
};
Thread thread0 = new Thread(()->{
String s0 = threadLocalOld.get();//每个线程调度get函数获取本线程的副本。
// do sth
threadLocalOld.set("dsad");// set函数set的值,只会设置本线程的值,不会对其他线程有任何影响。
});
Thread thread1 = new Thread(()->{
String s0 = threadLocalOld.get();//获取的仍然是初始化的值"dsadsa",而不是"dsad";
});
2. 疑问
为什么不直接为每个线程都建一个变量?
因为你可能不知道到时候使用这个变量的线程会有多少个,即使你知道,比如有10个线程,几千个变量这些线程在本线程内独立使用,那是否你需要建立几万个这样的变量,而依托于ThreadLocal,需要多少个这样的变量,你建多少个就行了。
3. 变化
依托在今后对变量操作上会存在哪些变化呢?
- 修改变量时,用ThreadLocal的set方法;
- 获取变量时,用ThreadLocal的get方法;
- 初始化赋值时,创建ThreadLocal对象时,就重写其中的initialValue方法。
- 删除这个变量时,用ThreadLocal的remove方法。那么ThreadLocal对象就会被删除,对应的值也会被删除。
4. 从原理上来解释
- 有个类叫ThreadLocalMap,它是ThreadLocal的内部类,Thread类中的成员变量定义有ThreadLocalMap,你可以将其理解为Map集合,键是ThreadLocal类型,值就是你要操作的变量。
- ThreadLocal中的核心方法都是针对本线程中维护的ThreadLocalMap对象中以本ThreadLocal对象为键的键值对(entry)的操作。
- set就是改变了这个entry的值;
- get就是获取这个entry的值;
- remove就是删除这个entry;
- 每个线程中维护的成员变量ThreadLocalMap存储着很多不同ThreadLocal对象为键的键值对,在不同的线程中相同变量对应的ThreadLocal对象是同一个,但是由于存储在不同线程维护的ThreadLocalMap中,ThreadLocal对象的那些方法又是针对本线程ThreadLocalMap操作(获取正在运行的线程,然后获取对应的ThreadLocalMap)的,所以做到了多个线程用同一个ThreadLocal对象,每个线程生成了独立的变量副本,操作也只对本线程的功能,具体源码参照第三块。
5. 与Sychrosize比较
- ThreadLocal相当于是在每个线程中新建了一个独立的变量,相同的只是ThreadLocal对象,而实际操作的变量值另外有独立副本。Sychrosize并不会建立副本,而是在时间上隔离了各个线程对这个变量的操作。
- ThreadLocal无法让线程间共享这个变量,共享的只是ThreadLocal对象本身,但多个线程并不会对ThreadLocal对象写操作,所以ThreadLocal对象的安全性无需考虑。
6. 使用场景
满足以下场景:假设存在A类,一串程序代码,多个线程在执行这个程序,比如多个用户同时在访问一个服务器的相同业务。
- A类对象在代码中不同地方都用到,且这些地方不连通,比如在不同方法,不同类中用到。(这就说明不能直接手写创建。)
- 需要保证A类对象在不同地方用的是同一个,也就是说对象只能创建一个,另外不能通过传参的方式传递。(好像可以用单例模式)
- 多个线程执行这个程序时,关于A类,每个线程都有自己的A类对象,也就是多个线程不能使用同一个A类对象;(这就说明不能光用单理模式)
分析:
可能有的人想到用单例模式,单例模式确实可以做到程序中的任何地方引用都是同一个对象,但是单例模式创建的变量是公共的,你写单例模式怎么写的就知道了。所以要怎么做到一个类的对象在单个线程内多处用的是同一个,但在多线程中是不同的呢?
答案就是将这个对象归为线程所有,就像静态变量归为类所有一样,将这个变量归线程所有,这样只要这个线程在运行那么获取的就是同一个对象,除非这个对象被线程删除了。那么就用到ThreadLocal类了。ThreadLocal会给当前线程的ThreadLocalMap成员变量赋值,并自己为键,A类对象为值来存在这个类似Map的集合中,这样就相当于把这个类的对象作为了Thread的成员变量。这样就可以实现通过公共的ThreadLocal对象让多个线程使用各自的A类对象。
二、ThreadLocal内部结构
1. 旧jdk版本
每个 ThreadLocal都创建一个Map(ThreadLocalMap),然后用线程作为Map的key,要存储的局部变量作为Map的value,这样就能达到各个线程的局部变量隔离的效果。
2. JDK1.8
(1)每个Thread线程内部都有—个Map(ThreadLocalMap),可以在Thread里面查看是一个ThreadLocalMap类型的成员变量,默认值为null。
(2)Map里面存储Threadlocal对象(key)和线程的变量副本( value)。
(3)Thread内部的Map是由ThreadLocal堆维护的,由Threadlocal负责向map获取和设置线程的变量值(后面祥讲)。
(4)对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
ThreadLocalMap是ThreadLocal的一个内部类,在Thread的条件里面
3. 后者优势
- 每个Map存储的Entry数量变少;
- 当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存的使用;
三、ThreadLocal 核心方法源码分析
视频地址:ThreadLocal 核心方法源码
方法声明 | 描述 |
---|---|
protected T initialValue() | 返回当前线程局部变量的初始值,这个方法并非像构造方法,他只是个普通方法 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前程绑定的局部变量 |
1. public void set(T value)
总结:
- 首先获取当前线程,并根据当前线程其管理的ThreadLocalMap对象(Thread中的成员变量)
- 如果获取的ThreadLocalMap不为空,则将参数设置到ThreadLocalMap中(当前 Threadlocal的引用作为key,value参数为值)
- 如果ThreadLocalMap为空,则给该线程创建同样参数设置的ThreadLocalMap对象,赋值给当前线程管理的ThreadLocalMap
/*合设置当前线程对应的 Threadloca的值
合@ param value将要保存在当前线程对应的 Threadloca的值*/
public void set(T value){
//获取当前线程对象
Thread t= Thread.currentThread();
//获取此线程对象中维护的 ThreadLocalMap对象
ThreadLocalMap map= getMap(t);
//判断map是否存在
if(map != null)
//存在则调用map.set设置此实体 entry
map set(this, value);
else
//1)当前线程 Thread不存在 ThreadLoca lMap对象
//2)则调用 createMap进行 ThreadLoca IMap对象的初始化
//3)并将t(当前线程)和 value(t对应的值)作为第一个 entry存放至 ThreadLoca lMap中
createMap(t, value);
}
public void set(T value) {
//获取当前线程对象
Thread t = Thread.currentThread();
//获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//判断map是否存在
if (map != null)
//存在则调用map.set设置此实体entry
map.set(this, value);
else
//1)当前线程Thread不存在 ThreadLocalMap对象
//2)则调用createMap进行 ThreadLocalMap对象的初始化
//3)并将t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
}
/*
获取当前线程 Thread对应维护的 Thr eadLoca IMap
@param t 当前线程
Reture the map对应维护的ThreadLocalMap
ThreadLocalMap作为Thread里的一个成员变量在被维护
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//新建一个ThreadLocalMap,赋值给当前线程的threadLocals成员变量
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
Thread类截图
2. public T get()
- 首先获取当前线程,根据当前线程获取一个Map
- 如果获取的Map不为空,则在Map中以 Threadlocal的引用作为key来在Map中获取对应的 Entry e,否则转到4
- 如果e不为nul,则返回 e value,否则转到4
- Map为空或者e为空,则通过 initialvalue函数获取初始值vaue,然后用 ThreadLocal的引用和vaue作为firstKeyi和 firstvaluet创建一个新的Map
总结:先获取当前线程的 ThreadLocalMap变量,如果存在则返回值,不存在则刨建并返回初始值
public T get() {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t); 获取当前线程管理的ThreadLocalMap对象(成员变量)
if (map != null) {
//ThreadLocalMap对象不为null,根据当前ThreadLocal获取键值对
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
//键值对不为null,返回对象的值
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//ThreadLocalMap对象为null或者键值对为null则初始化
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();//初始一个值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
3. public void remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());//获取当前线程管理的ThreadLocalMap对象
if (m != null)
m.remove(this);//如果不为空就删除掉对应的键值对
}
4. protected T initialValue()
- 这个方法是一个延迟调用方法,从上面的代码我们得知在set方法还未调用而先调用了get方法时才执行,并且一个线程仅执行1次。当然如果remove了,又get也会执行这个方法的,get的逻辑理解了,这个应该不难理解。
- 这个方法缺省实现直接返回一个null。
- 如果想要一个除null之外的初始值,可以重写此方法。(备注:该方法是一个 protected的方法,显然是
为了让子类覆盖而设计的)
protected T initialValue() {
return null;
}
下面内容供了解
四、ThreadLocalMap源码分析
从前面不难看出,ThreadLocal核心方法就是围绕着当前线程中维护的ThreadLocalMap对象(Thread中的一个局部变量)来操作的,ThreadLocalMap是ThreadLocal的一个内部类,类似于Map集合,存储着key(本线程的ThreadLocal<T>对象)—value(局部变量值T v)。
1. ThreadLocalMap结构图
说明:
- 在 ThreadLocalMap中,也是用Entry来保存KV结构数据的。不过Entry中的key只能是 ThreadLocal对象,这点在构造方法中已经限定死了。
- 另外,Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将 Threadlocal对象的生命周期和线程生命周期解绑,什么意思见下面。
2. 从内存泄漏了解ThreadLocalMap
首先去了解一下内存泄漏和四种引用的概念,这里从内存泄漏的角度说明了ThreadLocalMap中key为什么要使用弱引用(A2引用)。也可以借此机会实际了解一下各种引用。
说明:上图中实线表示强引用,这里强弱讨论的是A2也就是entry中键对ThreadLocal对象的引用
1. 强/弱引用的分析
- 假设在业务代码中使用完Threadlocal, ref这个对ThreadLocal对象的引用被回收了,A1消失。
- 但由于Key对Threadlocal的引用(A2)未结束,所以Threadlocal对象不会被回收,由于B系列引用所以Entry不会被回收,这就造成了内存泄漏。
- 如果A2是弱引用,则Threadlocal会被回收,但同样的entry仍然被强引用,其中的value也不会被回收,也存在泄漏,但是好一点,至少回收了Threadlocal,key变成了null。
- 所以说要记得remove(),可以直接将Entry设置为null;
综上, ThreadLocal内存泄漏的根源是:由于ThreadLocalMap作为Thread的一个成员变量,其生命周期跟Thread一样长,线程不结束,B的引用链都存在。
为什么用弱引用呢?
事实上,在 ThreadLocalMap中的set/getEntry方法(对应ThreadLocal中set/get方法会有调用)中,会对key为null(也即是 Threadlocal为null)进行判断,如果为null的话,那么是会对vaue置为null。这就意味着使用完Threadlocal, CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次获取其它值时调用到ThreadLocalMap的set, get, remove中的任一方法的时,key为null的就会被清除,从而避免内存泄漏。