ThreadLocal详解——一文彻底弄懂ThreadLocal含源码分析

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

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

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

场景一每个 Thread 内有自己的实例副本,不共享

假设有一个转换日期的date方法调用SimpleDateFormat类的format方法来实现日期的转换

public String date(int seconds){
        Date date=new Date(1000*seconds);  //毫秒转换为秒
        SimpleDateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return dateFormat.format(date);
}

第一步我们要创建十个线程来执行日期转换,代码如下

public class ThreadLocalTest01 {
    public static void main(String[] args) {
        for(int i=0;i<10;i++){    //创建十个线程
            int finalI = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String date=new ThreadLocalTest01().date(finalI);   //调用自己的date方法
                    System.out.println(date);
                }
            }).start();
        }
    }
}

代码执行的结果如下:

ThreadLocal详解——一文彻底弄懂ThreadLocal含源码分析_第1张图片

第二步我们需要创建1000个线程来完成日期转换

当线程数来到1000后再使用for loop创建1000线程就比较耗费资源了,此时我们想到了上一节的线程池

代码如下:

public class ThreadLocalTest02 {
    public static ExecutorService threadpool=
            Executors.newFixedThreadPool(10); //创建一个固定核心线程数的线程池
    public static void main(String[] args) {
        for(int i=0;i<1000;i++){
            int finalI = i;
            threadpool.submit(new Runnable() {   //向线程池中提交任务
                @Override
                public void run() {
                    String date=new ThreadLocalTest01().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadpool.shutdown();
    }
}

运行结果如下,结果显示并没有什么问题

ThreadLocal详解——一文彻底弄懂ThreadLocal含源码分析_第2张图片

但此时我们想到这1000个线程要创建1000个SimpleDateFormat类是不是有点浪费资源

我们尝试只用一个SimpleDateFormat类并将其设为静态资源,每个线程直接调用即可

第三步共享SimpleDateFormat类

public class ThreadLocalTest02 {
    public static ExecutorService threadpool=
            Executors.newFixedThreadPool(10); //创建一个固定核心线程数的线程池
    public static SimpleDateFormat dateFormat=
            new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");; //将SimpleDateFormat改为静态
    public static void main(String[] args) {
        for(int i=0;i<1000;i++){
            int finalI = i;
            threadpool.submit(new Runnable() {   //向线程池中提交任务
                @Override
                public void run() {
                    Date date1=new Date(1000*finalI);  //毫秒转换为秒
                    String date=dateFormat.format(date1);  //直接调用静态成员的方法
                    System.out.println(date);
                }
            });
        }
        threadpool.shutdown();
    }
}

此时代码的执行结果如下,此时已经可以看到问题,有两个线程的执行结果相同,但是传入每个线程的时间都是不同的,说明出现了线程安全问题

ThreadLocal详解——一文彻底弄懂ThreadLocal含源码分析_第3张图片

第四步使用synchronized加锁

public void run() {
    Date date1=new Date(1000*finalI);  //毫秒转换为秒
    String date=null;
    synchronized (ThreadLocalTest03.class){      //调用共享资源dateFormat时加锁,可避免线程安全问题
        date=dateFormat.format(date1);
    }
    System.out.println(date);
}

使用这种方法是可行的,加锁之后同一时间只有一个线程调用dateFormat

但是这种方法造成线程都在排队等待释放锁,必然导致效率的降低

第五步使用ThreadLocal完美解决问题

public class ThreadLocalTestFinal {
    public static ExecutorService threadpool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) {
        for(int i=0;i<1000;i++){
            int finalI=i;
            threadpool.submit(new Runnable() {
                @Override
                public void run() {
                    //通过threadLocal的get方法获取到SimpleDateFormat对象
                    //这是线程独有的自己的对象
                    String date=TreadLocalTest.threadLocal.get().format(1000*finalI);
                    System.out.println(date);
                }
            });
        }
        threadpool.shutdown();
    }
}

class TreadLocalTest{
    public static ThreadLocal<SimpleDateFormat> threadLocal=new ThreadLocal<SimpleDateFormat>(){
        //需重写initialvalue方法,返回指定格式的SimpleDateFormat
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };

}

ThreadLocal详解——一文彻底弄懂ThreadLocal含源码分析_第4张图片

场景二每个线程内需要保存全局变量

实例当前用户信息需要被线程所有方法共享,如果每次都将用户信息作为参数进行传递是很麻烦的

  • 第一种方案是设置一个UserMap所有方法都可以put和get,当多线程同时工作时,我们需要保证线程安全,可以用synchronized,也可以用ConcurrentHashMap,但无论用什么,都会对性能有所影响
  • 更好的办法是使用ThreadLocal,这样无需synchronized,可以在不影响性能的情况下,也无需层层传递参数,就可达到保存当前线程对应的用户信息的目的
  1. 用ThreadLocal保存一些业务内容(用户权限信息、从用户系统获取到的用户名、user ID等,这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的
  2. 在线程生命周期内,都通过这个静态ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象(例如user对象)作为参数传递的麻烦
  3. 强调的是同一个请求内(同一个线程内)不同方法间的共享,不需重写initialValue()方法,但是必须手动调用set()方法

ThreadLocal的两个作用和好处

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

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

总结ThreadLocal的两个作用就是“线程间隔离,线程内共享

关于初始化的时机

  1. initialValue在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机可以由我们控制
  2. 如果需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息用ThreadLocal.set直接放到我们的ThreadLocal中去,以便后续使用

使用ThreadLocal的好处

  1. 达到线程安全
  2. 不需要加锁,提高执行效率
  3. 更高效地利用内存、节省开销

–相比于每个任务都新建一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销

主要方法介绍

  • T initialValue( ):初始化并返回当前线程对应的"初始值",这是一个延迟加载的方法,只有在调用get的时候,才会触发,如果此前已经使用了set方法,不会再调用initialValue() 。如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。

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

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

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

原理、源码分析

首先要理清楚Thread,ThreadLocal,ThreadLocalMap三者关系

ThreadLocal详解——一文彻底弄懂ThreadLocal含源码分析_第5张图片

initialValue方法源码

protected T initialValue() {
    return null;
}
  • 没有默认实现
  • 如果要用initialValue方法,需要自己实现
  • 通常使用匿名内部类的方式实现(回顾代码)

get方法源码解析

public T get() {
    Thread t = Thread.currentThread();   //获取当前线程
    ThreadLocalMap map = getMap(t);      //获取当前线程的ThreadLocalMap变量
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);  
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;     //如果非空,以当前类为key获取value值
            return result;
        }
    }
    return setInitialValue();   //如果为空,则执行初始化方法
}

private T setInitialValue() {
    T value = initialValue();       //调用默认的初始化方法赋值,如果没有重写则为默认值null
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);    //如果存在map,直接set值
    else
        createMap(t, value);     //不存在map,则创建一个map并赋初值
    return value;
}
  1. get方法是先取出当前线程的ThreadLocalMap
  2. 然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入
  3. 取出map中属于本ThreadLocal的value

