前言
ThreadLocal是Java中的一个类,用于创建线程本地变量,即每个线程都有自己的变量副本,互不干扰。
一、Threadlocal通俗理解
1.1定义
它能够提供了线程的局部变量,让每个线程都可以通过set/get来对这个局部变量进行操作。 不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。
1.2场景
1.2.1日期时间格式化工具类
格式化/转化的实现是用的SimpleDateFormat。
但众所周知SimpleDateFormat不是线程安全的,所以我们就用ThreadLocal来让每个线程装载着自己的SimpleDateFormat对象,以达到在格式化时间时,线程安全的目的。
在方法上创建SimpleDateFormat对象也没问题,但每调用一次就创建一次有点不优雅。
1.2.2Spring事务
Spring提供了事务相关的操作,而我们知道事务是得保证一组操作同时成功或失败的。 这意味着我们一次事务的所有操作需要在同一个数据库连接上。 但是在我们日常写代码的时候是不需要关注这点的。
Spring就是用的ThreadLocal来实现,ThreadLocal存储的类型是一个Map 。 Map中的key 是DataSource,value 是Connection (为了应对多数据源的情况所以是一个Map)。 用了ThreadLocal保证了同一个线程获取一个Connection对象,从而保证一次事务的所有操作需要在同一个数据库连接上。
1.3原理
ThreadLocal是一个壳子,真正的存储结构是ThreadLocal里有ThreadLocalMap这么个内部类 而有趣的是,ThreadLocalMap的引用是在Thread上定义的。
ThreadLocal本身并不存储值,它只是作为key来让线程从ThreadLocalMap获取value。 所以,得出的结论就是ThreadLocalMap该结构本身就在Thread下定义,而ThreadLocal只是作为key,存储set到ThreadLocalMap的变量当然是线程私有的咯。
疑问:
我可以在ThreadLocal下定义Map,key是Thread,value是set进去的值吗? 就是说,为啥我要把ThreadLocal做为key,而不是Thread做为key?这样不是更清晰吗?
答案:
实际上就是所有的线程都访问ThreadLocal的Map,而key是当前线程。 但这有点小问题,一个线程是可以拥有多个私有变量的嘛,那key如果是当前线程的话,意味着还点做点 [手脚],来唯一标识set进去的value 。
还有个问题就是:并发量足够大时,意味着所有的线程都去操作同一个Map,Map体积有可能会膨胀,导致访问性能的下降。 并且这个Map维护着所有的线程的私有变量,意味着你不知道什么时候可以[销毁]。
现在JDK实现的结构就不一样了。 线程需要多个私有变量,那有多个ThreadLocal对象足以,对应的Map体积不会太大。
只要线程销毁了,ThreadLocalMap也会被销毁品。
二、内存泄漏
在使用ThreadLocal时,如果没有及时清理ThreadLocal变量,就会导致内存泄漏问题。
讲内存泄漏问题前,先科普一下Java中有四种引用类型,分别为:
- 强引用 (Strong Reference):被强引用关联的对象不会被垃圾回收器回收,只有在该引用被显式地赋值为null后,对象才会被回收。
- 软引用 (Soft Reference):被软引用关联的对象只有在内存不足时才会被垃圾回收器回收。
- 弱引用 (Weak Reference):被弱引用关联的对象在下一次垃圾回收时一定会被回收。
- 虚引用 (Phantom Reference):被虚引用关联的对象在任何时候都可能被垃圾回收器回收。虚引用通常用于跟踪对象被垃圾回收的状态,可以在对象被回收时收到一个系统通知。
ThreadLocal的内部实现是将每个线程维护的变量存储在一个Map中,线程的ThreadLocal变量作为Map的key,而变量值则作为Map的value。当线程结束时,ThreadLocal变量不会被垃圾回收器回收,因为它们仍然被Map所引用。
如果不主动清理ThreadLocal变量,就会导致Map中的所有对象都无法被垃圾回收,从而引起内存泄漏。一般来说,需要在ThreadLocal使用完毕后及时地调用remove方法将其删除。
以下是一个示例代码,演示了如何使用ThreadLocal解决线程安全问题,并展示了如何避免内存泄漏:
代码语言:javascript复制public class MyService {
private static ThreadLocal<Object> threadLocal = new ThreadLocal<>();
public void setValue(Object value) {
threadLocal.set(value);
}
public Object getValue() {
return threadLocal.get();
}
public void removeValue() {
threadLocal.remove();
}
}
在使用ThreadLocal时,需要注意以下几点:
- 尽量避免创建过多的ThreadLocal变量,因为每个ThreadLocal都需要占用一定的内存。
- 在ThreadLocal使用完毕后,必须调用remove方法将其删除,否则会引起内存泄漏。
- 在使用ThreadLocal时,建议使用try-finally语句块,确保即使出现异常,也能够及时地清理ThreadLocal变量,避免内存泄漏问题。
疑问1:
为什么要将ThreadLocalMao的key设置为弱引用呢?强引用不香吗?
回答:
外界是通过ThreadLocal来对ThreadLocalMap进行操作的,假设外界使用ThreadLocal的对象被置null了,那ThreadLocalMap的强引用指向ThreadLocal也毫无意义啊。
弱引用反而可以预防大多数内存泄漏的情况。 毕竟被回收后,下一次调用set/get/remove时ThreadLocal内部会清除掉。
疑问2:
建议把ThreadLocal修饰为static,为什么?
回答:
ThreadLocal能实现了线程的数据隔离不在于它自己本身,而在于Thread的Thr eadLocalMap。
所以,ThreadLocal可以只初始化一次只分配一块存储空间就足以了,没必要作为成员变量多次被初始化