什么是ThreadLocal?
ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。 ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal 提供了线程本地的实例。它其实是一种线程的隔离机制,保障了多线程环境下对于共享变量访问的安全性。
ThreadLocal与Synchronized的区别
ThreadLocal和Synchonized都用于解决多线程并发访问。 ThreadLocal与synchronized有本质的区别:
- Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
- Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本 ,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
TheadLocal使用场景
场景一:代替参数的显式传递
当我们在写API接口的时候,通常Controller层会接受来自前端的入参,当这个接口功能比较复杂的时候,可能我们调用的Service层内部还调用了 很多其他的很多方法,通常情况下,我们会在每个调用的方法上加上需要传递的参数。
但是如果我们将参数存入ThreadLocal中,那么就不用显式的传递参数了,而是只需要ThreadLocal中获取即可。
场景二:全局存储用户信息
在现在的系统设计中,前后端分离已基本成为常态,分离之后如何获取用户信息就成了一件麻烦事,通常在用户登录后, 用户信息会保存在Session或者Token中。这个时候,我们如果使用常规的手段去获取用户信息会很费劲,拿Session来说,我们要在接口参数中加上HttpServletRequest对象,然后调用 getSession方法,且每一个需要用户信息的接口都要加上这个参数,才能获取Session,这样实现就很麻烦了。
在实际的系统设计中,我们肯定不会采用上面所说的这种方式,而是使用ThreadLocal,我们会选择在拦截器的业务中, 获取到保存的用户信息,然后存入ThreadLocal,那么当前线程在任何地方如果需要拿到用户信息都可以使用ThreadLocal的get()方法 (异步程序中ThreadLocal是不可靠的)
场景三:解决线程安全问题
在Spring的Web项目中,我们通常会将业务分为Controller层,Service层,Dao层, 我们都知道@Autowired注解默认使用单例模式,那么不同请求线程进来之后,由于Dao层使用单例,那么负责数据库连接的Connection也只有一个, 如果每个请求线程都去连接数据库,那么就会造成线程不安全的问题,Spring是如何解决这个问题的呢?
在Spring项目中Dao层中装配的Connection肯定是线程安全的,其解决方案就是采用ThreadLocal方法,当每个请求线程使用Connection的时候, 都会从ThreadLocal获取一次,如果为null,说明没有进行过数据库连接,连接后存入ThreadLocal中,如此一来,每一个请求线程都保存有一份 自己的Connection。于是便解决了线程安全问题
ThreadLocal原理
- 图中有两个线程Thread1以及Thread2。
- Thread类中有一个叫做threadLocals的成员变量,它是ThreadLocal.ThreadLocalMap类型的。
- ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型对象值。
set
public void set(T value) {
//1、获取当前线程
Thread t = Thread.currentThread();
//2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
//则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 初始化thradLocalMap 并赋值
createMap(t, value);
}
ThreadLocal set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。
ThreadLocalMap
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。
get
public T get() {
//1、获取当前线程
Thread t = Thread.currentThread();
//2、获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//3、如果map数据不为空,
if (map != null) {
//3.1、获取threalLocalMap中存储的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
通过 ThreadLocalMap 获取当前线程的存储的 Value 值
设置 ThreadLocal 的初始化值,未 set(T value) 初次获取 Thread 对应的 Value 值时会调用,即被 setInitialValue 方法调用。需要重写该方法。
remove
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
remove方法,直接将ThrealLocal 对应的值从当前相差Thread中的ThreadLocalMap中删除。
总结
- Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
- ThreadLocalMap方法内部维护者Entry数组,其中key是ThreadLocal本身,而value则为其泛型值。
- 并发场景下,每个线程都会存储当前变量副本到自己的ThreadLocalMap中,后续这个线程对于共享变量的操作,都是从TheadLocalMap里进行变更,不会影响全局共享变量的值。
内存泄漏
原因
目前程序运行大多采用的是线程池模式,线程存在时间很长,如果不断往其中加入线程私有对象而得不到回收,会导致OOM。所以为了减少程序员手动回收,同时兼顾避免OOM,设计了一套弱引用自动回收机制。
由于threadLocal对象是弱引用,如果外部没有强引用指向的话,它就会被GC回收,那么这个时候导致Entry的key就为NULL,如果此时value外部也没有强引用指向的话,那么这个value就永远无法访问了,按道理也该被回收。但是由于entry还在强引用value,那么此时value就无法被回收,此时内存泄漏就出现了。本质原因是因为value成为了一个永远无法被访问也无法被回收的对象。
避免方法
每次使用完毕之后记得调用一下remove()方法清除数据。 ThreadLocal变量尽量定义成static final类型,避免频繁创建ThreadLocal实例。这样可以保证程序中一直存在ThreadLocal强引用,也能保证任何时候都能通过ThreadLocal的弱引用访问Entry的value值,从而进行清除。
ThreadLocal面试题
你能解释一下ThreadLocal是什么以及它在Java中通常用来做什么吗?
- ThreadLocal 是Java中的一个类,它提供了一种线程本地变量的机制。即这些变量对于使用同一个变量的每个线程都有独立的初始化副本,避免了线程安全问题。
- ThreadLocal通常是为了在一次请求的上下文中临时存储和传递数据,确保数据的安全性和独立性。它特别适合存储诸如用户认证信息或其他需要在一个请求生命周期内保持的数据。
在项目中,如何使用ThreadLocal来存储和管理用户的认证信息?
- 初始化︰在拦截器的preHandle方法中,可以从请求中提取JWT Token,并进行校验。一旦校验通过,就把用户的信息从Token中解析出来,并存储到ThreadLocal中。
- 使用:在请求处理过程中,不同的组件(如Service层或Repository层)可以通过ThreadLocal获取之前存储的信息,用于执行业务逻辑、访问数据库等操作。这样做的好处是,它消除了通过方法参数传递信息的需要,使得方法签名更简洁、逻辑更清晰。
- 清理:在请求的生命周期即将结束时,例如在返回响应之前,需要显式清除ThreadLocal中的数据。在拦截器的afterCompletion方法中完成清理,确保每个请求结束后清理掉所有关联的数据,防止内存泄漏。
为什么使用ThreadLocal?
- 性能:避免了在每个方法调用中传递数据,减少了方法参数的复杂性,有助于保持代码的清洁和易管理,具备便捷性。
- 数据隔离:每个请求的数据存储在独立的线程中,确保了数据的隔离性,避免了并发访问中的数据安全问题。
- 避免锁竞争:由于每个线程都有自己的数据副本,不需要使用锁来保护共享数据,从而避免了锁竞争和线程间的同步开销。
使用需要谨慎:一定要在每个请求结束时清理ThreadLocal存储的数据。未能清理可能导致严重的内存泄漏。在线程池中服用复用同一个线程未及时清理会导致下一次HTTP请求时得到上一次ThreadLocal存储的结果。
讲讲ThreadLocal的数据结构,为什么能实现数据隔离?
- Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
- ThreadLocalMap的类内部维护着Entry数组(这种哈希表的形式,解决Hash冲突使用的开放定址法,即继续寻找下一个空的位置),其中key是ThreadLocal本身,而value则为我指定的泛型值。
- 并发场景下,每个线程都会存储当前变量副本到自己的ThreadLocalMap中,后续这个线程对于共享变量的操作,都是从TheadLocalMap里进行变更,不会影响全局共享变量的值。
- 具体而言,当执行存入数据,执行Set方法,是通过Thread.currentThread()获取当前线程对象,然后通过getMap方法,这个方法会从当前线程的threadLocals 变量中取出这个map,然后进行判断,如果这个map不为空,就设置值,如果为空,就调用创建Map的方法设置值。因此每个线程是单独set自己的 ThreadLocalMap,可以实现数据隔离。
高并发场景下ThreadLocal会造成内存泄漏吗?什么原因导致?如何避免?
- 会,因为ThreadLocalMap是由一个个Entry构成的数组,并且每个Entry的key是弱引用,这就意味着当触发GC时,Entry的key也就是ThreadLocal就会被回收。(当一个对象仅仅被弱引用指向,GC运行,这个对象就会被回收,不论当前的内存空间是否足够,这个对象都会被回收。)
- 如果此时value外部也没有强引用指向的话,那么这个value就永远无法访问了,按道理也该被回收。但是由于entry还在强引用value。那么此时value就无法被回收,此时内存泄漏就出现了。本质原因是因为value成为了一个永远无法被访问也无法被回收的对象。
- 避免:
- 每次使用完毕之后记得调用一下remove()方法清除数据。
- ThreadLocal变量尽量定义成static final类型,避免频繁创建ThreadLocal实例。这样可以保证程序中一直存在ThreadLocal强引用,在内存中只有一个副本,这种声明方式确保了ThreadLocal对象自身不会被垃圾回收,从而保证了线程可以安全地访问和操作存储在其中的数护,直到显式地进行清理。