Java多线程9:ThreadLocal原理和使用场景

前言:面试官经常会问到 ThreadLocal 是什么?ThreadLocal为什么会发生内存泄漏?为什么数据库连接池使用ThreadLocal?他其实想考察候选人对ThreadLocal的定义、原理和应用场景的了解程度。

一、ThreadLocal简介

1.1、ThreadLocal定义

ThreadLocal,线程本地变量,顾名思义,它是每个线程私有的本地变量。通俗点讲,当你创建了一个ThreadLocal变量,每个线程在访问该变量时,都会拷贝一个副本至本地内存,所以多线程下操作ThreadLocal变量时,其实各自都是在操作自己拷贝的副本,互不影响,这样自然而然就避免了线程安全问题。

ThreadLocal有点类似于Map类型的数据变量。ThreadLocal类型的变量每个线程都有自己的一个副本,某个线程对这个变量的修改不会影响其他线程副本的值,可以说ThreadLocal为我们提供了一个保证线程安全的新思路。需要注意的是一个ThreadLocal变量,其中只能set一个值。一个ThreadLocal只能存储一个Object对象,如果需要存储多个Object对象那么就需要多个ThreadLocal!

看个例子,可能就明白了:

public class ThreadLocalTest {


    //定义一个ThreadLocal变量,泛型指定为Integer
    private ThreadLocal threadLocal = new ThreadLocal() {
        @Override
        protected Integer initialValue() {
            //初始值置为1
            return 1;
        }
    };


    @Test
    public void testThreadLocal() {
        //创建线程thread1
        MyThread thread1 = new MyThread("thread1");
        //启动线程thread1
        thread1.start();


        //创建线程thread2
        MyThread thread2 = new MyThread("thread2");
        //启动线程thread2
        thread2.start();
    }


    class MyThread extends Thread {


        MyThread(String name) {
            super(name);
        }


        @Override
        public void run() {
            super.run();
            //获取threadLocal的值,并输出
            System.out.println(getName() + "------ threadLocalValue is " + threadLocal.get());
            //threadLocal的值 + 1,并设置到threadLocal
            threadLocal.set(threadLocal.get() + 1);
            //再次获取threadLocal的值,并输出
            System.out.println(getName() + "------ after ++, threadLocalValue is " + threadLocal.get());
        }
    }
}

输出结果:

 thread1------ threadLocalValue is 1
 thread1------ after ++, threadLocalValue is 2
 thread2------ threadLocalValue is 1
 thread2------ after ++, threadLocalValue is 2

例子很简单,开启两个线程去操作ThreadLocal变量,从控制台的输出结果,便可以证明我们上面说的定义,每个线程对threadLocal变量的访问与操作互不影响,做到了线程隔离。

每个线程中都有一个 ThreadLocalMap数据结构,当执行set方法时,其值是保存在当前线程的 threadLocals变量中,当执行get方法中,是从当前线程的 threadLocals变量获取。 (ThreadLocalMap的key值是ThreadLocal类型)

1.2、ThreadLocal使用说明

ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
threadLocal.get();
threadLocal.set(1);
threadLocal.remove();

没错,这四行代码已经把ThreadLocal的使用方法表现得明明白白。

  • getThreadLocal拿出一个当前线程所拥有得对象

  • set给当前线程绑定一个对象

  • remove将当前线程绑定的当前对象移除

「记住在使用的以后,一定要remove,一定要remove,一定要remove」为什么要remove。因为ThreadLocal会导致内存泄漏问题。


二、ThreadLocal原理

如果只回答到怎么使用就结束了,显然配不上Java程序员内卷的时代,接下来手撕源码:

  • 先从Thread类看起

class Thread implements Runnable {
    ...//省略不相关代码
    //每个线程内部有个ThreadLocalMap的私有成员变量
    ThreadLocal.ThreadLocalMap threadLocals = null;
     ...//省略不相关代码
    private void exit() {
        ...
        //线程退出时, threadLocals变量置空 
        threadLocals = null;
        ...
    }  
}

