ThreadLocal

    科技2026-06-21  5

    文章目录

    前言一、ThreadLocal案例案例一:使用线程池打印日期信息案例二:全局变量两种场景总结 二、ThreadLocal原理1.结构2.方法2.1 initialValue()初始化2.2 get()2.3 set()2.4 remove() 3.ThreadLocalMap==4.内存泄漏== 总结


    前言

    本文主要介绍ThreadLocal,包括它的底层结构、源码以及注意事项(内存泄漏问题)

    一、ThreadLocal案例

    案例一:使用线程池打印日期信息

    public class ThreadLocalNormalUsage03 { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); //利用SimpleDateFormat对象,返回日期信息 public String date(int seconds) { //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时 Date date = new Date(1000 * seconds); return dateFormat.format(date); } public static ExecutorService threadPool = Executors.newFixedThreadPool(10); public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000; i++) { int finalI = i; threadPool.submit(new Runnable() { @Override public void run() { String date = new ThreadLocalNormalUsage03().date(10 + finalI); //打印日期信息(每个线程打印的都不一样) System.out.println(date); } }); } threadPool.shutdown(); } }

    最后的结果可以发现,会出现相同的值,这说明发生了线程安全问题。因为所有线程公用了一个SimpleDateFormat对象,当发生安全问题时,导致拿到的值相同 这时候可以考虑加锁:

    synchronized (ThreadLocalNormalUsage04.class) { s = dateFormat.format(date); }

    然而加锁的效率太低。

    这时候就可以考虑使用ThreadLocal,利用ThreadLocal,给每个线程分配自己的dateFormat对象,既保证线程安全,又高效利用内存

    public class ThreadLocalNormalUsage05 { public String date(int seconds) { //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时 Date date = new Date(1000 * seconds); //通过threadLocal的get方法获取到SimpleDateFormat对象 SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get(); return dateFormat.format(date); } public static ExecutorService threadPool = Executors.newFixedThreadPool(10); public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000; i++) { int finalI = i; threadPool.submit(new Runnable() { @Override public void run() { String date = new ThreadLocalNormalUsage05().date(10 + finalI); System.out.println(date); } }); } threadPool.shutdown(); } } class ThreadSafeFormatter { public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() { @Override //初始化方法一,重写initialValue 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")); }

    案例二:全局变量

    public class ThreadLocalNormalUsage06 { public static void main(String[] args) { new Service().process(); } } class Service { public void process() { User user = new User("张三"); UserContextHolder.holder.set(user); new Service2().process(); } } class Service2 { public void process() { User user = UserContextHolder.holder.get(); System.out.println("Service2拿到用户:" + user.name); new Service3().process(); } } class Service3 { public void process() { User user = UserContextHolder.holder.get(); System.out.println("Service3拿到用户:" + user.name); UserContextHolder.holder.remove(); //确保不会出现内存泄漏 } } class User { String name; public User(String name) { this.name = name; } } class UserContextHolder { public static ThreadLocal<User> holder = new ThreadLocal<>(); }

    通过ThreadLocal保存全局变量,可以避免传参的麻烦

    两种场景总结

    二、ThreadLocal原理

    1.结构

    每个线程存在一个ThreadLocalMap,每个Map里面包含多个ThreadLocal,而每个ThreadLocal都是以K-V形式存储的

    2.方法

    2.1 initialValue()初始化

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

    当线程第一次使用get方法访问变量时,将调用此方法,除非线程先调用了set方法,在这种情况下,不会为线程调用本initialValue方法。

    通常每个线程最多调用一次此方法,但如果已经调用了remove()后,在调用get(),则可以再次调用此方法。(remove之后,map变为null)

    如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。

    //初始化方法一,重写initialValue public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); } };

    2.2 get()

    拿到当前线程对象获取map如果存在,就去获取entry,即K-V拿到entry后如果不为null,就可以拿到value

    2.3 set()

    public static ThreadLocal<User> holder = new ThreadLocal<>(); holder.set(user); 拿到map向map里面存K-V

    2.4 remove()

    删除掉当前的entry

    3.ThreadLocalMap

    threadLocalMap类似于是一个HashMap它在发生冲突的时候采用的是线性探测法,就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链

    4.内存泄漏

    当某个对象不再有用,但是占用的内存却不能被回收。

    ThreadLocal就存在内存泄漏的问题 在上面的ThreadLocalMap里面,最重要的就是Entry对象,它包含key-value,可以看到整个Entry对象继承WeakReference是弱引用(当某个对象只被弱引用关联,它就可以被回收),然而,value=v属于强引用

    正常情况下,当线程停止,它里面的value就会被垃圾回收,那么就不存在强引用了但是当线程不终止,那么key就不能被回收,就存在 Thread——>ThreadLocalMap——>Entry(key为null)——>Value

    JDK已经考虑到了这样的问题,在set,remove,rehash这些方法中会扫描key为null的Entry,并把value设置为null,这样就可以被回收 但是,若这些方法都没有被调用,线程又不停止,那么就会出现内存泄漏 在使用完ThreadLocal之后(业务逻辑中不需要再使用),主动调用remove()方法,避免内存泄漏。


    总结

    复习重点:initialize()、get()、内存泄漏

    Processed: 0.009, SQL: 10