ThreadLocal的作用
ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据。为多线程环境下,变量安全提供的一种解决思路,ThreadLocal是给没一个线程建立一个自己的单独的变量副本,每个线程都可以独立的去改变自己的变量副本,从而不会影响其他线程。
ThreadLocal源码
get() 用来获取ThreadLocal在当前线程中保存的变量副本
代码语言:java复制public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 操作ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
set() 用来设置当前线程中的变量副本
代码语言:java复制public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
remove() 用来移除当前线程中变量的副本。
代码语言:javascript复制public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
initialValue() 是一个protected方法,一般是用来使用时进行重写。
代码语言:javascript复制
protected T initialValue() {
return null;
}
ThreadLocal的原理
通过上述ThreadLocal的源码查看,其内部主要依靠ThreadLocalMap。内部类ThreadLocalMap才是真正实现线程隔离机制的关键,类似Map,由键值对key/value组成的一个Entry数组,key是ThreadLocal本身的一个弱引用,也就是当前ThreadLocal
对象,value就是对应的线程变量副本值。
ThreadLocalMap数据结构,采用数组 开放地址法
代码语言:javascript复制static class Entry extends WeakReference<> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
ThreadLocal注意事项
上述原理可以看到,Entry继承了WeakReference,即Entry的key是弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉(GC)。所以key会在垃圾回收的时候被回收掉, 而key对应的value则不会被回收, 这样会导致一种现象:key为null,value有值。
所以,如果ThreadLocal对象越来越多,久而久之,ThreadLocalMap里面就会有很多没有key的value,就会造成内存不足,进而内存泄漏。
解决办法内存泄漏
(1)自动清除,ThreadLocal会自动清除key为null的value
ThreadLocal的get()、set()的时候都会清除线程ThreadLocalMap里所有key为null的value。这也是ThreadLocal的巧妙,防止程序忘记手动remove。
(2)手动清除,使用完毕后及时调用ThreadLocal.remove()
remove方法会主动将当前的key和value(Entry)进行清除。
(3)把ThreadLocal设置为全局变量
ThreadLocal设置为全局变量使得它无法被GC回收。因为成员变量中使用就将修饰符设置为public static,那么内存中一直存在这个key,所以value也就不会存在key为null的情况, 也就不会内存泄漏。
模拟ThreadLocal内存泄露案例
设置程序堆大小
接下来用代码模拟ThreadLocal内存泄露的问题,并查看有没有手动回收的情况,有什么不一样。案例代码如下,也就是,创建30个对象,每个大对象,占用 10 m。
代码语言:java复制public class ThreadLocalOOM {
// 全局ThreadLocal变量
public static ThreadLocal<LargeObject> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(30);
// 提交大量任务到线程池
for (int i = 0; i < 30; i ) {
executor.submit(new Runnable() {
@Override
public void run() {
// 在每个任务中分配一个大对象,并存入ThreadLocal
LargeObject largeObject = new LargeObject();
threadLocal.set(largeObject);
}
});
}
// 关闭线程池,但这并不会自动清理线程局部变量
executor.shutdown()
}
static class LargeObject {
// 假设这是一个非常大的对象,占用大量内存
byte[] data = new byte[1024 * 1024 * 10]; // 10MB的数据数组
}
}
没有手动remove
调用remove,不会出现内存泄漏
那么为什么要这样设计呢?
问题1:Entry的key可以设置为强引用吗?
若是强引用,即使线程变为null,key的引用依然指向ThreadLocal对象,GC也不会回收,导致这一块内存一直存在,除非该线程结束,也就是只能手动删除,否则这对于程序来说,就出现了内存泄漏。
问题2:Entry的value可以设置为弱引用吗?
假如value被设计成弱引用,那么很有可能当你需要取这个value值的时候,取出来的值是一个null。因为一旦把本应该强引用的value设计成了弱引用,那么只要jvm执行一次GC操作,value就直接被回收掉了。当你需要从当前线程中取值的时候,最终得到的就是null。
使用场景
(1)全局存储用户信息,服务端拦截用户请求,将用户的验证信息保存到ThreadLocal中,使得整个线程上下文都可以获取,就不用每次去获取用户信息。
(2)多线程进行日期格式化
由于SimpleDateFormat是线程不安全的,导致多个线程共享这个同一个对象时出现线程不安全问题,所以多线程访问可能会报错。
代码语言:java复制public class DateUtilNotSale {
private static final ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);
public static Date parse(String dateStr){
Date date = null;
try {
// date = sdf.parse(dateStr); parse线程不安全,没有做线程安全处理,不能保证原子性,
// 导致多个线程共享这个同一个对象时出现线程不安全问题
date = sdf.get().parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}