整个Thread类中只需要关注两点就行,第一点,每个Thread都会有一个ThreadLocalMap类型的threadLocals变量,第二点就是这个threadLocals会在线程退出时置空。至于啥时候往里面放东西,先不用急,后面自然会提到

  • 看看ThreadLocal的内部类ThreadLocalMap吧


//ThreadLocalMap功能类似HashMap,内部维护了一个Entry数组进行存储数据
static class ThreadLocalMap {
        //Entry是K-V形式的实体,key为ThreadLcal变量,value为任意类型的变量
        static class Entry extends WeakReference<ThreadLocal> {
            //该value与ThreadLocal变量绑定
            Object value;


            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
        //内部维护的Entry数组
        private Entry[] table;


        //构造方法
        ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
            ... //省略不相关代码
            //初始化Entry数组
            table = new Entry[INITIAL_CAPACITY];
            ... //省略不相关代码
        }
}

ThreadLocalMap其实就是一个数据存储结构,类似HashMap。它内部也是维护了一个Entry实体数组,这个Entry是以K-V形式进行存储数据,key为ThreadLocal变量,value支持任意类型的变量,目前还不知道它具体存了什么,接着再往下看。

  • ThreadLocal本身终于要亮相了,看看ThreadLocal的核心方法setInitialValue / get / set 都做了啥

set(T value) 设置当前线程的线程局部变量的值

T get() 返回当前线程所对应的线程局部变量

void remove() 将当前线程局部变量的值删除

public class ThreadLocal{
   //设置初始值
   private T setInitialValue() {
         //通过我们重写initialValue方法获取初始值
        T value = initialValue();
         //获取到当前线程
        Thread t = Thread.currentThread();
         //通过getMap方法,传入当前线程作为参数去获取ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
         //如果获取到的ThreadLocalMap不为空,就直接存入,否则就创建ThreadLocalMap并存入。
        if (map != null)
            //key为threadLocal本身,value就是我们业务上需要存储的值
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
  //获取ThreadLocalMap对象
  ThreadLocalMap getMap(Thread t) {
        //重点!!!
        //这里返回的其实就是最上面讲到的每个Thread都会有一个ThreadLocalMap成员变量
        return t.threadLocals;
  }
  //获取ThreadLocal的value值
  public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //跟上面一样,拿取当前线程的ThreadLocalMap变量
        ThreadLocalMap map = getMap(t);
        //到ThreadLocalMap中查找数据并返回
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
  //设置ThreadLocal的value值
  public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //又是跟上面一样,拿取当前线程的ThreadLocalMap变量
        ThreadLocalMap map = getMap(t);
        //存储数据
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
}

从ThreadLocal的源码可以看到,三个核心方法的思路其实都是如出一辙的。通过Thread.currentThread()获取当前线程,因为在最早分析Thread类时,我们就提到每个Thread都有一个自己的ThreadLocalMap成员变量,所以拿到当前线程便可以轻松访问到当前线程的ThreadLocalMap对象,那样存数据、取数据等操作直接由ThreadLocalMap完成就行了。因为每个线程拿到ThreadLocalMap对象不同,所以存储的数据做到了线程隔离,这也是ThreadLocal最为重要的地方。

上面源码理解了,ThreadLocal的原理也就没问题了,最后再附上一张图总结一下:

图片

Thread类有属性变量threadLocals(类型是ThreadLocal.ThreadLocalMap) ,每个线程都有一个自己的ThreadLocalMap对象,所以每个线程往这个ThreadLocal中读写隔离的,并且是互相不会影响的。

ThreadLocalMap对象里维护了一个K-V格式的Entry数组,Key存的是ThreadLocal对象本身,value是需要存储的值。当我们要操作ThreadLocal变量时,会先获取当前的线程,根据当前线程拿到对应的ThreadLocalMap对象,进而操作ThreadLocalMap内部数组里存储的数据。

需要注意的是一个ThreadLocal变量,其中只能set一个值。一个ThreadLocal只能存储一个Object对象,如果需要存储多个Object对象那么就需要多个ThreadLocal!


三、ThreadLocal应用场景

说完ThreadLocal的原理,我们来看看ThreadLocal的使用场景。

3.1、 保存线程上下文信息,在任意需要的地方可以获取

比如我们在使用Spring MVC时,想要在Service层使用HttpServletRequest。一种方式就是在Controller层将这个变量传给Service层,但是这种写法不够优雅。Spring早就帮我们想到了这种情况,而且提供了现成的工具类:

public static final HttpServletRequest getRequest(){
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    return  request;
}

public static final HttpServletResponse getResponse(){
    HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
    return response;
}

上面的代码就是使用ThreadLocal实现变量在线程各处传递的。

3.2、 保证某些情况下的线程安全,提升性能

性能监控,如记录一下请求的处理时间,得到一些慢请求(如处理时间超过500毫秒),从而进行性能改进。这边我们以Spring MVC的拦截器功能为列子。


public class StopWatchHandlerInterceptor extends HandlerInterceptorAdapter {  
    //NamedThreadLocal是Spring对ThreadLocal的封装,原理一样
    //在多线程情况下,startTimeThreadLocal变量必须每个线程之间隔离
    private NamedThreadLocal<Long>  startTimeThreadLocal = new NamedThreadLocal<Long>("StopWatch-StartTime");  
    @Override  
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws Exception {  
        //1、开始时间
        long beginTime = System.currentTimeMillis();  
        //线程绑定变量(该数据只有当前请求的线程可见)  
        startTimeThreadLocal.set(beginTime);
        //继续流程  
        return true;
    }  
      
