1. ThreadLocal两种使用场景

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

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

1.1 场景1:每个线程需要一个独享的对象

每个Thread内有自己的实例副本,不共享,比喻:教材只有一本,一起做笔记有线程安全问题,复印后没问题。
下面通过介绍SimpleDateFormat的进化之路,来演示场景1的用法,SimpleDateFormat本身是一个线程不安全的类。

1)2个线程分别用自己的SimpleDateFormat

image.png
使用两个线程打印日期。

  1. /**
  2. * description: 两个线程打印日期<br>
  3. * date: 2020-06-10 21:42 <br>
  4. * author: wzy <br>
  5. * version: 1.0 <br>
  6. */
  7. public class ThreadLocalNormalUsage00 {
  8. public String date(int seconds) {
  9. // 参数的单位是毫秒,是从1970.1.1 00:00:00 GMT计时,往上加,我们在东八区,会加八个小时
  10. Date date = new Date(1000 * seconds);
  11. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
  12. return sdf.format(date);
  13. }
  14. public static void main(String[] args) {
  15. new Thread(new Runnable() {
  16. @Override
  17. public void run() {
  18. String date = new ThreadLocalNormalUsage00().date(10);
  19. System.out.println(date);
  20. }
  21. }).start();
  22. new Thread(new Runnable() {
  23. @Override
  24. public void run() {
  25. String date = new ThreadLocalNormalUsage00().date(104707);
  26. System.out.println(date);
  27. }
  28. }).start();
  29. }
  30. }

执行结果:正常的打印了日期,没有出现问题。
image.png

2)延伸出10个线程使用SimpleDateFormat

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

  1. /**
  2. * description: 十个线程打印日期 <br>
  3. * date: 2020-06-10 21:42 <br>
  4. * author: wzy <br>
  5. * version: 1.0 <br>
  6. */
  7. public class ThreadLocalNormalUsage01 {
  8. public String date(int seconds) {
  9. // 参数的单位是毫秒,是从1970.1.1 00:00:00 GMT计时,往上加,我们在东八区,会加八个小时
  10. Date date = new Date(1000 * seconds);
  11. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
  12. return sdf.format(date);
  13. }
  14. public static void main(String[] args) throws InterruptedException {
  15. for (int i = 0; i < 30; i++) {
  16. int finalI = i;
  17. new Thread(new Runnable() {
  18. @Override
  19. public void run() {
  20. String date = new ThreadLocalNormalUsage01().date(finalI);
  21. System.out.println(date);
  22. }
  23. }).start();
  24. Thread.sleep(100);
  25. }
  26. }
  27. }

执行结果:没有出现问题,不过每一个线程创建一个SimpleDateFormat,明显不合理。
image.png

3)当需求变成了1000个

 必然要用线程池(否则消耗内存太多)缺点:创建了1000个对象。<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/421114/1591798320691-f15356dc-45cc-4abd-bcd4-2fc7784097bb.png#crop=0&crop=0&crop=1&crop=1&height=264&id=nLdDw&margin=%5Bobject%20Object%5D&name=image.png&originHeight=527&originWidth=1285&originalType=binary&ratio=1&rotation=0&showTitle=false&size=109459&status=done&style=none&title=&width=642.5)

多个线程指向同一个SimpleDateFormat对象,发生了线程安全问题。

/**
 * description: 1000个打印日期的任务,用线程池来执行 <br>
 * date: 2020-06-10 21:42 <br>
 * author: wzy <br>
 * version: 1.0 <br>
 */
public class ThreadLocalNormalUsage02 {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public String date(int seconds) {
        // 参数的单位是毫秒,是从1970.1.1 00:00:00 GMT计时,往上加,我们在东八区,会加八个小时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat sdf  = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return sdf.format(date);
    }

    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 ThreadLocalNormalUsage02().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }
}

执行结果:
image.png

4)所有线程共用一个SimpleDateFormat

让线程池中的所有的线程都共享同一个SimpleDateFormat,将会发生线程安全问题。


/**
 * description: 1000个打印日期的任务,用线程池来执行 <br>
 * date: 2020-06-10 21:42 <br>
 * author: wzy <br>
 * version: 1.0 <br>
 */
public class ThreadLocalNormalUsage03 {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    public static SimpleDateFormat sdf  = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    public String date(int seconds) {
        // 参数的单位是毫秒,是从1970.1.1 00:00:00 GMT计时,往上加,我们在东八区,会加八个小时
        Date date = new Date(1000 * seconds);
        return sdf.format(date);
    }

    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(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }
}

运行结果:可见运行结果,显示相同的时间,可见发生了线程安全问题。
image.png


5)加锁

缺点效率低;将线程不安全的方法进行加锁

/**
 * description: 加锁来解决线程安全问题 <br>
 * date: 2020-06-10 21:42 <br>
 * author: wzy <br>
 * version: 1.0 <br>
 */
public class ThreadLocalNormalUsage04 {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    public static SimpleDateFormat sdf  = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    public String date(int seconds) {
        // 参数的单位是毫秒,是从1970.1.1 00:00:00 GMT计时,往上加,我们在东八区,会加八个小时
        Date date = new Date(1000 * seconds);

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

        return s;
    }

    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 ThreadLocalNormalUsage04().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }
}

