ThreadLocal介绍和使用

本文详细介绍了ThreadLocal的两种典型应用场景,包括为每个线程提供独享的对象实例以确保线程安全,以及在多线程环境中共享全局变量。通过实例分析了ThreadLocal如何解决SimpleDateFormat的线程安全问题,并对比了加锁和使用ThreadLocal的优劣。此外,文章还探讨了ThreadLocal的内存泄漏问题及其避免方法,以及其在并发编程中的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一、概述 

1.1 介绍

1.2 ThreadLocal的两种典型应用场景

二、第一种应用场景

2.1 第一种典型场景

2.2 SimpleDateFormat的进化之路

2.3 利用 ThreadLocal 完美解决问题

三、第二种应用场景

3.1 暂无

3.2 第二种典型场景 part1

3.3 第二种典型场景 part2

四、ThreadLocal 的作用和主要方法

4.1 ThreadLocal 的两个作用和好处

4.2 暂无

4.3 ThreadLocal 的主要方法

五、 ThreadLocal的原理和注意点

5.1 图解 ThreadLocal 原理

5.2 暂无

5.3 ThreadLocal 的注意点

六、 课程总结

6.1 ThreadLocal 总结


一、概述 

1.1 介绍

        1. 两大使用场景——ThreadLocal的用途

        2. 使用ThreadLocal带来的好处

        3. 主要方法介绍

        4. 原理、源码分析

        5. 注意点

1.2 ThreadLocal的两种典型应用场景

典型场景1:

        每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有 SimpleDateFormat 和 Random )。

        工具类通常是线程不安全的。多个线程共享一个静态工具类的话是有很大风险的、或者说肯定会出错的。这就需要我们使用 ThreadLocal 给每个线程都制作一个独享的对象。每个线程拥有工具类不同的实例,相互之间不影响。

典型使用场景2:

        每个线程内需要保存全局变量(例如:在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦

二、第一种应用场景

2.1 第一种典型场景

        每个 Thread 内有自己的实例副本,不共享,其他线程访问不到。

        比喻:教材只有一本,一起做笔记有线程安全问题。并发的读写会导致数据不一致。复印后没问题。

SimpleDateFormat的进化之路:

        2个线程分别用自己的 SimpleDateFormat 。结果暂时没问题。

2.2 SimpleDateFormat的进化之路

        延伸出10个,那就有10个线程和10个 SimpleDateFormat。写法不优雅。

        但是当需求变成了1000个,那么必然要用线程池(否则消耗内存太多)。却衍生出一个问题,newFixedThreadPool(10) 10个线程对应1000个 SimpleDateFormat实例。

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    threadPool.submit(new Runnable() {
        @Override
        public void run() {
            String date = new ThreadLocalNormalUsage04().date(finalI);
            System.out.println(date);
        }
    });
    threadPool.shutdown();

        把SimpleDateFormat 实例设为 static,所有的线程都共用同一个 SimpleDateFormat实例,会出现线程安全问题(并发安全问题)。


    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static void main(String[] args) throws InterruptedException {
        // 代码体
    }

        加锁呢?加锁来解决线程安全问题。类锁。用了 synchronized 之后,加锁,保证同一时刻不可能超过一个线程来运行这段代码。这样的话,1000个线程要一个个排队了,会有性能问题。高并发情况下,一个个排队不符合我们的预期。

 public String date(int seconds) {
    //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
    Date date = new Date(1000 * seconds);
    String s = null;
    synchronized (ThreadLocalNormalUsage04.class) {
        s = dateFormat.format(date);
    }
    return s;
}

2.3 利用 ThreadLocal 完美解决问题

        更好的解决方案是使用 ThreadLocal。利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存。

SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
class ThreadSafeFormatter {
    // 常规写法
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
    // lambda表达式写法
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
            .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

三、第二种应用场景

3.1 暂无

3.2 第二种典型场景 part1

实例:当前用户信息需要被线程内所有方法共享

        一个比较繁琐的解决方案是把 user 作为参数层层传递,从 service-1() 传到 service-2() ,再从 service-2() 传到 service-3(),以此类推,这样做会导致代码冗余且不易维护。参数容易传错,每次都要对参数进行空判断。

         希望:每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦。

        不适合用全局变量( static )来保存。

        用 User Map:当多线程同时工作时,我们需要保证线程安全,可以用 synchronized,也可以用 ConcurrentHashMap,但无论用什么,都会对性能有所影响。

        更好的办法是使用 ThreadLocal,这样无需 synchronized,可以在不影响性能的情况下,无需层层传递参数,就可以达到保存当前线程对应的用户信息目的。

方法: 

        用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、userID等)。

        这些信息在同一个线程内相同,不同的线程使用的业务内容是不相同的。

        在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(例如 user 对象)作为参数传递的麻烦

        强调的是同一个请求内(同一个线程内)不同方法间的共享

        不需要重写 initialValue() 方法,但是必须手动调用 set() 方法
       

3.3 第二种典型场景 part2

UserContextHolder.holder.set(user);
User user = UserContextHolder.holder.get();
UserContextHolder.holder.remove();

class UserContextHolder {
    public static ThreadLocal<User> holder = new ThreadLocal<>();

}

四、ThreadLocal 的作用和主要方法

4.1 ThreadLocal 的两个作用和好处

        1. 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象) 

        2. 在任何方法中都可以轻松获取到该对象

