什么是ThreadLocal?有哪些应用场景?
ThreadLocal 类可以让每个线程绑定自己的值,也就是拥有自己的专属本地变量。
ThreadLocal 为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,并且不会和其他线程的变量冲突,实现了线程间的数据隔离,避免了线程安全问题。
ThreadLocal 的应用场景主要有以下几个方面:
- 保存线程上下文信息,避免参数的显示传递,在需要的地方可以直接获取
- 线程间数据隔离
- 进行事务操作时存储线程事务信息,因为事务和线程绑定在一起(Spring在事务开始时会给当前线程绑定一个Jdbc Connection对象,放在ThreadLocal中存储,这样在整个事务执行过程中都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性)
- 数据库连接(经典的使用场景是为每个线程分配一个JDBC Connection连接对象,这样可以保证每个线程的都在各自的Connection上进行数据库的操作,不会出现A线程关了B线程正在使用的Connection)
- session会话等线程级别的操作(Session 的特性很适合 ThreadLocal ,因为 Session 之前当前会话周期内有效,会话结束便销毁)
ThreadLocal 原理
从Thread 类的源代码可以看出Thread 类中有一个 threadLocals 和一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,实际上当前线程调用 ThreadLocal 类的 set 或get 方法时,我们调用的是当前线程的ThreadLocalMap 类对应的 get() 、set() 方法。
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
ThreadLocal 是线程本地存储,每个线程中都具备一个ThreadLocalMap ,ThreadLocalMap 可以存储以ThreadLocal 为 key ,Object 对象为 value 的键值对。ThreadLocalMap 是ThreadLocal 的内部类,可以理解为一个Map容器,其维护了一个 Entry 数组,由一个个key-value对象Entry 构成。
由于每一条线程都含有线程私有的ThreadLocalMap容器,这些容器间相互独立不影响,因此不会存在线程安全的问题,从而无需使用同步机制来保证多条线程访问容器的互斥性。
- 使用set方法时:
ThrealLocal 类中可以通过Thread.currentThread() 获取到当前线程对象后,直接通过getMap(Thread t) 可以访问到该线程的ThreadLocalMap 对象,再以当前的ThreadLocal对象为key,将值存入ThreadLocalMap对象中。
- get方法执行过程类似,首先ThreadLocal获取当前线程对象,然后获取当前线程的ThreadLocalMap对象,再以当前的ThreadLocal对象为key,获取对应的value。
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 T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocal 内存泄漏问题
**内存泄露 **:指的是为程序在申请内存后,无法释放已申请的内存空间。一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存迟早会被占光。简单来说,不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
与OOM的区别:内存泄漏是内存占用无法释放,而OOM是内存不够,内存泄漏会导致OOM。
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,而value还存在着强引用。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set() 、get() 、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal 方法后最好手动调用remove() 方法。
看到Entry继承自 WeakReferencr<ThreadLocal<?>> ,就是一个 key-value 形式的对象。它的 key 就是 ThreadLocal 对象,并且是一个弱引用,如果没有指向 key 的强引用后,该 key 就会被垃圾回收器回收;Entry 的 value 就是存储 Object 对象,是强引用。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
强引用:
指在代码之中普遍存在的引用赋值,即使用 new 对象创建强引用。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会对被引用的对象进行回收。
Object obj = new Object();
如果想要取消强引用和某个对象之间的关联,可以显示将引用赋值为null,或者超过了引用的作用域时,如方法结束,就可以当作垃圾回收,这样JVM就可以在合适的时间对其回收。
弱引用:
也是用来描述那些非必须对象,但被弱引用关联的对象不管内存是否足够都一定会被回收,也就是说它只能存活到下一次垃圾回收发生为止,比起软引用,只具有弱引用的对象拥有更短暂的生命周期。可以使用 WeakReference 类来创建弱引用。
为什么要将key设计成ThreadLocal的弱引用?
如果key 是强引用,同样会发生内存泄漏的。引用ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
如果是弱引用的话,引用ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收,此时的key为null,但在下一次调用ThreadLocalMap的set()、get()、remove()方法时,会清除 key 为 null 的 value 值,避免内存泄漏。
因此,ThreadLocal内存泄漏的根本原因是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
所以两种方案比较下来,还是ThreadLoacl 的key 为弱引用好一些。
ThreadLocal的正确使用方法:
- 当ThreadLocal作为局部变量时,每次使用完(方法结束)都调用其 remove() 方法清除数据(生命周期不需要和项目的生存周期一样长的)。
- 将ThreadLocal变量定义成为private static,这样就一直存在ThreadLocal的强引用,ThreadLocal就不会轻易被回收,可以保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉无用的value。
|