最近有小伙伴,想让我写篇博客,来总结下关于ThreadLocal的内容。ThreadLocal也是一个比较高频的面试知识点了吧,之前关于ThreadLocal的内容一直躺在我的印象笔记里,那么今天我就写篇博客讲解下ThreadLocal的基本原理。
废话不多说,学习之前先要知道ThreadLocal是干啥的。能帮我们干什么?为什么平时会使用到ThreadLocal。
ThreadLocal和synchronized一样,都是用来解决线程安全问题的。只不过ThreadLocal和synchronized解决线程安全问题的解决方案不同:synchronized是通过牺牲时间解决线程安全问题,ThreadLocal是通过牺牲空间解决线程安全问题。
关于synchronized,想必大家都很清楚了,我就不赘述了。如果使用synchronized修饰,那么同一时刻只有一个线程去操作数据,此时肯定是线程安全的,因为只有一个线程操作。但是其他获取不到资源的线程就要阻塞挂起,直到别人释放资源,才能去获取资源操作数据。但是挂起线程或者唤醒线程都会使CPU会从用户态切换到内核态,比较浪费性能。
并且使用ThreadLocal保证线程安全,使用起来也很简单,如果想放到ThreadLocal一个int值,例子如下:
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
threadLocal .set(1);//存储值到ThreadLocal
threadLocal .get();//从ThreadLocal获取值,返回1
那么他的原理是什么呢?
上面说到ThreadLocal是牺牲空间解决线程安全问题,那么他到底是怎么解决的呢?
其实,每个线程都对应了一个ThreadLocal,那么想一下我们如果将数据存储到ThreadLocal这个对象中,那么岂不是就解决了线程安全问题(因为每个线程都对应了自己的ThreadLocal,那么每个线程都操作自己的ThreadLocal不就不会产生线程安全问题了)。
其实真正存储数据的并不是ThreadLocaMap这个类,而是ThreadLocal的一个静态内部类ThreadLocalMap,这个东西类似一个Map,但是又有一些不一样,接下来会详细讲述。
上边就是ThreadLocal的基本原理了,那么到底具体是咋做的呢?想学会,看源码是必不可少的。come on!!
ThreadLocal最常用的方法就是
public T get();//获取ThreadLocal存储的数据,T是泛型,表示返回的结果类型
public void set(T value);//向ThreadLocal存储数据,其中T类型的value表示想要存储的数据
首先,来看看ThreadLocal的set()操作,看看ThreadLocal是如何帮我们将数据存储在每个线程内的。
当我们家调用了set方法,会发生什么呢?set方法源码如下:
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//根据当前线程获取ThreadLocalMap
//ThreadLocalMap是ThreadLocal的一个静态内部类,下边会讲,可以暂时理解成一个HashMap(是暂时哦~~)
ThreadLocalMap map = getMap(t);
if (map != null)
//map不是null的话(已经初始化)就调用ThreadLocalMap的get()方法将这个value设置进去。
//注意!!!!此时的值时value,键是this,this表示这个对象,也就是调用set方法的TheadLocal对象
map.set(this, value);
else
//如果为空,就去初始化一个map
createMap(t, value);
}
然后看上边源码第三行的getMap方法:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到返回一个ThreadLocalMap,返回的是一个Thread的变量threadLocals,那么近Thread类看看:
这也证明了我们上边的所说的原理。
不知道大家是不是被转晕了,总结一下:
如果理解了上边的set方法,那么get方法就是洒洒水的事情。
老规矩,上源码:
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//根据当前线程获取ThreadLocalMap
//ThreadLocalMap是ThreadLocal的一个静态内部类,下边会讲,可以暂时理解成一个HashMap(是暂时哦~~)
ThreadLocalMap map = getMap(t);
if (map != null) {
//调用ThreadLocalMap的get()方法获取数据
//注意此时get()方法内的参数是this,也就是当前调用get方法的ThreadLocal对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
原理和set()方法是一样的:
那么ThreadLocal的原理就讲完了,存数据的时候让当前ThreadLocal对象作为一个key存到ThreadLocalMap中,获取数据的时候再从ThreadLocalMap中根据当前的ThreadLocal对象获取数据。因为同一个线程只对应一个ThreadLocalMap,所以他是线程安全的。
那么ThreadLocalMap是何方神圣呢?
上面只是说了类似一个HashMap,又有点不同,那么是哪里不同呢?
类似于HashMap,ThreadLocalMap中存储数据的也是一个entry数组。其他地方都是类似的,只有一个地方需要注意,就是这个entry类继承了弱引用
,废话不多说,直接上源码:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
//entry类的构造函数
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
补充:如果一个对象引用是一个弱引用,那么在发生GC的时候该弱引用引用的对象就会被回收掉。
如果一个引用是一个强引用,那么即使发生OOM也不会被回收。
此时Entry对象中的key是一个弱引用,那么为什么设置成一个弱引用呢,这就跟我们经常说的内存泄漏
有着很大的关联了?
试想一下,如果ThreadLocalMap中的ThreadLocal使用强引用,那么此时对于一个ThreadLocal对象就会存在两个强引用。由于ThreadLocalMap中还存在一个该对象的强引用,即使原来的对象引用失效的时候,这个ThreadLocal对象也不会被回收。
但是如果使用弱引用的话,如果ThreadLocal的对象引用失效了,此时发生GC,因为ThreadLocalMap中使用弱引用,所以个对象会被回收。
但是这产生了另一个问题,因为我们ThreadLocalMap中的数据是以ThreadLocal为key进行存储的,获取数据也是根据这个ThreadLocal获取,如果发生GC,这个ThreadLocal对象被回收了(value不是弱引用,不会被回收),那么这个value我们就获取不到了,产生了内存泄漏
。
这可如何是好呀?
很显然我们还没有达到开发JDK的程度,JDK也想到了这个问题,当我们调用set()或者get()方法的时候,会帮我们自动清除掉为null的键值对,此时就避免了内存泄漏的情况。
但是我们不可能总是调用set或者get方法吧,还可以在使用ThreadLocal完成之后,调用ThreadLocal的remove()方法,这个方法会将存在ThreadLocalMap的数据移除,这样就不会由于GC之后ThreadLocal被回收造成的内存泄漏问题了。
另外一个避免内存泄漏的方法就是将ThreadLocal用static修饰,设置成静态的,这样就不会被GC掉,一直可以通过ThreadLocal对象获取到map中的数据。
我的分布式电商项目中,不是使用ThreadLocal来解决线程安全问题的,而是利用了存在ThreadLocal中的数据在一个线程执行的任意时刻都能获取到的特性。
我们的许多模块都需要登录才能操作,比如订单模块。像下单这种操作必须使用到当前登陆的用户信息,当人可以存储在session中,用的时候去获取。但是如果service层想要使用呢?存在session的数据必须在controller层就获取到,如果service想使用,必须每次用的时候在controller获取,然后再作为参数传到service层,这样显然太麻烦。
我再项目中写了一个拦截器,通过session判断用户登录信息,然后将用户信息存储在ThreadLoca中,下次在service中用的时候就可以使用ThreadLocal获取数据了,因为从拦截器->controller->service都是一个线程,可以获取到。
线程是使用线程池获取,所以很可能造成内存泄漏,那么如何解决内存泄漏呢?
我使用static进行修饰,这样ThreadLocal对象就永远不会被回收,并且在类中只存在一个对象。下个用户使用同一个线程的时候,就会覆盖掉原来的数据。这样我们的系统的并发有多少,那么内存中就会有多少个ThreadLocal对象,还是可以接受的。