虽然加锁能够解决线程安全问题,但是所有线程只能串行执行,效率将会大大降低。

6)使用ThreadLocal

更好的解决方案是使用ThreadLocal,为线程池每个线程都分配一个SimpleDateFormat对象,这样在每个线程内部都有不同的对象,就可以保证线程安全问题。

/**
 * description: 利用ThreadLocal给每个线程分配自己dateFormat对象 <br>
 * date: 2020-06-10 21:42 <br>
 * author: wzy <br>
 * version: 1.0 <br>
 */
public class ThreadLocalNormalUsage05 {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public String date(int seconds) {
        // 参数的单位是毫秒,是从1970.1.1 00:00:00 GMT计时,往上加,我们在东八区,会加八个小时
        Date date = new Date(1000 * seconds);
        // 在每个线程中会有一份,总共有十份
        SimpleDateFormat sdf  = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return sdf.format(date);
    }

    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(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }
}

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

    // 也可以使用Lambada表达式方式进行实现
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));

}

image.png

1.2 场景2:解决传参问题

实例:当用户信息需要被线程内所有方法共享
一个比较繁琐的解决方案是把user作为参数层层传递,从service-1()传到service2,再到service3,以此类推,但是这样做会导致代码的冗余且不易维护。

image.png
每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦。比如用ThreadLocal保存一些业务内容(用户权限信息,从用户系统获取到的用户名,userId等),这些信息在同一个线程内相同,但是 不同的线程使用的业务内容是不同的。

实例:当前用户信息需要被线程内的方法共享
在此基础上可以演进,使用UserMap
image.png
image.png

用同步锁或线程安全map对性能也会有影响,因为用到了锁。强调的是同一个请求内(同一线程内)不同方法建的共享。不需要重写initialValue()方法,但是必须手动调用set()方法。

2. ThreadLocal原理

2.1 ThreadLocal类之间的关系

搞清楚Thread、ThreadLocal以及ThreadLocalMap三者之间的关系。
每个Thread对象中都持有一个ThreadLocalMap成员变量。

image.png

Thread类中有ThreadLocalMap的成员变量threadLocals,而ThreadLocalMap则是ThreadLocal中的一个内部类。

image.png

2.2 ThreadLocal主要方法

T initialVaule(): 初始化
void set(T t): 为这个线程设置一个新值。
T get():得到这个线程对应的value。如果是首次调用get(),则会调用initialize来得到这个值。
void remove():删除对应这个线程的值

1) initiaValue()

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

2) get()

get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value。
注意,这个map以及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中。
image.png
getMap方法,获取到了当前线程的ThreadLocalMap
image.png
如果通过ThreadLocal的引用没有取到值,则回去调用initialValue方法去获取初始值,并把初始值set到map中,如果Map没有被创建则创建一个Map再保存值,并将值返回。
image.png

3) set()

首先获取当前线程对象,根据线程对象的引用获取保存在Thread对象中的ThreadLocalMap,ThreadLocal的引用作为key,value为ThreadLocalMap中保存的值。
image.png

4) remove

根据当前ThreadLocal的引用,移除当前线程中的ThreadLocalMap中的元素。
image.png

2.3 ThreadLocalMap类源码

image.png
ThreadLocalMap类,也就是Thread.threadLocals

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

拉链法,HashMap
image.png

ThreadLocalMap 有所不同,采用的是线性探测法。也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链。ThreadLocalMap 插入元素,去找空的位置,不回去做红黑树或者链表。

2.4 总结

每一个线程中,都有一个ThreadLocalMap,最开始是空的,有ThreadLocal赋值就不是空的,键就是ThreadLocal的指针,值就是要保存的内容。

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

3. TheadLocal的注意点

3.1 内存泄漏问题

什么是内存泄露:某个对象不再有用,但是占用的内存却不能被回收,容易进入到OOM,内存泄露分两种情况,第一种,key,另一种是value。

1)key的情况

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

image.png

2)value的情况

ThreadLocalMap的每个Entry都是一个对key的弱引用,同时,每个Entry都包含了一对value的强引用,正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了。但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下调用链:
Thread-> ThreadLocalMap -> Entry(key为null)-> Value

因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM,JDK已经考虑到了这个问题,所以在set、remove、rehash方法中会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以回收。

reset方法
image.png
但这不够,因为我们不一定去调用这些方法。如果ThreadLocal不被使用,那么实际上set,remove,rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么导致了value的内存泄露。

如何避免内存泄露(阿里规约)
调用remove方法,就会删除对应的Entry对象,可以避免内存泄露,所以使用完ThreadLocal之后,应该调用remove方法。

3.2 空指针异常问题

在进行get之前,必须先set,否则可能会报空指针异常,

3.3 共享对象问题

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

3.4 Thread的使用原则问题

如果可以不使用ThreadLocal就解决问题,那么不要强行使用。如果线程很少的话,比如就两个线程,就不需要用ThreadLocal。
优先使用框架的支持,而不是自己创造,例如在spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能回忘记调用remove等方法,造成内存泄露
DateTimeContenxtHolder类也用到了ThreadLocal,每次HTTP请求都对应一个线程,线程之间相互隔离,这就是ThreadLocal的典型应用场景。