一、ThreadLocal简介
1.ThreadLocal介绍
ThreadLocal是线程变量,在ThreadLocal中声明的变量仅属于当前线程,对其他线程是隔离的。ThreadLocal为变量在每个线程中创建了一个副本,每个线程可以访问自己内部的副本变量。
2. 注意点
- 因为每个ThreadLocal内有自己的实例副本,且该副本只能当前Thread使用。
- 每个Thread有自己的实例副本,且其他Thread不可访问,不存在多线程共享问题。
ThreadLocal变量通常被private static修饰,当一个线程结束时,它所使用的所有ThreadLocal相对应的副本都可被回收。
3.ThreadLocal使用状态图
ThreadLocal是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(ThreadLocal,value),虽然不同的线程之间ThreadLocal的key值一样,但是不同的线程所拥有的ThreadLocalMap是唯一的,不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而实现了线程间变量隔离的目的,在同一个线程中value的地址是一样的。
二、ThreadLocal与Synchronized区别
ThreadLocal与Synchronized都可以解决多线程并发访问。
区别:
- Synchronized用于线程间的数据共享,ThreadLocal用于线程间的数据隔离。
- Synchronized是使用锁的机制,使变量或代码块在某一时刻只能被一个线程访问,而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的共享。
三、ThreadLocal中的核心方法
1.ThreadLocal中的set方法
public void set(T value) {
/* 返回对当前执行的线程对象引用 */
Thread t = Thread.currentThread();
/* 获取线程中的属性threadLocalMap,如果threadLocalMap 不为空
* 则直接更新要保存的变量值 */
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
/* 初始化threadLocalMap,并赋值 */
createMap(t, value);
}
}
从上面代码可以看出,ThreadLocal set赋值时首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value,如果map为空,则实例化threadLocalMap,并将value值初始化。
2.ThreadLocalMap
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
在上面代码可以看出,ThreadLocalMap是ThreadLocal的内部静态类,它的构成主要是用Entry来保存数据,而且还是继承的弱引用。在Entry内部,使用ThreadLocal作为key,使用我们设定的值作为value。
ThreadLocalMap中,使用的key是ThreadLocal的弱引用,弱引用的特点是如果这个只存在弱引用,那么在下一次垃圾回收时的时候必然会被清理掉。
所以如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候会被清理掉,这样下去,ThreadLocalMap中使用这个ThreadLocal的key也会被清理掉。但是,value是强引用,不会被清理,这样一来就会出现key为null的value。
ThreadLocal是与线程绑定的一个变量,如果没有将ThreadLocal中的变量删除(remove)或替换掉,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这意味着线程持续的时间不可猜测,甚至和JVM的生命周期一致。比如:如果ThreadLocal中直接或间接包装了类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对它做操作,内部的集合类和复杂对象所占用的空间就会开始膨胀。
3.ThreadLocal中的get()方法
public T get() {
/* 获取当前线程 */
Thread t = Thread.currentThread();
/* 获取当前线程的ThreadLocalMap */
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null) {
/* 获取ThreadLocalMap中存储的值 */
ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
/* 如果数据为null,则初始化,初始化的结果,ThreadLocalMap中存放的Key值是ThreadLocal,值为null */
return setInitialValue();
}
4.ThreadLocal中的remove()方法
public void remove() {
ThreadLocal.ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
在上面的代码可以看出,remove方法,直接将ThreadLocal对应的值从当前的ThreadLocalMap中删除,关于为什么删除,涉及到内存泄露问题。
四、ThreadLocal与Thread,ThreadLocalMap之间的关系
- 每个Thread线程内部都有一个Map(ThreadLocalMap)
- Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
- Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向Map获取和设置线程的变量值。
- 对于不同的线程,每次获取副本值时,别的线程并不能获取当前线程的副本值,形成了副本的隔离,互不干扰。
五、Spring使用ThreadLocal解决线程安全问题
一般的Web应用划分为M - mode 对象层,封装到 domain 里。V - view 展示层,但因为目前都是前后端分离的项目,几乎不会在后端项目里写 JSP 文件了。C - Controller 控制层,对外提供接口实现类。DAO 算是单独拿出来用户处理数据库操作的层。在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程。
这样根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一个ThreadLocal变量都是当前线程所绑定的。
public class TopicDao {
/* 使用ThreadLocal保存Connection变量 */
private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
public static Connection getConnection(){
/* 如果connThreadLocal没有本线程对应的Connection,就需要创建一个新的
* 并保存到线程本地变量 */
if (connThreadLocal.get() == null){
/* 假设存在的类*/
Connection connection = ConnectionManager.getConnection();
connThreadLocal.set(connection);
return connection;
}else {
/* 直接返回线程本地变量 */
return connThreadLocal.get();
}
}
public void addTopic(){
/* 从ThreadLocal中获取线程对应的 */
Statement statement = getConnection().createStatement();
}
}
六、ThreadLocal 内存泄露的原因
1.ThreadLocal使用原理
ThreadLocal的主要用途是实现线程间变量的隔离,表面上他们使用的是同一个ThreadLocal,但是实际上使用的值value是自己独有一份。
当线程使用threadLocal时,是将threadLocal当做当前线程thread的属性ThreadLocalMap中的一个Entry的key值,实际上存放的变量是Entry的value值,实际使用的值是value。
2.内存泄漏原因
Entry将ThreadLocal作为Key,值作为value保存,继承自WeakReference,在下面构造函数的第一行super(k),意味着ThreadLocal对象是一个弱引用。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
内存泄漏的根源上是:由于ThreadLocalMap的生命周期跟Thread一样长,对于重复利用的线程来说,如果没有手动删除(remove()方法) 对应Key就会导致entry(null,value)的对象越来越多,从而导致内存泄漏。
3.为什么 key 要用弱引用
key如果是强引用,假设在使用完ThreadLocal,ThreadLocal ref被回收,但是因为threadlocalMap的Entry使用的是强引用threadLocal(key是threadLocal),造成ThreadLocal无法回收。在没有手动删除Entry以及CurrentThread仍然运行前提下,始终有强引用链CurrentThread Ref -> CurrentThread -> Map(ThreadLocalMap) -> entry,Entry就不会被回收。
事实上,在ThreadLocalMap中的set/getEntry方法中,会对Key为null(ThreadLocal为null)进行判断,如果为null的话,那么也会把value置为null。这也就是说,使用ThreadLocal,CurrentThread仍然运行的情况下,就算忘记调用remove方法,弱引用比强引用可以多一层保障,弱引用的ThreadLocal会被回收。对应value在下一次ThreadLocal调用get()/set()/remove()中的任意一个方法的时候会被清除,从而避免内存泄漏。
4.如何正确使用ThreadLocal
1、将ThreadLocal变量定义为private static,这样ThreadLocal的生命周期就更长。这样就能保证根据ThreadLocal的弱引用访问到Entry的value值,然后remove,防止内存泄漏。
2.每次使用完ThreadLocal,都调用remove()方法。
七、参考来源