ThreadLocal翻译成中文比较准确的叫法应该是:线程局部变量。
这个玩意有什么用处,或者说为什么要有这么一个东东?先解释一下,在并发编程的时候,成员变量如果不做任何处理其实是线程不安全的,各个线程都在操作同一个变量,显然是不行的,并且我们也知道volatile这个关键字也是不能保证线程安全的。那么在有一种情况之下,我们需要满足这样一个条件:变量是同一个,但是每个线程都使用同一个初始值,也就是使用同一个变量的一个新的副本。这种情况之下ThreadLocal就非常使用,比如说DAO的数据库连接,我们知道DAO是单例的,那么他的属性Connection就不是一个线程安全的变量。而我们每个线程都需要使用他,并且各自使用各自的。这种情况,ThreadLocal就比较好的解决了这个问题。
在使用之前,我们先来认识几个ThreadLocal的常用方法
方法声明 | 描述 |
---|---|
ThreadLocal() | 创建ThreadLocal对象 |
public void set( T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
我们从源码的角度来分析这个问题。
首先定义一个ThreadLocal:
public final class ConnectionUtil {
private ConnectionUtil() {}
private static final ThreadLocal<Connection> conn = new ThreadLocal<>();
public static Connection getConn() {
Connection con = conn.get();
if (con == null) {
try {
Class.forName("com.mysql.jdbc.Driver");
con = DriverManager.getConnection("url", "userName", "password");
conn.set(con);
} catch (ClassNotFoundException | SQLException e) {
// ...
}
}
return con;
}
}
这样子,都是用同一个连接,但是每个连接都是新的,是同一个连接的副本。
那么实现机制是如何的呢?
1、每个Thread对象内部都维护了一个ThreadLocalMap这样一个ThreadLocal的Map,可以存放若干个ThreadLocal。
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
2、当我们在调用get()方法的时候,先获取当前线程,然后获取到当前线程的ThreadLocalMap对象,如果非空,那么取出ThreadLocal的value,否则进行初始化,初始化就是将initialValue的值set到ThreadLocal中。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
3、当我们调用set()方法的时候,很常规,就是将值设置进ThreadLocal中。
4、总结:当我们调用get方法的时候,其实每个当前线程中都有一个ThreadLocal。每次获取或者设置都是对该ThreadLocal进行的操作,是与其他线程分开的。
5、应用场景:当很多线程需要多次使用同一个对象,并且需要该对象具有相同初始化值的时候最适合使用ThreadLocal。
具体的过程是这样的:
(1) 每个Thread线程内部都有一个Map (ThreadLocalMap)
(2) Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
(3)Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
(4)对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
虽然ThreadLocal模式与synchronized关键字都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同。
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用’以时间换空间’的方式, 只提供了一份变量,让不同的线程排队访问 | ThreadLocal采用’以空间换时间’的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
JAVA基础补充
JDK 1.2
版本开始,对象的引用被划分为 4种级别,使程序能更加灵活地控制对象的生命周期。这4
种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
- 强引用: new出来的对象就是强引用类型,只要强引用存在 GC将永远不会回收被引用的对象,哪怕内存不足的时候
- 软引用:使用
SoftReference
修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收- 弱引用:使用
WeakReference
修饰的对象被称为弱引用,弱引用指向的对象只要发生 GC 就会被回收- 虚引用:使用
PhantomReference
进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知总结
引用类型 被垃圾回收时间 用途 生存时间 强引用 从来不会 对象的一般状态 JVM停止运行时终止 软引用 当内存不足时 对象缓存 内存不足时终止 弱引用 正常垃圾回收时 对象缓存 垃圾回收后终止 虚引用 正常垃圾回收时 跟踪对象的垃圾回收 垃圾回收后终止
内存泄漏相关概念
ThreadLocal内存泄漏问题
内存泄漏问题,我发现网上很多描述是ThreadLocals的Entry的key为弱引用,在gc时,threadLocal对象被回收,造成key为null,value无法清除的问题,从而导致内存泄漏。我先说明观点,这次现象是存在的,但是在业务代码里是不可能出现的。
内存泄漏场景(不使用remove)
如果线程没有被回收(可能是被线程池管理,或者短时间内创建了大量的线程),那么每个线程对象内,都维护了一个threadLocalMap, 假设我们在项目里定义了50个threadlocal,有50个线程,每个线程内都维护了50个threadlocal的key-value缓存,那么极限情况下,就有2500个key-value缓存同时存在。如果value比较大,是有可能把内存撑爆的。
假设这2500个key-value缓存没有把内存撑爆,那么始终会占据一部分不小的内存,假设是30%,如果其他业务代码,需要用到大量的内存操作,比如80%,那么同样也会oom,这次oom,其他业务代码负主要责任,但是这2500个key-value缓存,同样也是造成oom的凶手,因为没有这些缓存,是不会发生oom的。所以不remove掉这些键值对,会增大oom的风险。
什么时候key是null?当这个threadlocal没有被虚拟机栈引用,没有被类静态成员变量引用,那么threadlocal是会在下一次gc的时候被回收的,这个时候key为null。写法就是在方法内new一个threadlocal,我认为这种写法本身就失去了用threadlocal的目的,我是想不到这样的写法在什么场景下适用,如果有,也请评论区留言,我修正文章观点。
总结
由此可以发现,使用ThreadLocal造成内存泄露的问题是因为:ThreadLocalMap的生命周期与Thread一致,如果不手动清除掉Entry对象的话就可能会造成内存泄露问题。
因此,需要我们在每次在使用完之后需要手动的remove掉Entry对象。
ThreadLocalMap作为一个HashMap和java.util.HashMap的实现是不同的。对于java.util.HashMap使用的是链表法来处理冲突:
但是,对于ThreadLocalMap,它使用的是简单的线性探测法,如果发生了元素冲突,那么就使用下一个槽位存放: