目录
- 背景
- 过程
- ThreadLocal
- 什么是ThreadLocal?
- 既然都是保证线程访问的安全性,那么和Synchronized区别是什么呢?
- ThreadLocal的使用
- TheadLocal使用场景
- 原理
- 高并发场景下ThreadLocal会造成内存泄漏吗?什么原因导致?如何避免?
- 如何避免
背景
明确ThreadLocal和Synchronized 之间的区别
过程
ThreadLocal
什么是ThreadLocal?
ThreadLocal英文翻译过来就是:线程本地量,它其实是一种线程的隔离机制,保障了多线程环境下对于共享变量访问的安全性。
看到上面的定义之后,那么问题就来了,ThreadLocal是如何解决共享变量访问的安全性的呢?
其实ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。由于副本都归属于各自的线程,所以就不存在多线程共享的问题了。
便于理解,我们看一下下图。
至于上述图中提及的threadLocals(ThreadLocalMap),我们后文看源代码的时候再继续来看。大家心中暂时有个概念。
既然都是保证线程访问的安全性,那么和Synchronized区别是什么呢?
在上面聊到共享变量访问安全性的问题上,其实大家还会很容易想起另外一个关键字Synchronized。聊聊区别吧,整理了一张图,看起来可能会更加直观一些,如下。
通过上图,我们发现ThreadLocal其实是一种线程隔离机制。Synchronized则是一种基于Happens-Before规则里的监视器锁规则从而保证同一个时刻只有一个线程能够对共享变量进行更新。
Synchronized加锁会带来性能上的下降。ThreadLocal采用了空间换时间的设计思想,也就是说每个线程里面都有一个专门的容器来存储共享变量的副本信息,然后每个线程只对自己的变量副本做相对应的更新操作,这样避免了多线程锁竞争的开销。
ThreadLocal的使用
上面说了这么多,咱们来使用一下。就拿SimpleDateFormat来做个例子。当然也会有一道这样的面试题,SimpleDateFormat是否是线程安全的?在阿里Java开发规约中,有强制性地提到SimpleDateFormat 是线程不安全的类。其实主要的原因是由于多线程操作SimpleDateFormat中的Calendar对象引用,然后出现脏读导致的。
public class DateFormatTest {
private static final SimpleDateFormat simpleDateFormat =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static Date parse(String dateString) {
Date date = null;
try {
date = simpleDateFormat.parse(dateString);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
executorService.execute(()->{
System.out.println(parse("2024-02-01 23:34:30"));
});
}
executorService.shutdown();
}
}
上述咱们通过线程池的方式针对SimpleDateFormat进行了测试
们通过ThreadLocal的方式将其优化一下。代码如下:
public class DateFormatTest {
private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static Date parse(String dateString) {
Date date = null;
try {
date = dateFormatThreadLocal.get().parse(dateString);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 20; i++) {
executorService.execute(()->{
System.out.println(parse("2024-02-01 23:34:30"));
});
}
executorService.shutdown();
}
}
TheadLocal使用场景
- 上面针对SimpleDateFormat的封装也算是一个吧。
- 用来替代参数链传递:在编写API接口时,可以将需要传递的参数放入ThreadLocal中,从而不需要在每个调用的方法上都显式地传递这些参数。这种方法虽然不如将参数封装为对象传递来得常见,但在某些情况下可以简化代码结构。
- 数据库连接和会话管理:在某些应用中,如Web应用程序,ThreadLocal可以用来保持对数据库连接或会话的管理,以简化并发控制并提高性能。例如,可以使用ThreadLocal来维护一个连接池,使得每个请求都能共享相同的连接,而不是每次都需要重新建立连接。
- 全局存储信息:例如在前后端分离的应用中,ThreadLocal可以用来在服务端维护用户的上下文信息或者一些配置信息,而不需要通过HTTP请求携带大量的用户信息。这样做可以在不改变原有架构的情况下,提供更好的用户体验。
项目使用:
个代码实际上使用了单例模式,具体来说是使用了双重检查锁定(double-checked locking)的变种,也被称为静态内部类(static inner class)单例模式。这种实现方式利用了Java类加载机制的特性,在UserContextHolder类首次被加载时,会创建SingletonHolder类的一个实例,同时初始化其中的sInstance字段,这是线程安全的。
下面是关于代码中单例模式使用的详细解释:
UserContextHolder类有一个私有的构造器private UserContextHolder(),这是为了确保外部无法直接实例化UserContextHolder。
getInstance()方法用于获取UserContextHolder的实例。这个方法调用了UserContextHolder.SingletonHolder.sInstance,而sInstance是在SingletonHolder静态内部类中初始化的。
SingletonHolder是一个私有的静态内部类,它持有一个静态的UserContextHolder实例sInstance。由于Java的类加载机制保证了静态内部类只会在第一次被引用时加载,因此sInstance只会被初始化一次,并且这个过程是线程安全的。
所以,尽管代码中并没有直接显示诸如synchronized关键字或者显式的锁机制,但通过利用Java的类加载机制,这个实现已经保证了单例的创建是线程安全的。
此外,UserContextHolder类使用ThreadLocal来存储线程上下文。每个线程都有它自己的ThreadLocal变量副本,因此,setContext、getContext和clear方法可以在不同的线程中独立地设置、获取和清除它们的上下文数据,而不会影响到其他线程。
这样的话,每个请求过来的时候都在在过滤器那里解密,从而获取一些信息存储在ThreadLocal 变量中,然后在需要这些参数的时候直接拿出啦,使用,我认为这里既进行了全局存储信息,有代替参数链传递,而且这里还用了单例模式,
原理
图中有两个线程Thread1以及Thread2。
Thread类中有一个叫做threadLocals的成员变量,它是ThreadLocal.ThreadLocalMap类型的。
ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型对象值。
高并发场景下ThreadLocal会造成内存泄漏吗?什么原因导致?如何避免?
首先明确前提:
引用的强弱,稍微聊一下,这里其实涉及到jvm的回收机制。在JDK1.2之后,java对引用的概念其实做了扩充的,分为强引用,软引用,弱引用,虚引用。
强引用:其实就是咱们一般用“=”的赋值行为,如 Student s = new Student(),只要强引用还在,对象就不会被回收。
软引用:不是必须存活的对象,jvm在内存不够的情况下即将内存溢出前会对其进行回收。例如缓存。
弱引用:非必须存活的对象,引用关系比软引用还弱,无论内存够还是不够,下次的GC一定会被回收。
虚引用:别名幽灵引用或者幻影引用。等同于没有引用,唯一的目的是对象被回收的时候会受到系统通知。
明白这些概念之后,咱们再看看上面的源代码,我们就会发现,原来Key其实是弱引用,而里面的value因为是直接赋值行为所以是强引用。
图中我们可以看到由于threadLocal对象是弱引用,如果外部没有强引用指向的话,它就会被GC回收,那么这个时候导致Entry的key就为NULL,如果此时value外部也没有强引用指向的话,那么这个value就永远无法访问了,按道理也该被回收。但是由于entry还在强引用value(看源代码)。那么此时value就无法被回收,此时内存泄漏就出现了。本质原因是因为value成为了一个永远无法被访问也无法被回收的对象。
那肯定有小伙伴会有疑问了,线程本身生命周期不是很短么,如果短时间内被销毁,就不会内存泄漏了,因为只要线程销毁,那么value也会被回收。这话是没错。但是咱们的线程是计算机珍贵资源,为了避免重复创建线程带来开销,系统中我们往往会使用线程池,如果使用线程池的话,那么线程的生命周期就被拉长了,那么就可想而知了。
内存泄漏的原因:
1、长生命周期的 ThreadLocal:
如果 ThreadLocal 的实例被声明为静态的,那么它的生命周期将会很长,可能会与应用程序的生命周期一样长。
2、ThreadLocalMap 的引用:
当线程结束时,通常它的栈帧会被垃圾回收器回收。但是,如果 ThreadLocal 的实例还存在(因为它是静态的),那么它持有的 ThreadLocalMap 的引用就不会被释放。
3、Entry 的强引用:
在 ThreadLocalMap 中,键(ThreadLocal 的弱引用)和值(实际存储的对象)都是强引用。这意味着即使 ThreadLocal 的键被回收(因为它是弱引用),只要值还存在,Entry 对象就不会被回收。
4、内存泄漏的发生:
如果线程池中的线程被重复使用,而 ThreadLocal 没有在不再需要时被正确清理(即调用 remove() 方法),那么 ThreadLocalMap 中存储的值就会一直存在,即使这些值已经不再需要。随着时间的推移,这会导致内存泄漏,因为垃圾回收器无法回收这些不再使用的对象。
如何避免
每次使用完毕之后记得调用一下remove()方法清除数据。
ThreadLocal变量尽量定义成static final类型,避免频繁创建ThreadLocal实例。这样可以保证程序中一直存在ThreadLocal强引用,也能保证任何时候都能通过ThreadLocal的弱引用访问Entry的value值,从而进行清除。
不过话说回来,其实ThreadLocal内部也做了优化的。在set()的时候也会采样清理,扩容的时候也会检查(这里希望大家自己深入看一下源代码),在get()的时候,如果没有直接命中或者向后环形查找的时候也会进行清理。但是为了系统的稳健万无一失,所以大家尽量还是将上面的两个注意点在写代码的时候注意下。