    @Override  
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,Object handler, Exception ex) throws Exception {  
        long endTime = System.currentTimeMillis();//2、结束时间  
        long beginTime = startTimeThreadLocal.get();//得到线程绑定的局部变量(开始时间)  
        long consumeTime = endTime - beginTime;//3、消耗的时间  
        if(consumeTime > 500) {//此处认为处理时间超过500毫秒的请求为慢请求  
        //TODO 记录到日志文件  
        System.out.println(String.format("%s consume %d millis", request.getRequestURI(), consumeTime));  
        }          
    }  
}  

说明:其实要实现上面的功能,完全可以不用ThreadLocal(同步锁等),但是上面的代码的确是说明ThreadLocal这个是用场景很好的列子。

3.3、ThreadLocal的最佳实践

从上面的图中可以看到,Entry的key指向ThreadLocal用虚线表示弱引用 ,下面我们来看看ThreadLocalMap:

Java对象的引用包括 : 强引用,软引用,弱引用,虚引用 。弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,该对象如果被弱引用关联,那么就会被回收。

当仅仅只有ThreadLocalMap中的Entry的key指向ThreadLocal的时候,ThreadLocal会进行回收的。ThreadLocal被垃圾回收后,在ThreadLocalMap里对应的Entry的键值会变成null,但是Entry是强引用,那么Entry里面存储的Object,并没有办法进行回收,所以ThreadLocalMap 存在内存泄露的风险。

所以最佳实践,应该在我们不使用的时候,主动调用remove方法进行清理。这里给出一个建议方案:


public class Dynamicxx {
    
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public void doSomething(){
        try {
             contextHolder.set("name");
            // 其它业务逻辑
        } finally {
            contextHolder .remove();
        }
    }

}

简单总结

  • 每个Thread对象内部都有一个ThreadLoacalMap的成员变量,这个变量类似一个Map类型,其中key为我们定义的ThreadLocal变量的this引用,value则为我们使用set方法设置的值;

  • 如果线程不消亡,在ThreadLocalMap中存放的ThreadLocal实例对象可能一直不会清除,所以当我们不需要在使用ThreadLocal的值时,就应该手动调用remove方法清除该值。


四、为什么数据库连接池使用ThreadLocal?

数据库连接池使用ThreadLocal:

一是为了把同一个数据库连接分享给同一线程的多个Dao操作保证事务的控制;

