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

ThreadLocal详解

作者头像
砖业洋__
发布2023-05-06 20:27:08
2170
发布2023-05-06 20:27:08
举报
文章被收录于专栏:博客迁移同步博客迁移同步

1. ThreadLocal介绍

1.1 ThreadLocal是什么?

定义:提供线程局部变量;一个线程局部变量在多个线程中,分别有独立的值(副本)。

1.2 ThreadLocal使用场景

典型场景1:每个线程需要一个独享的对象(通常是工具类,典工具类型需要使用的类有SimpleDateFormatRandom

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

我们来看看场景一的例子:

代码语言:javascript
复制
public class ThreadLocalTest {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) { // 新建了1000个SimpleDateFormat对象
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }
    public static String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return dateFormat.format(date);
    }
}

这里1000个线程即便使用了线程池,但是每个线程都会在执行过程中创建一个SimpleDateFormat对象,这比较耗费内存资源。

改进一:将SimpleDateFormat提出来用static修饰,这样每个线程都可以公用一个SimpleDateFormat对象,减少内存消耗,但是这样会打印出相同的时间,所有线程都在争夺这个资源,我们需要一个锁去控制,避免出现线程安全问题。

改进二:在改进一的基础上添加锁控制,代码如下:

代码语言:javascript
复制
public class ThreadLocalTest {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    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 = date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }
    public static String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        String s = null;
        synchronized (ThreadLocalTest.class) {
            s = dateFormat.format(date);
        }
        return s;
    }
}

这虽然能够满足要求,但是在高并发场景下,所有线程需要一个个的去获取锁,需要排队等待,这显然性能损耗太大。

改进三:使用ThreadLocal(不仅线程安全,而且也没有synchronized带来的性能问题,每个线程内有自己独享的SimpleDateFormat对象)

代码语言:javascript
复制
// 利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
public class ThreadLocalTest {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public static String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get(); // 拿到initialValue返回对象
        return dateFormat.format(date);
    }
}

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"));
}

像这种需要每个线程内独享的对象,一般使用场景是工具类中。后面再讲解原理,讲讲每个线程为什么都有独享的对象,这里先看用法。

我们来看看场景二的例子

需求:当前用户信息需要被线程内所有方法共享   当一个请求进来了,一个线程负责处理该请求,该请求会依次调用service-1(), service-2(), service-3(), service-4(),同时,每个service()都需要获得调用方用户user的信息,也就是需要拿到user对象。

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

  在此基础上可以演进,使用UserMap,就是每个用户的信息都存在一个Map中,当多线程同时工作时,我们需要保证线程安全,可以用synchronized也可以用ConcurrentHashMap,但这两者无论用什么,都会对性能有所影响。

  有没有更好的方法呢?ThreadLocal就来了

代码语言:javascript
复制
public class ThreadLocalTest {
    public static void main(String[] args) {
        new Service1().process("");
    }
}
class Service1 {
    public void process(String name) {
        User user = new User("张三");
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}
class Service2 {
    public void process() {
        User user = UserContextHolder.holder.get();
        ThreadSafeFormatter.dateFormatThreadLocal.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 UserContextHolder {
    public static ThreadLocal<User> holder = new ThreadLocal<>(); // 对比上一个例子,这里没有重写initialValue方法
}
class User {
    String name;
    public User(String name) {
        this.name = name;
    }
}

运行结果:

在这里插入图片描述
在这里插入图片描述

这样,不管哪个Service都能拿到User对象,能获取User对象内的所有信息。并且假如有多个请求,一个张三,一个李四,因为他们并没有直接共享User对象,所以他们之间不会有线程安全问题。

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

后面从源码再说说为什么这里ThreadLocal不会有线程安全问题。

2. ThreadLocal的作用和好处

2.1 ThreadLocal的两个作用

  • 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
  • 在任何方法中都可以轻松获取到该对象

2.2 ThreadLocal两种用法

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

  • 不受外界传参影响的时候,可以选择重写initialValue()方法来初始化保存对象,会在ThreadLocal第一次调用get()方法的时候初始化对象,对象的初始化时机可以由我们控制,比如上面第一个例子工具类。
  • 如果需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set直接放到我们的ThreadLocal中去,以便后续使用,对应代码就是上面第二个例子。

2.3 使用ThreadLocal带来的四个好处

  1. 达到线程安全
  2. 不需要加锁,提高执行效率
  3. 更高效地利用内存节省开销,上面例子中,相比于成千上万个任务,每个任务都新建一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销。
  4. 免去传参的繁琐,不需要每次都传同样的参数,ThreadLocal使得代码耦合度更低,更优雅

3. ThreadLocal主要方法介绍

主要是initialValuesetgetremove这几个方法,关于源码分析,将在第4节介绍

  • initialValue方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发。
  • 当线程第一次使用get方法访问变量时,将调用initialValue方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法。
  • 通常,每个线程最多调用一次initialValue()方法,但如果已经调用了一次remove()后,再调用get(),则可以再次调用initialValue(),相当于第一次调用get()
  • 如果不重写initialValue()方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。

4. ThreadLocal原理源码分析

4.1 Thread、ThreadLocal、ThreadLocalMap三者的关系

从图中可以看出,每个Thread对象都有一个ThreadLocalMap,每个ThreadLocalMap可以存储多个ThreadLocal

4.2 get方法

代码语言:javascript
复制
    public T get() {
        Thread t = Thread.currentThread();
        // 如果之前调用过set方法,那么这里getMap就不为null
        ThreadLocalMap map = getMap(t); // getMap就是看看当前线程有没有创建ThreadLocalMap集合,如果没有,这个集合就是为null
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                // 调用过set会从这里return
                return result;
            }
        }
        // 如果当前线程还没有创建ThreadLocalMap,执行setInitialValue方法
        return setInitialValue();
    }
    private T setInitialValue() {
        T value = initialValue();  // 调用你重写的initialValue方法,获取返回值
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        /*
			只有第一次使用get方法才调用initialValue方法的原因,第一次创建ThreadLocalMap
			第二次及以后,getMap发现ThreadLocalMap不是null,走不到这个方法来了。
			set存的key是什么?this是当前ThreadLocal对象!
		*/
        if (map != null) 
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

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

注意:这个map以及map中的keyvalue都是保存在线程中ThreadLocalMap的,而不是保存在ThreadLocal

getMap方法:获取到当前线程内的ThreadLocalMap对象 每个线程内都有ThreadLocalMap对象,名为threadLocals,初始值为null

4.3 set方法

因为set方法与setInitialValue方法很类似,这里分析一下set方法

代码语言:javascript
复制
	 // 把当前线程需要全局共享的value传入
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        // map对象为空就创建,不为空就覆盖
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

4.4 initialValue方法

这个方法没有默认实现,如果要用initialValue方法,需要自己实现,通常使用匿名内部类的方式实现(可以回顾上面代码)

4.5 remove方法

代码语言:javascript
复制
 // 删除对应这个线程的值
 public void remove() {
 	//  获取当前线程的ThreadLocalMap 
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
		// 移除这个ThreadLocal对应的值
         m.remove(this);
 }

4.6 ThreadLocalMap类

ThreadLocalMap类,也就是Thread.threadLocals

代码语言:javascript
复制
// 此行声明在Thread类中,创建ThreadLocalMap就是对Thread类的这个成员变量赋值
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap 类是每个线程Thread类里面的变量,但ThreadLocalMap这个静态内部类定义在ThreadLocal类中,其中发现这一行代码

代码语言:javascript
复制
private Entry[] table;

里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对:

  • 键:这个ThreadLocal
  • 值:实际需要的成员变量,比如User或者SimpleDateFormat对象

这个思路和HashMap一样,那么我们可以把它想象成HashMap来分析,但是实现上略有不同。

比如处理冲突方式不同,HashMap采用链地址法,而ThreadLocalMap采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链

通过源码分析可以看出,setInitialValue和直接set最后都是利用map.set()方法来设置值,最后都会对应到ThreadLocalMap的一个Entry

5. ThreadLocal需要注意的点

5.1 ThreadLocal内存泄漏问题

什么是内存泄漏? 某个对象不再有用,但是占用的内存却不能被回收

  • ThreadLocalMap中的Entry继承自 WeakReference,是弱引用
  • 弱引用:通过WeakReference类实现的,在GC的时候,不管内存空间足不足都会回收这个对象,适用于内存敏感的缓存,ThreadLocal中的key就用到了弱引用,有利于内存回收。
  • 强引用:我们平日里面的用到的new了一个对象就是强引用,例如 Object obj = new Object();JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不愿意回收具有强引用的存活着的对象。

ThreadLocal可能出现Value泄漏!

ThreadLocalMap 的每个 Entry 都是一个对key的弱引用,同时,每个 Entry 都包含了一个对value的强引用,如下:

代码语言:javascript
复制
static class Entry extends WeakReference<ThreadLocal<?>> {
 /** The value associated with this ThreadLocal. */
     Object value;
     Entry(ThreadLocal<?> k, Object v) {
         super(k); // key值给WeakReference处理
         value = v; // value直接用变量保存,是强引用
     }
 }

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

Thread---->ThreadLocalMap---->Entry(keynull,弱引用被回收)---->value

因为valueThread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM

JDK已经考虑到了这个问题,所以在set, remove, rehash方法中会扫描keynullEntry,并把对应的value设置为null,这样value对象就可以被回收 比如rehash里面调用resize

代码语言:javascript
复制
 private void resize() {
           ......省略代码
          ThreadLocal<?> k = e.get();
           if (k == null) {
               e.value = null; // Help the GC
           } 
           ......

如果key回收了,那么value也设置为null,断开强引用链路,便于垃圾回收。

但是如果一个ThreadLocal不被使用,那么实际上set, remove, rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄漏

5.2 ThreadLocal如何避免内存泄漏?

  及时调用remove方法,就会删除对应的Entry对象,可以避免内存remove泄漏,所以使用完ThreadLocal之后,应该调用remove方法。 比如拦截器获取到用户信息,用户信息存在ThreadLocalMap中,线程请求结束之前拦住它,并用remove清除User对象,这样就能稳妥的保证不会内存泄漏。

5.3 共享对象问题

  如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。

5.4 不要强行使用ThreadLocal

  如果可以不使用ThreadLocal就能解决问题,那么不要强行使用,在任务数很少的时候,可以通过在局部变量中新建对象解决。

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

  在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄漏。

6. 在Spring中实例中哪里用到了ThreadLocal?

  • DateTimeContextHolder类,应用了ThreadLocal
  • ThreadLocal的典型应用场景:每次HTTP请求都对应一个线程,线程之间相互隔离
  • RequestContextHolder,也是用到了ThreadLocal,看NamedThreadLocal源码,再看getRequestAttributes的调用
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-03-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

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

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. ThreadLocal介绍
    • 1.1 ThreadLocal是什么?
      • 1.2 ThreadLocal使用场景
      • 2. ThreadLocal的作用和好处
        • 2.1 ThreadLocal的两个作用
          • 2.2 ThreadLocal两种用法
            • 2.3 使用ThreadLocal带来的四个好处
            • 3. ThreadLocal主要方法介绍
            • 4. ThreadLocal原理源码分析
              • 4.1 Thread、ThreadLocal、ThreadLocalMap三者的关系
                • 4.2 get方法
                  • 4.3 set方法
                    • 4.4 initialValue方法
                      • 4.5 remove方法
                        • 4.6 ThreadLocalMap类
                        • 5. ThreadLocal需要注意的点
                          • 5.1 ThreadLocal内存泄漏问题
                            • 5.2 ThreadLocal如何避免内存泄漏?
                              • 5.3 共享对象问题
                                • 5.4 不要强行使用ThreadLocal
                                  • 5.5 优先使用框架的支持,而不是自己创造
                                  • 6. 在Spring中实例中哪里用到了ThreadLocal?
                                  领券
                                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档