ThreadLocal详解
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关键字
虽然ThreadLocal模式与synchronized关键字都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同。
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用’以时间换空间’的方式, 只提供了一份变量,让不同的线程排队访问 | ThreadLocal采用’以空间换时间’的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
ThreadLocal内存泄漏问题
JAVA基础补充
JDK 1.2
版本开始,对象的引用被划分为 4种级别,使程序能更加灵活地控制对象的生命周期。这4
种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
- 强引用: new出来的对象就是强引用类型,只要强引用存在 GC将永远不会回收被引用的对象,哪怕内存不足的时候
- 软引用:使用
SoftReference
修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收- 弱引用:使用
WeakReference
修饰的对象被称为弱引用,弱引用指向的对象只要发生 GC 就会被回收- 虚引用:使用
PhantomReference
进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知总结
引用类型 被垃圾回收时间 用途 生存时间 强引用 从来不会 对象的一般状态 JVM停止运行时终止 软引用 当内存不足时 对象缓存 内存不足时终止 弱引用 正常垃圾回收时 对象缓存 垃圾回收后终止 虚引用 正常垃圾回收时 跟踪对象的垃圾回收 垃圾回收后终止
内存泄漏相关概念
- Memory overflow:内存溢出,没有足够的内存提供申请者使用。
- Memory leak: 内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。
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中的Hash冲突处理
ThreadLocalMap作为一个HashMap和java.util.HashMap的实现是不同的。对于java.util.HashMap使用的是链表法来处理冲突:
但是,对于ThreadLocalMap,它使用的是简单的线性探测法,如果发生了元素冲突,那么就使用下一个槽位存放: