ThreadLocal详解

1. ThreadLocal简介

1.1 ThreadLocal是什么?

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

ThreadLocal又叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就说该变量是当前线程独有的变量。ThreadLocal为变量在每一个线程中都创建了一个副本。那么每个线程都可以访问自己内部的副本变量。

同一个ThreadLocal所包含的对象,在不同的Threa中有不同的副本,这里有几点需要注意:

  • 因为每个Thread内有自己的实例副本,且该副本只能由当前Thread使用,这也是ThreadLocal命名的由来。
  • 既然每个Thread都有自己的实例副本,且其他Thread不可访问,那就不存在多线程共享的问题

ThreadLocal提供了线程本地的实例,它与普通变量的区别在于:

  • 每个使用该变量的线程都会初始化一个完全独立的实例副本。
  • ThreadLocal变量通常被private static修饰。
  • 当一个线程结束时,它所使用的所有ThreadLocal相对的实例副本都可被回收。

总的来说,ThreadLocal适用于每个线程需要自己独立的实例,且该实例需要在多个方法中被使用。即变量在线程间隔离,但是在方法和类间共享。

ThreadLocal在使用过程中状态
在这里插入图片描述
从图中可以看出,每个Thread对象都有一个ThreadLocalMap,每个ThreadLocalMap可以存储多个ThreadLocal

1. 2 ThreadLocal常见使用场景

如上文所述,ThreadLocal适用于以下两种场景:

  1. 每个线程需要有自己单独的实例。
  2. 实例需要在多个方法中共享,但不希望被多线程共享。

对于第一点,每个线程拥有自己的实例,实现它的方式有很多,例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。
对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现,ThreadLocal 使得代码耦合度更低,且实现更优雅。

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

public class ThreadLocalDemo {
    //创建一个核心线程为10的定长线程池
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    //格式化时间
    public static String date(int seconds){
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date=new Date(seconds*1000);
         //日期格式
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return  sdf.format(date);
    }

    public static void main(String[] args) {
        //启动1000个线程,新建1000个SimpleDateFormat对象
        for (int i = 0; i <1000 ; i++) {
            int finalI = i;
            threadPool.submit(()->{
                String date = date(finalI);
                System.out.println(date);
            });
        }
        //关闭线程池
        threadPool.shutdown();
    }
}

这里虽然使用了线程池,但是1000个线程在执行过程中都创建一个SimpleDateFormat对象,这比较耗费内存资源。那么如何改进呢?

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

	 //日期格式
    static  SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

在这里插入图片描述
改进二:上图可以看到出现了两个相同的时间,因此在改进一的基础上添加锁控制,代码如下:

  public class ThreadLocalDemo {
    //创建一个核心线程为10的定长线程池
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    //日期格式
    static  SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    //格式化时间
    public static String date(int seconds){
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date=new Date(seconds*1000);
        String str = null;
        synchronized (ThreadLocalDemo.class){
            str= sdf.format(date);
        }
        return str;

    }

    public static void main(String[] args) {
        //启动1000个线程,新建1000个SimpleDateFormat对象
        for (int i = 0; i <1000 ; i++) {
            int finalI = i;
            threadPool.submit(()->{
                String date = date(finalI);
                System.out.println(date);
            });
        }
        //关闭线程池
        threadPool.shutdown();
    }


}

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

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


class ThreadLocalSimpleDateFormat{
    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"));

}
// 利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
public class ThreadLocalDemo {
    //创建一个核心线程为10的定长线程池
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);


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

    }

    public static void main(String[] args) {
        //启动1000个线程,新建1000个SimpleDateFormat对象
        for (int i = 0; i <6 ; i++) {
            int finalI = i;
            threadPool.submit(()->{
                String date = date(finalI);
                System.out.println(date);
            });
        }
        //关闭线程池
        threadPool.shutdown();
    }
}

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

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

比如当一个请求进来了,一个线程负责处理该请求,该请求会依次调用service-1(), service-2(), service-3(),同时,每个service()都需要获得调用方用户user的信息,也就是需要拿到user对象

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

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

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