根据共享对象的生成时机不同,选择 initialValue 或 set 来保存对象

场景一:initialValue

        在 ThreadLocal 第一次 get 的时候把对象给初始化出来,对象的初始化时机可以由我们控制。

场景二:set

        如果需要保存到 ThreadLocal 里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息。

        用 ThreadLocal.set 直接放到我们的 ThreadLocal 中去,以便后续使用。

使用 ThreadLocal 带来的好处

        达到线程安全

        不需要加锁,提高执行效率

        更高效的利用内存、节省开销

        免去传参的繁琐—— ThreadLocal 使得代码耦合度更低,更优雅

4.2 暂无

4.3 ThreadLocal 的主要方法

T initialValue(): 初始化

        该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发。

        当线程第一次使用get方法访问变量时,将调用此方法。这正对应了 ThreadLocal 的两种典型用法。

        每个线程最多调用一次此方法,但如果已经调用了 remove() 后,再调用get(),则可以再次调用此方法。

        如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写 initialValue() 方法。

void set(T t): 为这个线程设置一个新值

T get(): 得到这个线程对应的 value。如果是首次调用 get(),则会调用 initialize 来得到这个值。

void remove(): 删除对应这个线程的值

五、 ThreadLocal的原理和注意点

5.1 图解 ThreadLocal 原理

get 方法: 

        get方法是先取出当前线程的 ThreadLocalMap。然后调用 map.getEntry 方法,把本 ThreadLocal 的引用作为参数传入。取出map中属于本 ThreadLocal 的value。

        注意:这个map以及map中的key和value都是保存在线程中的,而不是保存在 ThreadLocal 中。 

set 方法( setInitialValue 方法很类似):

initialValue方法:

        没有默认实现、如果要用initialValue方法 需要自己实现、通常使用匿名内部类的方式实现。

remove方法:

        只会从 ThreadLocalMap 中,删掉本 ThreadLocal 的value​​​​​​​

ThreadLocalMap 类:

        ThreadLocalMap类,也就是Thread.threadLocals。ThreadLocalMap 类是每个线程 Thread 类里面的变量,里面最重要的是一个键值对数组 Entry[] table,可以认为是一个map,键值对:键:这个 ThreadLocal;值:实际需要的成员变量,比如 user 或者 simpleDateFormat 对象。

        ThreadLocalMap 这里采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链。

两种使用场景殊途同归:

        通过源码分析可以看出,setInitialValue 和直接set 最后都是利用 map.set() 方法来设置值。也就是说,最后都会对应到 ThreadLocalMap 的一个 Entry,只不过是起点和入口不一样。

5.2 暂无

5.3 ThreadLocal 的注意点

内存泄漏:

        键是弱引用方式。弱引用特点:如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收。

        值是强引用,会导致内存泄漏。

        正常情况下,当线程终止,保存在 ThreadLocal 里的 value 会被垃圾回收,因为没有任何强引用了。但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下的调用链:

 JDK 解决方式:

        ThreadLocal 的 set remove reHash方法中会扫描key为null的Entry,并把对应的value设置为null。Help the GC。

如何避免内存泄漏(阿里规约)

        调用 remove 方法,就会删除对应的 Entry 对象,可以避免内存泄漏,所以使用完 ThreadLocal 之后,应该调用 remove 方法。

        在实际开发中,如果我们是用拦截器的方法获取到用户信息,那么同样应该用拦截器的方法 在这个线程请求退出之前,拦住他,并且把这里面所保存的 user 对象清除掉。这样一来才能非常稳妥的保证内存泄漏不发生。

空指针异常:

        long(基本类型) 和 Long(包装类型) 用法区别,涉及装箱拆箱。

共享对象

如果可以不使用 ThreadLocal 就解决问题,那么不要强行使用

优先使用框架的支持,而不是自己创造

六、 课程总结

6.1 ThreadLocal 总结

        1. 两大使用场景—— ThreadLocal 的用途

        2. 使用 ThreadLocal 带来的好处

        3. 主要方法介绍

        4. 原理、源码分析

        5. 注意点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

chengbo_eva

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

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

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

打赏作者

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

抵扣说明:

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

余额充值