前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ThreadLocal之美!

ThreadLocal之美!

原创
作者头像
Joseph_青椒
修改2023-08-08 20:12:23
2030
修改2023-08-08 20:12:23
举报
文章被收录于专栏:java_josephjava_joseph

来吃透threadlocal!

ThreadLocal使用

使用场景

1:需要独享对象的非线程安全的对象

常用于例如SimpleDateFormatter这样非线程安全的工具类上,比如需要1000次用到这个工具类,想要不频繁的创建导致的开销,以及高效的避免线程安全问题,就可以用

代码语言:javascript
复制
​
/**
 * @Author:Joseph
 * @bolg:https://li-huancheng.gitee.io/
 * @Package:threadLocal
 * @Project:bing-fa-demo
 * @name:ThreadLocalDateTest
 * @Date:2023-07-21 15:36
 * @Filename:ThreadLocalDateTest
 */
public class ThreadLocalDateTest {
​
    public static ExecutorService theadPool = Executors.newFixedThreadPool(10);
​
    public static void main(String[] args) {
​
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            theadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalDateTest().date(finalI);
                    System.out.println(date);
                }
            });
        }
​
​
    }
    public String date(int seconds){
​
        //从1970:1。1.0.0 00:00:00    GMT计sh'j
        // 因为Data是毫秒单位,标识秒,得乘1000
        Date date = new Date(1000*seconds);
        //格式化
//        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd:hh:mm: ss");
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return dateFormat.format(date);
​
    }
​
}
class ThreadSafeFormatter{
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd:hh:mm: ss");
        }
    };
​
}

例如这个场景,如果不用ThreadLocal,避免多次创建对象开销,那么只能通过synchronized加锁来保证线程安全,但是对应的就是重量级锁的效率问题,那么通过ThreadLocal就能很轻松的完成这个需求,且效率是很快的

2:线程内需要保存全局变量来避免传参麻烦

对于用户信息的传递,比如在interceptor层中,想要传递用户信息 ,首先直接用static是不行的,因为对于不同的线程,都有自己不同的信息,那么可以用一个线程安全的map,比如currentHashMap.让不同的线程存入,用到的时候再去取,即使currentHashMap效率算高了,但是他也是通过AQS cas这些操作来保障的,肯定是影响性能的,那么就可以用ThreadLocal,拦截解密token之后,放到theadLocal,用的时候取就可以了!。

代码语言:javascript
复制
Slf4j
public class LoginInterceptor implements HandlerInterceptor {
​
    public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();
​
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
​
        String accessToken = request.getHeader("token");
        if(accessToken == null ){
            accessToken = request.getParameter("token");
​
        }
​
        if(StringUtils.isNotBlank(accessToken)){
            //不为空
            Claims claims = JWTUtil.checkJWT(accessToken);
            if(claims == null){
                 //未登录
                CommonUtil.sendJsonMessage(response, JsonData.buildResult(BizCodeEnum.ACCOUNT_UNLOGIN));
                return false;
​
            }
            long userId= Long.valueOf(claims.get("id").toString());
            String headImg = (String)claims.get("head_img");
            String name = (String)claims.get("name");
            String mail = (String) claims.get("mail");
​
            LoginUser loginUser = LoginUser.builder()
                    .headImg(headImg)
                    .name(name)
                    .id(userId)
                    .mail(mail).build();
​
​
            //通过attribute传递用户信息
            //request.setAttribute("loginUser",loginUser);
​
            //通过threadlocal传递用户登录信息
            threadLocal.set(loginUser);
​
            return true;
​
​
​
        }
        CommonUtil.sendJsonMessage(response,JsonData.buildResult(BizCodeEnum.ACCOUNT_UNLOGIN));
​
        return false;
    }
​
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
​
    }
​
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        threadLocal.remove();
    }
}
​

这里可以看到使用了,很简单,拦截到set进去就行了,但是注意afterCompletion中的remove(),埋个伏笔,

需要使用的时候

image-20230721165651299
image-20230721165651299

拿出来就好了

这里总结一下ThreadLocal的作用

第一个就是让对象在线程间隔离,每个线程都可以持有自己独立的对象。简单的说,就是同个对象,在不同线程之间是不一样的,并没有重复的创建与销毁,只是创建了线程的数量而已,比如上面1000次,10个线程,那么仅仅创建了10个,对应的就是场景一,对于非线程安全的对象,要多次使用,就可以用theadLocal

第二个就是对于不同的线程(用户),都可以轻松的获取对象,来达到避免传参麻烦

注意事项