//用户类
class User {
    String name;
    Integer age;
    public User(String name,Integer age){
        this.name = name;
        this.age=age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

class UserUtil{
    private static  ThreadLocal<User> holder=new ThreadLocal<>();
  
    public static void setParamter(User user){
        holder.set(user);
    }
    public static User getParamter( ){
       return holder.get();
    }

    public static void remove( ){
       holder.remove();
    }
}


class Service1 {
    public void service1(User user) {
        //给ThreadLocal赋值,后续的服务直接通过ThreadLocal获取就行了。
        UserUtil.setParamter(user);
        new Service2().service2();
    }
}


class Service2 {
    public void service2(){
        User user = UserUtil.getParamter();
        System.out.println("service2拿到的用户:"+user);
        new Service3().service3();
    }
}

class Service3 {
    public void service3() {
        User user = UserUtil.getParamter();
        System.out.println("service3拿到的用户:" + user);
        //在整个流程执行完毕后,一定要执行remove
        UserUtil.remove();
    }
}

public class ThreadLocalDemo2 {
    public static void main(String[] args) {
            User user=new User("tomy",23);
            new Service1().service1(user);
    }
}

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

2. ThreadLocal的作用和好处

2.1 ThreadLocal的两个作用

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

2.2 ThreadLocal两种用法

根据共享对象的生成时机不同,选择initialValue()set()方法来保存对象。

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

2.2 使用ThreadLocal带来的四个好处

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

3. ThreadLocal主要方法介绍

主要有initialValue()set(),get()remove()这几个方法,关于源码分析,将在第4节介绍。

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

4. ThreadLocal原理源码分析

4.1 get()方法

//获取
public T get() {
		//获取当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程的ThreadLocalMap,如果之前调用过set方法,那么这里getMap就不为null
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        	//获取ThreadLocalMap存储的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                // 调用过set会从这里return
                return result;
            }
        }
        // 如果当前线程还没有创建ThreadLocalMap,执行setInitialValue方法去初始化
        return setInitialValue();
    }
// getMap就是看看当前线程有没有创建ThreadLocalMap集合,如果没有,这个集合就是为null
ThreadLocalMap getMap(Thread t) {
		//获取当前线程的threadLocals属性
        return t.threadLocals;
        //ThreadLocal.ThreadLocalMap threadLocals = null;
    }

 private T setInitialValue() {
 		//调用你重写的initialValue获取返回值
        T value = initialValue();
        //获取当前线程
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
        	/**
        	*只有第一次使用get()才调用initialValue()的原因是:
        	*第一次创建ThreadLocalMap第二次及以后,getMap发现ThreadLocalMap不是null,走不到这个方法来了。
        	*set存的key是什么?this是当前ThreadLocal对象!
        	*/
            map.set(this, value);
        else
        	//创建一个ThreadLocalMap对象
            createMap(t, value);
        return value;
    }
//创建一个ThreadLocalMap对象,并把引用指向线程的threadLocals 
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

总结:

  • get()方法:先取出当前线程对象的ThreadLocalMap,如果ThreadLocalMap不为空,则调用map.getEntry()方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocalvalue。,如果ThreadLocalMap为空,则调用setInitialValue()方法。
  • getMap():获取当前线程内的ThreadLocalMap对象,每个线程内都有ThreadLocalMap对象,名为threadLocals,初始值为null
  • setInitialValue():先去调用initialValue()方法(如果没有重写为null,可以使用匿名内部类的方式对其初始化),然后调用getMap()获取ThreadLocalMap对象,如果不为空则调用map.set(this, value);去设置值,反之调用 createMap(t, value);去初始化一个ThreadLocalMap对象。
  • 注意:这个mapmap中的keyvalue都是保存在线程中ThreadLocalMap的,而不是保存在ThreadLocal中。

4.2 set()方法