set方法源码解析

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

与setInitialValue方法很类似

remove方法源码解析

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());    //获取到ThreadLocalMap变量
    if (m != null)
        m.remove(this);   //移除以此类为key的entity
}

ThreadLocalMap 类是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对:

  • 键:这个ThreadLocal
  • 值:实际需要的成员变量,比如User或者SimpleDateFormat对象
  • ThreadLocalMap和HashMap一样,有set,get,rehash,resize等
  • ThreadLocalMap这里采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链法

注意点

内存泄漏

内存泄漏概念:某个对象不再有用,但是占用的内存却不能被回收

key的泄漏:ThreadLocalMap中的Entry继承自WeakReference,是弱引用

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

弱引用的特点:如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收所以弱引用不会阻止GC,因此这个弱引用的机制

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

  • 正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了

  • 但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下的调用链用

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

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

空指针异常

如果在set之前调用get,会返回一个null值

因为泛型T的原因,返回的是一个包装的类,如果get得到的是一个基本类型如int,那么在null转换为基本类型时会报空指针异常

实际应用场景——在Spring中的实例分析

ThreadLocal的典型应用场景:每次HTTP请求都对应一个线程,线程之间相互隔离

看RequestContextHolder,也是用到了ThreadLocal,看NamedThreadLocal源码,再看getRequestAttributes的调用

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

你可能感兴趣的:(分布式与多线程,java,开发语言)