二是为了确保不同时间多个线程可能拿到的是同一个连接引起的事务安全问题,那么此时threadlocal闪亮登场,就算我拿的是“同一个连接”,在引入了threadlocal后,每个线程之间都会创建独立的连接副本,将collection各自copy一份,这样就互相不干扰了,可以保证事务的隔离性。

本人在学threadlocal的时候,网上大部分人都在说数据库连接池是典型的用了ThreadLoca的例子

  • 数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个。数据库连接池是缓存并托管数据库连接,主要是为了提高性能。

  • 而ThreadLocal缓存连接,是为了把同一个数据库连接“分享”给同一个线程的不同调用方法。(不管调用哪个方法,都是使用的同一个连接,方便进行“跨方法”的事务控制)

①保证事务的控制:

如果一个请求中涉及多个 DAO 操作,而如果这些DAO中的Connection都是独立的话,就没有办法完成一个事务。但是如果DAO 中的 Connection 是从 ThreadLocal 中获得的(意味着都是同一个对象), 那么这些 DAO 就会被纳入到同一个 Connection 之下。

 如jdbc事务的请求,part i 代表不同事务,如更新用户信息、更新订单信息等,需要保证各事务资源的一致性。
每一个事务请求连接时,先去ThreadlocalMap里找,如果不存在,到连接池中请求分配连接,并存入ThreadlocalMap。

②保证事务的隔离性:

假设不同时间的多个线程要从数据库连接池拿连接,那这个时候就可能拿到的是同一个连接了,那我多个线程线程拿到的是同一个连接,也就是说在多个线程在同一个事务之内,线程a执行了插入还没来得及提交,线程b此时来了个更新,在线程a还未操作完之前,线程b更新完了后,直接把连接给close了,线程a插了一半发现插不了了。。。此时肯定在想,这™是谁在搞我。

为了确保不同时间多个线程可能拿到的是同一个连接,那么此时threadlocal闪亮登场,就算我拿的是“同一个连接”,在引入了threadlocal后,每个线程之间都会创建独立的连接副本,将collection各自copy一份,这样就互相不干扰了。


五、ThreadLocal为什么会发生内存泄漏?

ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。但是如果滥用 ThreadLocal,就可能会导致内存泄漏。

下面,我们将围绕三个方面来分析 ThreadLocal 内存泄漏的问题

  • ThreadLocal 实现原理

  • ThreadLocal为什么会内存泄漏

  • ThreadLocal为什么使用弱引用

  • ThreadLocal 最佳实践

5.1、ThreadLocal 实现原理

ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal 实例本身,value 是真正需要存储的 Object。也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。一个ThreadLocal只能存储一个Object对象,如果需要存储多个Object对象那么就需要多个ThreadLocal!

5.2、ThreadLocal为什么会内存泄漏

试问一个问题:如果应用程序觉得ThreadLocal对象的使命完成,将ThreadLocal ref 设置为null,如果Entry中引用ThreadLocald对象的引用类型设置为强引用的话,会发生什么问题?

答案是:ThreadLocal对象会无法被垃圾回收器回收,因为从thread对象出发,有强引用指向ThreadLocal obj。此时会违背用户的初衷,造成所谓的内存泄露。

由于ThreadLocalMap中的key是指向ThreadLocal,故从设计角度来看,设计为弱引用,将不会干扰用户的释放ThreadLocal意图。

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

但是这些被动的预防措施并不能保证不会内存泄漏:

  • 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏(参考ThreadLocal 内存泄露的实例分析)。

  • 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

5.3、ThreadLocal为什么使用弱引用

从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?

我们先来看看官方文档的说法:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.为了应对非常大和长时间的用途,哈希表使用弱引用的 key。

下面我们分两种情况讨论:

  • key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

  • key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,就会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

5.4、ThreadLocal 最佳实践

综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?

  • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。


参考链接:

面试官问了又问:ThreadLocal是什么?

面试官:ThreadLocal为什么会发生内存泄漏?

ThreadLocal为啥要用弱引用?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java架构何哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值