有没有发现,场景一,只用工具类的时候,用的initialValue而场景二用的set方法,原因在于,场景一创建的时间是我们可控的,代码运行到这里,进行初始化就可以创建好,就用InitialValue,set的话是不可控的,加载完也是空的,拦截器拦截到,再set进去,所以就会用set,这就是threadLocal使用的一些注意事项。

使用ThreadLocal的好处

1,线程安全

2.不用加锁,效率高

3更好的利用内存,节省开销,比如1000次那个场景,10个线程,对应只消耗10次对象的创建,不同的区分发生在内存的执行上,内存本身就要消耗。

4,不用层层传参

原理、源码

使用会了,现在搞源码!!

理清threadLocal,Thread、threadLocalMap三者的关系

我们知道,这个threadLocal的创建是以线程为单位的,每个线程持有一个对象,但是为啥要有threadLocalMap呢?

image-20230723083106677
image-20230723083106677

这是因为一个程序中,不一定只有一个threadLocal,我们再使用的时候,有可能用了一个拦截器的set,也用了一个工具类,就是使用中的两种情况,这就需要两个threadlocal

image-20230723083700926
image-20230723083700926

这是Thread中的,thread持有thredLoalMap,并命名为threadLocals对象

重要方法

第一个就是initalValue了,这个方法提供初始化,但是是延迟加载的,在get的时候才会触发

现在我们看一下这个方法,进入ThreadLocal类中

image-20230723084107023
image-20230723084107023

可以看到这个是return null,的 我们重写,才会有值

延迟加载,看一下get方法

image-20230723084214597
image-20230723084214597
image-20230723084241426
image-20230723084241426

这里的value就是我们重写的initialValue,不重写是null

当场景2,就是用set的时候,这个initialValue是不会取执行的

image-20230723084604657
image-20230723084604657

retrun的是我们set近ThreadLocalMap中的值

initaialValue方法还需要我们注意的是,只会在第一次调用get会触发,后面就不会触发了,会和set一样,进入if(map!=null)的逻辑,值得注意的是,当我们remove之后,threadLoalMap又空了,就可以触发了,

set方法

这个就是为线程设置一个新的值,

这个就比较简单了,拿到当前线程,再取找thradLocalMap找不到就去创建,找到就set进去

值得注意的是:threadLocal类中只是一些操作的方法,具体的保存,都是再threadLocalMap中的,也就是说,都是保存再线程中的,threadLocalMap中,key是threadLocal,value是set进去的值!!

get

就是拿到线程对应的value,threadLocalMap是以key-value为键值对的集合,这里指的就是对应threadLocal中的value

代码是这样的

image-20230723090424241
image-20230723090424241

就是从拿到threadLocalMap中存储我这个threadLocal的value

这里的map是thread类中的threadLocalmap,map中通过threadLocal作为key,放进去找到对应的value

remove方法就是移除value

这个就很简单了

image-20230723091449832
image-20230723091449832

都是一个套路。就是通过当前线程,拿到threadLocalMap,然后讲threadLocal作为key去remove数据

注意这个remove的key是当前的threadLocaL,并不是所有的threadLocal的value

threadLoalMap

这里有必要着重分析一下这个类,是整个threadLocal实现的核心

刚才我讲的,多多少少提到了

threadLocalMap指的就是thread类中的threadLocals,类型是ThreadLocal.thredLocalMap,也就是说,这个threadLoaclMap定义是在threadLocal中的,但是threadLocalMap和threadLocal使用是被thread类持有的,thread中持有threadLocalMap,threadLocalMap的key是threadLocal,

这个关系比较混乱,我们可以把它理解为一个hashMap但是略有区别

它发生hash冲突不是用拉链法,而是用线性探测法,冲突了,就在table中遍历往下找

通过源码分析

我们可以理解,initialValue和set都是通过map.set放value的,只不过initialVlaue起点是get方法,map为空,调用initial方法的罢了

内存泄漏

很多人都不容为什么拦截器的最后,要remove一下,Jvm垃圾回收器不是可以回收吗?

这里先理清一下内存泄漏和内存溢出的区别

内存泄漏指的是:对象不使用了,应该被回收,但是没被回收

内存溢出指的是:就是申请的内存不够用,造成outofMemory

再讲一下:四大引用

强软弱虚

强引用,Reference

这个是很普遍的,

比如String s=''ss'',在内存不足的时候,jvm宁愿抛出内存溢出oom,也不会回收这个对象,只有在这个jvm进程结束才会回收

软引用:Software

这里就是再降低一些要求,内存资源够的时候,触发GC也不会回收,但是内存不够了,就会回收,指的就是有用,但不是非要不可的地步

弱引用:WeakReference

用完就没用了的东西,用过了,gc的时候一定会被回收

