目录

Life in Flow

知不知,尚矣;不知知,病矣。
不知不知,殆矣。

X

ThreadLocal

ThreadLocal 适用场景

场景 1:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有 SimpleDateFormat、Random、工具类线程不安全,所以每个线程需要拥有自己独立的工具类)

1* 每个 Thread 内有自己的实例副本,不共享

SimpleDateFormat 进化过程

11. 2个线程分别用自己的 SimpleDateFormat 
22. 10个线程变为for循环,10个线程new 了10个SimpleDateFormat 
33. 1000个线程,必然用线程池(否则消耗太多内存)
44. static SimpleDateFormat ,所有线程公用同一个 SimpleDateFormat 对象
5	(发现线程不安全)
65. 用 synchronized 加锁修复了线程安全性,但是影响了并发能力。
76. ThreadLocal 登场。线程池中的每个线程中,都有一个 SimpleDateFormat 对象副本,提高并发能力,还不会像 synchronized 那样带来线程安全问题

示例:两个线程分别打印出自己的日期 (用 synchronized 加锁,修复线程不安全问题)
多个线程同时访问 SimpleDateFormat

 1import java.text.SimpleDateFormat;
 2import java.util.Date;
 3import java.util.concurrent.ExecutorService;
 4import java.util.concurrent.Executors;
 5
 6/**
 7 * 描述:     加锁来解决线程安全问题
 8 */
 9public class ThreadLocalNormalUsage04 {
10
11    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
12    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH🇲🇲ss");
13
14    public static void main(String[] args) throws InterruptedException {
15        for (int i = 0; i < 1000; i++) {
16            int finalI = i;
17            threadPool.submit(new Runnable() {
18                @Override
19                public void run() {
20                    String date = new ThreadLocalNormalUsage04().date(finalI);
21                    System.out.println(date);
22                }
23            });
24        }
25        threadPool.shutdown();
26    }
27
28    public String date(int seconds) {
29        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
30        Date date = new Date(1000 * seconds);
31        String s = null;
32        synchronized (ThreadLocalNormalUsage04.class) {
33            s = dateFormat.format(date);
34        }
35        return s;
36    }
37}

示例:两个线程分别打印出自己的日期 (线程池中的每个线程中,都有一个 SimpleDateFormat 对象副本,提高并发能力,还不会像 synchronized 那样带来线程安全问题,有多少个线程只就会有多少个副本,线程复用依然使用同一个副本。)
ThreadLocal 典型应用场景一

 1import java.text.SimpleDateFormat;
 2import java.util.Date;
 3import java.util.HashSet;
 4import java.util.concurrent.ExecutorService;
 5import java.util.concurrent.Executors;
 6
 7/**
 8 * 描述: 利用 ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
 9 */
10public class ThreadlocalNormalUsage666 {
11
12    public static void main(String[] args) {
13        ExecutorService executorService = Executors.newFixedThreadPool(10);
14        HashSet<String> hashSet = new HashSet<>();
15        for (int i = 0; i < 1000; i++) {
16            int circleNum = i;
17            executorService.submit(new Runnable() {
18                @Override
19                public void run() {
20                    String date = ThreadlocalNormalUsage666.date(circleNum);
21                    System.out.println(date);
22                    hashSet.add(date);
23                }
24            });
25        }
26        executorService.shutdown();
27        while (!executorService.isTerminated()) {
28        }
29        System.out.println(hashSet.size());//1000
30    }
31
32    public static String date(int seconds ) {
33        ////参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
34        Date date = new Date(1000 * seconds);
35        SimpleDateFormat simpleDateFormat = ThreadLocal4SafeDateFormatter.simpleDateFormatThreadLocal.get();
36        String format = simpleDateFormat.format(date);
37        return format;
38    }
39}
40
41class ThreadLocal4SafeDateFormatter {
42    public static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
43        @Override
44        protected SimpleDateFormat initialValue() {
45            return new SimpleDateFormat("yyyy-MM-dd HH🇲🇲ss");
46        }
47    };
48    
49    //lambda的形式
50    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal4Lambda = ThreadLocal
51            .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH🇲🇲ss"));
52    
53}

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

1* 强调的是同一个请求(同一个线程内)不同方法间的共享
2* 不需重写initialValue()方法,但是必须手动调用set()方法

user 作为参数层层传递
ThreadLoal 典型应用场景二

 1/**
 2 * 描述:     演示ThreadLocal用法2:避免传递参数的麻烦
 3 */
 4public class ThreadLocalNormalUsage06 {
 5
 6    public static void main(String[] args) {
 7        new Service1().process("");
 8    }
 9}