// 把当前线程需要全局共享的value传入
public void set(T value) {
		//获取当前线程
        Thread t = Thread.currentThread();
        //获取线程中的属性ThreadLocalMap ,
        ThreadLocalMap map = getMap(t);
        // map对象为空就创建,不为空就覆盖
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

总结:

  • set():先取出当前线程对象的ThreadLocalMap,如果ThreadLocalMap不为空,则直接更新value值,如果为空调用createMap()去初始化一个ThreadLocalMap对象。并将value初始化。
  • 上面也对createMap()进行分析,发现是新建ThreadLocal的一个静态内部类对象。
//创建一个ThreadLocalMap对象,并把引用指向线程的threadLocals 
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

  static class ThreadLocalMap {
 
        static class Entry extends WeakReference<ThreadLocal<?>> {
            
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }      
    }

  ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

可看出ThreadLocalMapThreadLocal内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。详细内容要大家自己去跟。

4.3 initialValue()方法

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

   public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal=new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

4.4 remove()方法

// 删除对应这个线程的值
 public void remove() {
 			//  获取当前线程的ThreadLocalMap
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
         // 移除这个ThreadLocal对应的值
             m.remove(this);
     }
  • remove():直接将ThreadLocal对应的值从当前线程的ThreadLocalMap中删除,为什么要删除呢,这涉及到内存泄漏的问题。
  • 实际上ThreadLocalMap中使用的keyThreadLocal软引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉
  • 所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocalkey 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

4.5 ThreadLocalMap类

ThreadLocalMap类,也就是Thread.threadLocals

// 此行声明在Thread类中,创建ThreadLocalMap就是对Thread类的这个成员变量赋值
ThreadLocal.ThreadLocalMap threadLocals = null;

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

private Entry[] table;

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

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

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

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

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

5. ThreadLocal需要注意的点

5.1 ThreadLocal内存泄漏问题

什么是内存泄漏?
某个对象不再有用,但是占用的内存却不能被回收,时机久了,内存中存储的数据越来越多,就会导致内存溢出(OOM)。

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

也就是说,ThreadLocal是可能出现Value泄漏的。

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

 static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);//key值交给WeakReference处理,是一个弱引用
                value = v;//value直接用变量保存,是强引用
            }
        }
  • 正常情况下,当线程终止时,保存在ThreadLocalMap中的value也会被垃圾回收,因为没有任何强引用了,但是在项目中我们一般使用线程池,线程都是复用的,一般线程都不会结束,那么key对应的value就不会被回收,因为有以下的调用链。

Thread---->ThreadLocalMap---->Entry<key为null,弱引用被回收>---->value

  • 因为valueThread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM
  • JDK已经考虑到了这个问题,所以在remove(), rehash()方法中会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以被回收,比如rehash()里面调用resize():
 private void resize() {
           ......省略代码
          ThreadLocal<?> k = e.get();
           if (k == null) {
               e.value = null; // Help the GC
           } 

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

但是如果一个ThreadLocal不被使用,那么实际上set, remove,
rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄漏。所以我们在使用结束后还是调用remove()方法去删除对应的Entry。

5.2 ThreadLocal如何避免内存泄漏?

使用结束后及时调用remove()方法,删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal之后,应该调用remove()方法。

比如使用拦截器获取到用户信息,用户信息保存在ThreadLocalMap中,线程请求结束之前拦住它,并用remove()清除User对象,这样就能保证不会内存泄漏。

5.3 共享对象问题

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

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

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

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

  • DateTimeContextHolder类,应用了ThreadLocal。
  • ThreadLocal的典型应用场景:每次HTTP请求都对应一个线程,线程之间相互隔离。
  • 看RequestContextHolder,也是用到了ThreadLocal,看NamedThreadLocal源码,再看getRequestAttributes的调用

5.6 ThreadLocal的实例以及其值存放在堆还是栈上?

  • 在java中,栈内存属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属的线程可见,即栈内存可以理解为线程的私有内存。
  • 堆内存中的对象对所有线程是可见的,堆内存中的对象可以被所有线程访问。
  • 有人可能会说,ThreadLocal的实例以及其值存放在栈上,理由是是只能当前线程访问,其他线程访问不到。其实不是的,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有)。而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。

5.7 ThreadLocal的数据可以在其他线程共享吗?

是可以的,只不过只能在主线程和子线程之间共享,也就是父线程和子线程之间可以共享数据。使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。

public class InheritableThreadLocalDemo {
    //创建一个InheritableThreadLocal实例,在多个线程线程共享
    private  static ThreadLocal<String> threadLocal=new InheritableThreadLocal<>();
    public static void main(String[] args) {
        threadLocal.set("我是主线程");

        Thread t=new Thread(()->{
            System.out.println("子线程获取主线程的值: "+threadLocal.get());
        });

        //启动线程
        t.start();
    }
}

测试结果:
在这里插入图片描述
在子线程中我是能够正常输出那一行日志的,这也是我之前提到过的父子线程数据传递的问题。

5.8 InheritableThreadLocal是怎么传递数据的呢?

传递的逻辑很简单,我在开头Thread代码提到threadLocals的时候,你们再往下看看就会发现放了另外一个变量:

    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

Thread源码中,我们看看Thread.init()初始化创建的时候做了什么:

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
      //省略其他代码
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    }
  • 我就截取了部分代码,如果线程的inheritThreadLocals变量不为空,比如我们上面的例子,而且父线程的inheritThreadLocals也存在,那么我就把父线程的inheritThreadLocals给当前线程的inheritThreadLocals。
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值