ThreadLocal 源码分析
ThreadLocal原理
一、ThreadLocal简介
ThreadLocal 能实现每一个线程都有自己专属的本地变量副本,不同线程之间不会相互干扰,主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。
简述:就是设置一个共享变量为线程局部变量,那么这个变量就会创建一个副本以<线程,value>的形式保存在ThreadLocal对象当中!
二、ThreadLocal的使用
ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:
1、void set(Object value)
代码解释:设置当前线程的线程局部变量的值。
2、public Object get()
代码解释:该方法返回当前线程所对应的线程局部变量。
3、public void remove()
代码解释: 将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。
4、protected Object initialValue()
代码解释:返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。如果有人心急则吃不了热豆腐,在还没有set的情况下,调用get则返回null。
**需要注意的是:**在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal 。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。关于Object和T的区别:Object是个根类,是个真实存在的类;T是个占位符,表示某个具体的类,仅在编译器有效,最终会被擦除用Object代替。
测试案例
我们知道,如果不进行线程安全的处理,共享变量Content就会有线程安全问题,那么我们应该如何处理?
public class ThreadLocalTest {
private String Content ;
public String getContent() {
return Content;
}
public void setContent(String content) {
Content = content;
}
public static void main(String[] args) {
ThreadLocalTest test = new ThreadLocalTest();
for (int i = 1; i <= 5 ; i++) {
new Thread(()->{
test.setContent(Thread.currentThread().getName() + "的数据");
System.out.println(Thread.currentThread().getName() + "------>" + test.getContent());
},"线程"+i).start();
}
}
}
方法一 :ThreadLocal
创建ThreadLocal对象,在set和get方法中做相应的处理!
public class ThreadLocalTest {
ThreadLocal<String> stl = new ThreadLocal<>() ;
private String Content ;
public String getContent() {
return stl.get();
}
public void setContent(String content) {
stl.set(content);
}
public static void main(String[] args) {
ThreadLocalTest test = new ThreadLocalTest();
for (int i = 1; i <= 5 ; i++) {
new Thread(()->{
test.setContent(Thread.currentThread().getName() + "的数据");
System.out.println(Thread.currentThread().getName() + "------>" + test.getContent());
},"线程"+i).start();
}
}
}
方法二:Synchronized
只需要加类锁,即可
public class ThreadLocalTest {
ThreadLocal<String> stl = new ThreadLocal<>() ;
private String Content ;
public String getContent() {
return Content;
}
public void setContent(String content) {
Content = content;
}
public static void main(String[] args) {
ThreadLocalTest test = new ThreadLocalTest();
for (int i = 1; i <= 5 ; i++) {
new Thread(()->{
synchronized (ThreadLocalTest.class){
test.setContent(Thread.currentThread().getName() + "的数据");
System.out.println(Thread.currentThread().getName() + "------>" + test.getContent());
}
},"线程"+i).start();
}
}
}
三、ThreadLocal vs Synchronized
Synchronized关键字也可以实现防止多线程共享变量冲突的问题。但是与TheadLocal还是存在不同
Synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制,只提供一份共享变量,让多个线程排队去访问,” 以时间换空间 “ | 为每个线程都额外提供了一份变量副本,同时时实现,1多线程并发访问而且互不干扰, ”用空间换时间“ |
侧重点 | 多线程访问共享资源同步 | 多线程之间的数据相互隔离 |
四、运用场景_事务案例
模拟一个简单的转账业务
甲方:转账给乙方100 ,余额 -100 ;
乙方:余额 +100
上述转账过程中我们需要保证事务的一致性,和多线程并发下connection是隔离的!
常规解决方法
通过传参保证Service 和 Dao的链接对象是同一个和开启事务保证数据的安全,加锁Synchronized!
常规操作步骤 :
- Dao层的方法添加一个参数
- Dao层不会再从连接池获取连接
- 注意:Dao层不会释放连接
- 加锁保证并发安全
常规解决方案的弊端: 提高代码的耦合度、失去了并发性
使用ThreadLocal解决
这种没有加锁、没有传参,这样既不会影响也保证的效率和数据的安全!注意:关闭连接前,需要调用remove方法解绑!防止内存泄漏
五、ThreadLocal内部结构
JDK8 的内部结构
JDK这样设计的好处是 : 1、每个Map保存的节点会变少,会减少Hash冲突
2、当Thread销毁的时候,其中的ThreadLocalMap也会自动销毁,减少内存占用!
ThreadLocal源码
1、set方法分析
public void set(T value) {
Thread t = Thread.currentThread(); //获取当前线程
ThreadLocalMap map = getMap(t); //获取线程的ThreadLocalMap对象
if (map != null) //map不为空
map.set(this, value); //将<threadlocal , 变量副本>以entry的形式设置进去
else
createMap(t, value); //map为空创建map
}
getMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //返回当前线程的Map对象, threadlocals即为Map对象的实例!
}
// ThreadLocal.ThreadLocalMap threadLocals = null;
createMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue); //为当前线程创建一个Map
}
归纳总结 :
1、获取当前线程,获取当前线程的map
2、如果获取的Map不为空,设置参数到Map当中(让当前ThreadLocal引用作为key )
3、如果Map为空,创建Map,并且设置初始值
2、get方法解析
public T get() {
Thread t = Thread.currentThread(); //获取当前线程
ThreadLocalMap map = getMap(t); //获取当前线程的ThreadLocalMap对象
if (map != null) { //map不为空
ThreadLocalMap.Entry e = map.getEntry(this); //通过key也就是ThreadLocal
//取出对应的变量副本<ThreadLocal , 副本变量>
if (e != null) { //节点不为空
@SuppressWarnings("unchecked")
T result = (T)e.value; //取出副本变量
return result; //返回变量副本
}
}
return setInitialValue(); //map为空,初始化ma一个map
}
getMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //返回当前线程的ThreadLocal对象
}
getEntry
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);
}
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; //返回初始value
}
归纳总结
1、获取当前线程,然后获取当前线程的Map
2、map不为空,通过key(需要的副本变量),获得一个Entry节点,如果节点不为空,通过节点获取副本变量
3、map为空,初始化一个map
3、remove方法分析
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread()); //获取当前线程的map
if (m != null) //map不为空
m.remove(this); //删除其中的一个Threadlocal
}
4、initialValue
protected T initialValue() {
return null; //就是用来被重写的,重写后重写初始值,使得初始值不为null
}
ThreadLocalMap
ThreadLocal 、 ThreadLocalMap 、Thread、Entry的关系
- 首先ThreadLocalMap是以数组形式存储一个个Entry键值对的,它是Thread的一个静态内部类,而Entry是ThreadLocalMap的静态内部类,Entry的key就是new出来的ThreadLocal,value就是set入的值,所以一个Thread可以有多个ThreadLocal-value键值对。
- JVM内部维护了一个线程版的Map<Thread,T>(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中),每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量
六、ThreadLocal内存泄漏问题
Thread Leak(内存泄漏)
什么是内存泄漏呢?简单的说,就是东西放在内存里面,但你忘记它放哪里了,它占着一块内存,但是不能回收。当这样的东西越来越多,内存就吃紧,最终导致服务器宕机。
再讲一个小故事,阐述一下内存泄漏。在抗日时期,有两名地下党A和B,A是上线,B是下线,B不能直接联系党中央的,他需要通过A来帮忙传话。一旦A发生意外,党中央就找不到B了,B一直存在,但是茫茫人海,党中央是无法启用B做战斗任务的安排,这种情况类似内存泄漏
- 即:不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
1、强、软、弱、虚、四大引用
1、强引用(StrongReference)
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。
Object o=new Object(); // 强引用
o=null; // 帮助垃圾收集器回收此对象
2、软引用(SoftReference)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
//当我们内存不够用的时候,soft会被回收的情况
SoftReference<MyObject> softReference = new SoftReference<>(new Object());
3、弱引用(WeakReference)
对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
//垃圾回收机制一运行,会回收该对象占用的内存
WeakReference<MyObject> weakReference = new WeakReference<>(new Object());
4、虚引用(PhantomReference)
顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象。
虚引用必须和引用队列 (ReferenceQueue)联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue();
//和引用队列进行关联,当虚引用对象被回收后,会进入ReferenceQueue队列中
PhantomReference<MyObject> phantomReference = new PhantomReference<>(new MyObject(),referenceQueue);
了解完上述引用我们知道,
2、Key == TheadLocal是强引用
我们假设当key对ThreadLocal是强引用,会出现内存泄漏吗?
ThreadLocal Ref这个是弱引用,但是我们的ThreadLocalMap中的key强引用了这个ThreadLocal,因为弱引用会被自动回收,所有我们的ThreadLocal使用过后会被GC自动回收,但是我们的ThreadLocalMap中的key是强引用,如果不再弱引用结束之前删除Entry节点的话,此时key引用的ThreadLocal将无法被回收!
内存泄漏就此发生!
也就是说: ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的
3、Key == ThreadLocal是弱引用
假设我们的key对ThreadLocal是弱引用,那么会出现内存泄漏吗?
注意区别:
- 如果是弱引用,ThreadLocal被回收后,key置为null
- 如果是强引用,ThreadLocal被回收后,key不会修改
4、内存泄漏的真实原因
1、 没有手动侧除这个 Entry
2、CurrentThread 当前线程依然运行
- 第一点很好理解,只要在使用完下 ThreadLocal ,调用其 remove 方法删除对应的 Entry ,就能避免内存泄漏。
- 第二点稍微复杂一点,由于ThreadLocalMap 是 Thread 的一个属性,被当前线程所引用,所以ThreadLocalMap的生命周期跟 Thread 一样长。那么在使用完 ThreadLocal 的使用,如果当前Thread 也随之执行结束, ThreadLocalMap 自然也会被 gc 回收,从根源上避免了内存泄漏。
综上, ThreadLocal 内存泄漏的根源是:
- 没有手动侧除这个 Entry
- CurrentThread 当前线程依然运行,由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用
5. key要使用弱引用的原因
无论 ThreadLocalMap 中的 key 使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。
事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的.
这就意味着使用完 ThreadLocal , CurrentThread 依然运行的前提下.就算忘记调用 remove 方法**,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收.对应value在下一次 ThreadLocaIMap 调用 set/get/remove 中的任一方法的时候会被清除,从而避免内存泄漏.**
参考文章: