让你读懂ThreadLocal的原理和应用以及面试
1.为什么需要ThreadLocal
我们举个生活中的例子来说明:
你带着三个孩子出去逛街,路过了玩具店,三个孩子都看中了一款变形金刚。
所以你买了一个变形金刚,打算让三个孩子轮着玩。
回到家你发现,孩子因为这个玩具吵架了,三个都争着要玩,谁也不让着谁。
这时候怎么办呢?你可以去拉架,去讲道理,说服孩子轮流玩,但这很累。
所以一个简单的办法就是出去再买两个变形金刚,这样三个孩子都有各自的变形金刚,世界就暂时得到了安宁。
映射到我们今天的主题,变形金刚就是共享变量,孩子就是程序运行的线程。
有多个线程(孩子),争抢同一个共享变量(玩具),就会产生冲突,而程序的解决办法是加锁(父母说服,讲道理,轮流玩),但加锁就意味着性能的消耗(父母比较累)。
所以有一种解决办法就是避免共享(让每个孩子都各自拥有一个变形金刚),这样线程之间就不需要竞争共享变量(孩子之间就不会争抢)。
所以为什么需要 ThreadLocal?
就是为了通过本地化资源来避免共享,避免了多线程竞争导致的锁等消耗。
这里需要强调一下,不是说任何东西都能直接通过避免共享来解决,因为有些时候就必须共享。
举个例子:当利用多线程同时累加一个变量的时候,此时就必须共享,因为一个线程的对变量的修改需要影响要另个线程,不然累加的结果就不对了。
再举个不需要共享的例子:比如现在每个线程需要判断当前请求的用户来进行权限判断,那这个用户信息其实就不需要共享,因为每个线程只需要管自己当前执行操作的用户信息,跟别的用户不需要有交集。
2.什么是Threadlocal?
我们看一下JDK1.8中关于Threadlocal的介绍
此类提供线程局部变量。这些变量与正常变量不同,因为每个访问一个线程(通过其{@code get}或{@code set}方法)的线程都有其自己的,独立初始化的变量副本。{@code ThreadLocal}实例通常是希望将状态与线程相关联的类中的私有静态字段(例如用户ID或交易ID)。
3.ThreadLocal的使用场景有哪些
1.可以使每个线程需要一个独享的对象,每个Thread内有自己的实例副本比如构造一个线程安全的SimpleDateFormat
/** * 生产出线程安全的日期格式化工具 */ class ThreadSafeFormatter { public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); }
2.让每个线程内需要保存全局变量
例如使用拦截器中获取用户信息,可以让不同方法直接使用,避免参数传递的麻烦
class Interceptor { public void preHandle(String userId) { //模拟查询数据库后获取到的用户信息 User user = new User("月伴飞鱼 " + userId); UserContextHolder.holder.set(user); new Service().process(); } } class UserContextHolder { public static ThreadLocal<User> holder = new ThreadLocal<>(); } class User { String name; public User(String name) { this.name = name; } }
3.Spring中也有使用到ThreadLocal的地方
DateTimeContextHolder类,用到了ThreadLocal
public final class DateTimeContextHolder { private static final ThreadLocal<DateTimeContext> dateTimeContextHolder = new NamedThreadLocal("DateTimeContext"); }
4.Threadlocal的原理
每个线程中都有一个ThreadLocalMap数据结构,当执行set方法时,其值是保存在当前线程的threadLocals变量中,当执行set方法中,是从当前线程的threadLocals变量获取
//java.lang.Thread#threadLocals ThreadLocal.ThreadLocalMap threadLocals = null;
我们先来写一个小demo来理解一下
package JUC学习.ThreadLockDemo; /** * @author 李斌 * @date 2021/12/29 12:22 */ import java.util.Locale; public class ThreadLockTest { private static ThreadLocal<Integer> local; private static int a =10; public static void main(String[] args) { new Thread(() -> { local = new ThreadLocal<>(); local.set(a+10); System.out.println(local.get()); },"A").start(); new Thread(() -> { local = new ThreadLocal<>(); System.out.println(local.get()); },"B").start(); } }
运行结果:
Threadlocal 怎么实现线程变量的隔离的?
源码解读
首先我们看一下get方法
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { //取出map中属于本ThreadLocal的value ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
我们来分析一下第二个为什么结果是null
首先我们看一下get方法1中的前两行代码
Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t);
第一行我们知道是获取当前线程
然后根绝当前线程获取ThreadLocalMap对象 至于这个对象我们现在不起给出是干什么的 但我们直接就调用的get方法就进来了 应该是没有创建这个对象所以下面的if判断为空
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; }
首先会执行T value = initialValue();
protected T initialValue() { return null; }
所以这个value为null
下面的代码是不是在get方法中见过 我们知道map == null 所以执行else语句
我们在看一下这个createMap(t, value);方法
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
t 代表当前线程 firstValue就是我们上面得到的value 为null
而这里的this指的就是ThreadLocal的实例,也就是之前的local:
当我们执行完createMap方法以后就直接返回value 也就是一个null
所以我们就知道为什么get方法返回一个null了
那为什么第一个get不是null呢 因为他执行了set方法 那么我们来看一下set方法的源码
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
一看到这个源码应该很熟悉了吧 注意此时的value不是null 而是我们传进来的参数 a+10 因为a = 10 所以此处的参数为20 我们看一下 createMap(t, value)方法
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
是不是也很熟悉 此时的firstValue为20 this还是那个local对象 t还是当前线程
然后我们再回到get方法 注意此时我们已经创建了一个ThreadLocalMap对象
此时map不为空 执行map.getEntry()方法
首先我们来看一下这个ThreadLocalMap对象究竟是什么
经过我们上面的分析,我们知道ThreadLocal的设置值的方式是key-value的形式,也知道了这里的key其实就是ThreadLocal的实例,value就是要设置的值。
这里我们看下ThreadLocalMap,它其实是一个数据结构,就是用来存放我们的值的,而且它也是ThreadLocal的一个核心
ThreadLocalMap中存储的是Entry对象,Entry对象中存放的是key和value。
到这里,我们就知道了这个Entry是如何保存键值对的了,也知道Entry其实就是个弱引用。
我们回到get方法 此时就得到了设置的value值并且返回 也就是得到了20
5.ThreadLocal的内存泄露问题
首先我们要知道什么是内存泄露
说的简单点那就是因为操作不当或者一些错误导致没有能释放掉已经不再使用的内存,这就是内存泄漏,也就是说,有些内存已经不会再使用了,但是却没有给它释放掉,这就一直占用着内存空间,从而导致了内存泄漏。
我们知道,如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收 所以弱引用不会阻止GC,因为这个弱引用的机制,ThreadLocalMap的每个Entry都是一个对key的弱引用,同时每个Entry都包含了一个对value的强引用
正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了
但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下的调用链:Thread ---> ThreadLocalMap ---> Entry(key为null) ---> value
因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM
JDK已经考虑到了这个问题,所以在set,remove,rehash方法中会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以被回收
但是如果一个ThreadLocal不被使用,那么实际上set,remove,rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄漏
如何解决内存泄漏
调用remove方法,就会删除对应的Entry对象,可以避兔内存泄漏,所以使用完ThreadLocal之后,应该调用remove方法
记录学习笔记和感悟 以及面试八股文等