10
11class Service1 {
12
13    public void process(String name) {
14        User user = new User("超哥");
15        UserContextHolder.holder.set(user);//设置到ThreadLocal中
16        new Service2().process();
17    }
18}
19
20class Service2 {
21
22    public void process() {
23        User user = UserContextHolder.holder.get();
24        System.out.println("Service2拿到用户名:" + user.name);//Service2拿到用户名:超哥
25        new Service3().process();
26    }
27}
28
29class Service3 {
30
31    public void process() {
32        User user = UserContextHolder.holder.get();
33        System.out.println("Service3拿到用户名:" + user.name);//Service3拿到用户名:超哥
34        UserContextHolder.holder.remove();//避免ThreadLocalMap中的Entry内存泄漏。
35    }
36}
37
38class UserContextHolder {
39    public static ThreadLocal<User> holder = new ThreadLocal<>();
40}
41
42class User {
43    String name;
44    public User(String name) {
45        this.name = name;
46    }
47}

ThreadLocal 作用

  • 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象),在固定线程数的每个线程中有独立的副本对象(节省内存),同时避免了 synchronized 带来并发性能下降的问题。
  • 同一个请求内(一次请求对应一个线程),任何方法都可以获取到该对象,避免传参的繁琐。

initialValue 适用的场景
 在 ThreadLocal 第一次 get 的时候把对象初始化出来,对象的初始化时机可以由我们控制。

set 适用的场景
 如果需要保存到 ThreadLocal 里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用 ThreadLocal.set 直接放到我们的 ThreadLocal 中去,以便后续使用。

ThreadLocal 带来的好处

  • 达到线程安全(static 共享并不能做到线程安全)
  • 不需要加锁、提高了并发效率
  • 更搞笑地利用内存、节省开销:相比于每个任务都新建一个 SimpleDateFormat,显然用 ThreadLocal 可以节省内存和开销。
  • 免去传参的繁琐(无论是场景一的工具类、还是场景二的用户,都可以在任何地方直接通过 ThreadLocal 拿到,再也不需要每次都传同样的参数。Thread 使得代码耦合度更低,更优雅)

ThreadLocal 原理

Thread、ThreadLocal、ThreadLocalMap 三者之间的关系

  • 每个 Thread 对象中都持有一个 ThreadLocalMap 成员变量。
  • 一个 ThreadLocalMap 中包含多个 ThreadLocal 对象。
    ThreadLocal 原理图

initialValue()

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

void set(T t)
 为这个线程设置一个 ThreadLocal 对象的值。

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

void remove()
 删除对应这个线程中 ThreadLocal 对象的值。

两种场景殊途同归
 setInitialVlaue()、set() 最后都是利用 map.set() 方法来设置值。最后都会对应到 ThreadLocalMap 的一个 Entry ,只不过是起点和入口不一样。

ThreadLocal 注意点

内存泄漏
 内存泄漏:某个对象不再有用,但是占用的内存却不能被回收。
ThreadLocalMap 的每个 Entry 都是一个对 key 的弱引用,同时,每个 Entry 都包含了一个对 value 的强引用。 一个线程长时间被复用,导致其 ThreadLocalMap 中会有很多遗留无法回收的 Entry。
 JDK 已经考虑到了这个问题,所以在 set、remove、rehash 方法中会扫描 key 为 null 的 Entry,并吧对应的 value 设置成 null,这样 value 对象就可以被回收。
 但是如果一个 ThreadLocal 不被使用,那么实际上 set,remove,rehash 防范也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了 value 的内存泄漏。
调用 remove 方法,就会删除对应的 Entry 对象,可以避免内存泄露,所以使用完 ThreadLocal 之后,应该调用 remove 方法。

1# 线程池复用线程(某线程长时间被复用),以下调用链会导致内存泄漏:
2Thread --> ThreadLocalMap --> Entry(key为null) --> Value (强引用:无法被GC回收)

ThreadLocal 空指针异常
 抛出异常是因为类型转换,null 无法从包装类转换成基本类型。get 本身只会返回 null,并不会抛出异常。

 1public class ThreadLocalNPE {
 2
 3    ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>();
 4
 5    public void set() {
 6        longThreadLocal.set(Thread.currentThread().getId());
 7    }
 8
 9    /**
10     * 装箱拆箱   Long -> long
11     * @return
12     */
13    public long get() {
14        return longThreadLocal.get();//null 无法转换成long
15    }
16
17    public static void main(String[] args) {
18        ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
19        System.out.println(threadLocalNPE.get());
20        Thread thread1 = new Thread(new Runnable() {
21            @Override
22            public void run() {
23                threadLocalNPE.set();
24                System.out.println(threadLocalNPE.get());
25            }
26        });
27        thread1.start();
28    }
29}

共享对象
 如果在每个线程中 ThreadLocal.set() 进去的东西本身就是多个线程国祥的对象(比如 static 对象),那么多个线程的 ThreadLocal.get() 取得的还是这个共享对象本身,依然存在并发问题。不应该在 ThreadLocal 中存放静态对象。

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

优先使用框架的支持,而不是自己创造
 例如在 Spring 中,如果可以使用 RequestContextHolder,那么就不需要自己维护 ThreadLocal,因为自己可能会忘记调用 remove() 方法等,造成内存泄漏。

  • DateTimeContextHolder、RequestContextHolder ……
  • 每次 HTTP 请求都对应一个线程,线程之间相互隔离,这就是 ThradLocal 的典型应用场景。

作者:Soulboy