虚引用就用的很少了,目的是让系统知道这个对象被回收

  • 总结

引用类型

被垃圾回收时刻

用途

生存时间

强引用

从来不会

对象的一般状态

JVM停止运行时终止

软引用

在内存不足时

对象简单,缓存,文件缓存,图片缓存

内存不足时终止

弱引用

在gc垃圾回收时

对象简单,缓存,文件缓存,图片缓存

gc运行后终止

虚引用

任何时候都可能被垃圾回收器回收

基本不写,虚拟机使用, 用来跟踪对象被垃圾回收器回收的活动

未知

我们看一下threadLocalMap

image-20230723101457846
image-20230723101457846

注意这里k的赋值,并不是直接赋值,而是super了一个key,也就是虚引用,这个再发生垃圾回收是可以回收的,

但是这个value是强引用

我们知道,线程执行完就会消失,那么

Thread->ThreadLocalMap->Entry(key=null)->value

这个调用链,发生gc之后,key消失了,但是value还在,当线程执行完毕后,value会随着Threa生命的终止,也会丢失调用链路,可达性分析算法,大家可以了解,这样也是可以回收的

但是当我们用线程池的时候,一个线程是重复利用的,比如拦截器这里,我这个线程处理完一个用户,再处理下一个,那么这里的threadLocal和value是会很多的,value强引用,是不会被回收的,因为线程池服用线程,进程不会结束,

jdk考虑到了这个问题

remove\resize\set方法,会扫描为null的,并设置为null

image-20230723102220448
image-20230723102220448

比如这里,k为null,就会让value=null,注释也写了,help the gc,

但是当一个threadLocal不再被使用,那么这些方法也无法调用,

所有就需要我们再threadLocal结束前,要去提前remove

image-20230723102417784
image-20230723102417784

现在知道这个伏笔了吧!

内存泄漏是threadLocal的一个坑,同样还有

空指针异常

可能听过这样一句话,threadLocal用的时候要set,不然get的时候会报空指针异常 这个问题,简直不要太离谱。我们知道,initialValue方法,即使不调用,也是会有一个默认的null,的,顶多get到一个null,那么为什么有人会遇到get下,报空指针异常问题呢?这下,你就得听我好好唠唠了,不然真开发中遇到,排错很麻烦的

代码语言:javascript
复制
**
 * @Author:Joseph
 * @bolg:https://li-huancheng.gitee.io/
 * @Package:threadLocal
 * @Project:bing-fa-demo
 * @name:ThreadLoaclNullPointer
 * @Date:2023-07-23 11:59
 * @Filename:ThreadLoaclNullPointer
 */
public class ThreadLoaclNullPointer {
​
    ThreadLocal<Long> longThreadLocal= new ThreadLocal<Long>();
    public void set(){
        longThreadLocal.set(Thread.currentThread().getId());
    }
    public long get(){
        return longThreadLocal.get();
    }
​
    public static void main(String[] args) {
        ThreadLoaclNullPointer threadLoaclNullPointer = new ThreadLoaclNullPointer();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(threadLoaclNullPointer.get());
            }
        });
        thread.start();
    }
}
​
image-20230723142201878
image-20230723142201878

没错,报错了!

你能发现那里的错误吗,先别看答案,自己copy代码debug一下再看答案,

答案就是

自动拆箱,出现了空指针异常,拆箱的时候,null是无法拆箱的

代码语言:javascript
复制
//修改为Long
public Long get(){
        return longThreadLocal.get();
    }
image-20230723142446416
image-20230723142446416

这样就正常了,这是我们需要注意的地方

错误样例

threadLocal使用,还有注意点

static对象,当我们set的时候,还是共享的,因为static本身类变量就是共享的,我们想用直接类.static对象就可以了,不需要放到threadLocal中

另外没必要用theadLocal就不要用了,比如在场景一中,只需要用两三次的工具类,直接new三次对象就行了

Spring中的应用

spring中用到threadLocal也是很多的,XXXContextHolder

比如RequestContextHolder

image-20230723143449243
image-20230723143449243

进入

image-20230723143507452
image-20230723143507452

只是对threadLocal做了层小小的包装,取名字而已

image-20230723143621027
image-20230723143621027

比如这个方法,就是把请求参数返回,也是从threadLocal中去取

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ThreadLocal使用
    • 使用场景
      • 注意事项
        • 使用ThreadLocal的好处
        • 原理、源码
          • 理清threadLocal,Thread、threadLocalMap三者的关系
            • 重要方法
              • threadLoalMap
              • 内存泄漏
              • 空指针异常
              • 错误样例
              • Spring中的应用
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档