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 加锁,修复线程不安全问题)
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 那样带来线程安全问题,有多少个线程只就会有多少个副本,线程复用依然使用同一个副本。)
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()方法
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 对象。
initialValue()
- 该方法会返回当前线程对应的“初始值”,这是个延迟加载的方法,只有在调用 get() 的时候,才会触发。
- 当前线程第一次使用 get() 方法访问变量时,将调用此方法, 除非线程先前调用了 set() 方法, 在这种情况下,不会为线程调用本 initialValue() 方法。
- 通常,每个线程最多调用一次此方法,但如果已经调用了 remove() 后,再调用 get(),则可以再次调用此方法。
- 如果不重写本方法,这个防范会返回 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 的典型应用场景。