ThreadLocal详解-笔记

什么是ThreadLocal

ThreadLocal在《Java核心技术 卷一》中被称作线程局部变量,我们可以利用ThreadLocal创建只能由同一线程读和写的变量。因此就算两个线程正在执行同一段代码,并且这段代码具有对ThreadLocal变量的引用,这两个线程也无法看到彼此的ThreadLocal变量。

ThreadLocal两大使用场景

1、每个线程需要一个独享的线程变量(通常是在工具类中使用,典型的比如线程非安全的SimpleDateFormat和Random类)

2、 每个线程内需要保存全局变量,可以让不同方法使用,避免参数不断传递的麻烦(列如在拦截器中获取用户信息,用于当前线程中使用)。

ThreadLocal在场景一中的使用

  1. /**
  2. * @author: Durian
  3. * @date: 2020/9/3 16:59
  4. * @description: 场景一 SimpleDateFormat
  5. */
  6. public class ThreadLocalDemo1
  7. {
  8. public static void main(String[] args)
  9. {
  10. new Thread(() -> new ThreadLocalDemo1().date(10)).start();
  11. new Thread(() -> new ThreadLocalDemo1().date(120)).start();
  12. }
  13. private void date(int seconds)
  14. {
  15. Date date = new Date(seconds * 1000); // 从1970-1-1开始
  16. SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd hh:mm:ss");
  17. String s = sdf.format(date);
  18. System.out.println(Thread.currentThread().getName() + " = " + s);
  19. }
  20. }
  21. 运行结果:
  22. Thread-0 = 1970-01-01 08:00:10
  23. Thread-1 = 1970-01-01 08:02:00

上面的代码展示了当两个线程同时使用SimpleDateFormat去格式化时间,可以看出并没有任何问题,因为在该代码中,SimpleDateFormat在每个线程中都会被实例化,每个线程都拥有自己的SimpleDateFormat类,所以并不会有线程安全问题。但是这里仅仅是两个线程使用,如果是很多线程使用呢,那么就会创建许多SimpleDateFormat对象,但是使用完马上又被销毁,这是很消耗资源的操作,于是将SimpleDateFormat提取出来作为一个static全局变量,降低资源 消耗。

看下面的代码:

  1. /**
  2. * @author: Durian
  3. * @date: 2020/9/3 17:09
  4. * @description: 许多线程使用SimpleDateFormat
  5. */
  6. public class ThreadLocalDemo2
  7. {
  8. private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  9. private static Set<String> dates = Collections.synchronizedSet(new HashSet<>());
  10. public static void main(String[] args) throws InterruptedException
  11. {
  12. ExecutorService executorService = Executors.newFixedThreadPool(10);
  13. for (int i = 0; i < 100; i++)
  14. {
  15. int finalI = i;
  16. executorService.execute(() -> new ThreadLocalDemo2().date(finalI * 10));
  17. }
  18. executorService.shutdown();
  19. Thread.sleep(1000);
  20. System.out.println(dates.size());
  21. }
  22. private void date(int seconds)
  23. {
  24. Date date = new Date(seconds * 1000); // 从1970-1-1开始
  25. String s = sdf.format(date);
  26. System.out.println(Thread.currentThread().getName() + " = " + s);
  27. dates.add(s);
  28. }
  29. }
  30. 部分运行结果:
  31. pool-1-thread-9 = 1970-01-01 08:01:20 // 1970-01-01 08:01:20
  32. pool-1-thread-3 = 1970-01-01 08:00:00
  33. pool-1-thread-8 = 1970-01-01 08:01:20 // 1970-01-01 08:01:20
  34. pool-1-thread-4 = 1970-01-01 08:00:40
  35. pool-1-thread-10 = 1970-01-01 08:02:00
  36. pool-1-thread-10 = 1970-01-01 08:02:10
  37. pool-1-thread-3 = 1970-01-01 08:02:30
  38. set总数:81

运行结果出现了重复的结果,并且set集合大小不等于100,可以表明在多个线程同时使用一个SimpleDateFormat变量时,将出现线程安全问题。

在SimpleDateFormat的源码中,可以看到format的方法中直接对calendar进行了直接的值操作,在多线程环境中将出现安全问题。

ThreadLocal笔记 - 图1

从上面代码可以看出,多线程中使用SimpleDateFormat出现线程安全问题的源头就是,SimpleDateFormat对象被线程共享了,而SimpleDateFormat类的方法并不是线程安全的,所以解决的办法就是使得SimpleDateFormat不被线程所共享。而ThreadLocal的本意就是“线程本地”,意思就是将一个全局变量为每一个线程做一份拷贝,每个线程的变量是隔离的,互不影响。

通过代码演示:

  1. /**
  2. * @author: Durian
  3. * @date: 2020/9/3 17:23
  4. * @description:
  5. */
  6. public class ThreadLocalDemo3
  7. {
  8. private static ThreadLocal<SimpleDateFormat> sdfThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")); // 使用lambda表达式初始化ThreadLocal
  9. // 用来判断结果是否都是唯一
  10. private static Set<String> dates = Collections.synchronizedSet(new HashSet<>());
  11. public static void main(String[] args) throws InterruptedException
  12. {
  13. ExecutorService executorService = Executors.newFixedThreadPool(10);
  14. for (int i = 0; i < 100; i++)
  15. {
  16. int finalI = i;
  17. executorService.execute(() -> new ThreadLocalDemo3().date(finalI * 10));
  18. }
  19. executorService.shutdown();
  20. Thread.sleep(3000); // 确保线程全部执行完
  21. System.out.println("set总数: " + dates.size());
  22. }
  23. private void date(int seconds)
  24. {
  25. Date date = new Date(seconds * 1000); // 从1970-1-1开始
  26. SimpleDateFormat sdf = sdfThreadLocal.get();
  27. String s = sdf.format(date);
  28. System.out.println(Thread.currentThread().getName() + " = " + s);
  29. dates.add(s);
  30. sdfThreadLocal.remove();
  31. }
  32. }
  33. 运行结果
  34. pool-1-thread-10 = 1970-01-01 08:14:20
  35. pool-1-thread-6 = 1970-01-01 08:16:10
  36. pool-1-thread-3 = 1970-01-01 08:14:30
  37. pool-1-thread-4 = 1970-01-01 08:14:10
  38. pool-1-thread-7 = 1970-01-01 08:14:00
  39. pool-1-thread-2 = 1970-01-01 08:13:50
  40. pool-1-thread-6 = 1970-01-01 08:16:30
  41. pool-1-thread-10 = 1970-01-01 08:16:20
  42. pool-1-thread-9 = 1970-01-01 08:16:00
  43. pool-1-thread-1 = 1970-01-01 08:15:50
  44. pool-1-thread-8 = 1970-01-01 08:15:30
  45. pool-1-thread-5 = 1970-01-01 08:15:20
  46. set总数: 100

从运行结果可以发现,使用了ThreadLocal后,SimpleDateFormat线程安全的问题被解决了。

建议使用Java8的日期时间类和对应的线程安全的DateTimeFormat类

ThreadLocal在场景二中的使用

比如我们有这么一个类,它需要获取一个用户信息,然后执行一系列的业务逻辑操作,这些逻辑操作分散在各个方法中。

如果不使用一个全局变量,就得对每一个方法进行参数传递,而使用全局变量后的代码如下:

  1. /**
  2. * @author: Durian
  3. * @date: 2020/9/3 17:41
  4. * @description: 场景二的使用,每个线程内需要保存全局变量
  5. */
  6. public class ThreadLocalDemo4
  7. {
  8. private static User user;
  9. public static void main(String[] args)
  10. {
  11. for (int i = 0; i < 10; i++)
  12. {
  13. new Thread(() ->
  14. {
  15. new Intercept().intercept();
  16. new Service1().intercept();
  17. }).start();
  18. }
  19. }
  20. // 模拟拦截器
  21. static class Intercept
  22. {
  23. public void intercept()
  24. {
  25. // 拦截器获取用户信息放入全局变量user中,方便操作
  26. user = new User(Math.random() * 10);
  27. }
  28. }
  29. // 业务类1
  30. static class Service1
  31. {
  32. public void intercept()
  33. {
  34. new Service2().process();
  35. new Service3().process();
  36. }
  37. }
  38. // 业务类2
  39. static class Service2
  40. {
  41. public void process()
  42. {
  43. System.out.println(user.getMoney());
  44. user.setMoney(Math.random() * 10);
  45. }
  46. }
  47. // 业务类3
  48. static class Service3
  49. {
  50. public void process()
  51. {
  52. System.out.println(user.getMoney());
  53. }
  54. }
  55. }
  56. @Data
  57. @AllArgsConstructor
  58. class User
  59. {
  60. private Double money;
  61. }
  62. 部分运行结果:
  63. 2.295436641359472
  64. 2.295436641359472
  65. 2.295436641359472
  66. 2.295436641359472
  67. 2.295436641359472
  68. 2.295436641359472
  69. 0.1597985241974753

可以看到运行结果有很多money是重复的,这是不符合预期的,因为user是全局变量,在多线程环境中对user的值进行操作会出现线程安全问题。这也就是ThreadLocal使用的第二种场景,每个线程内需要保存全局变量,可以让不同方法使用,避免参数不断传递的麻烦。

使用ThreadLocal后:

  1. /**
  2. * @author: Durian
  3. * @date: 2020/9/3 17:41
  4. * @description: 场景二的使用,每个线程内需要保存全局变量
  5. */
  6. public class ThreadLocalDemo5
  7. {
  8. public static void main(String[] args)
  9. {
  10. for (int i = 0; i < 10; i++)
  11. {
  12. new Thread(() ->
  13. {
  14. new Intercept().intercept();
  15. new Service1().intercept();
  16. }).start();
  17. }
  18. }
  19. // 模拟拦截器
  20. static class Intercept
  21. {
  22. public void intercept()
  23. {
  24. User user = new User(Math.random() * 10);
  25. // 将拦截器获取到的用户信息放入ThreadLocal中
  26. UserContextHolder.holder.set(user);
  27. }
  28. }
  29. // 业务类1
  30. static class Service1
  31. {
  32. public void intercept()
  33. {
  34. new Service2().process();
  35. new Service3().process();
  36. }
  37. }
  38. // 业务类2
  39. static class Service2
  40. {
  41. public void process()
  42. {
  43. System.out.println(UserContextHolder.holder.get().getMoney());
  44. // 使用ThreadLocal的get方法拿到User
  45. UserContextHolder.holder.get().setMoney(Math.random() * 10);
  46. }
  47. }
  48. // 业务类3
  49. static class Service3
  50. {
  51. public void process()
  52. {
  53. System.out.println(UserContextHolder.holder.get().getMoney());
  54. // 使用完成后remove掉user
  55. UserContextHolder.holder.remove();
  56. }
  57. }
  58. }
  59. // 表示持有User对象
  60. class UserContextHolder
  61. {
  62. // 在第二种场景下使用ThreadLocal是不需要先初始化的
  63. public static ThreadLocal<User> holder = new ThreadLocal<>();
  64. }

在引入ThreadLocal后,每个业务类的方法可以方便的通过UserContextHolder的静态属性holder拿到拦截器放入的User信息,不用再进行参数传递,也避免了使用static变量造成的线程安全问题。

ThreadLocal的原理

在ThreadLocal中有一个重要内部类ThreadLocalMap;而在每个Thread类中,拥有一个ThreadLocal.ThreadLocalMap类型的属性threadLocals,也就是说,每个线程都有属于自己的threadLocals,而ThreadLocalMap中有一个核心类Entry,Entry为key-value对,key中存储的是当前线程的ThreadLocal,value为要保持的线程的私有变量,也就是想要放入ThreadLocal的变量。它们的关系如下图所示:

ThreadLocal笔记 - 图2

每个线程的ThreadLocalMap拥有多个Entry,Entry的key是本线程的ThreadLocal,而Value可以有多个不同的值,这是为了保证在一个线程中可能会使用多个ThreadLocal来保持变量。

上面的分析可知,ThreadLocal的核心就时ThreadLocalMap,接下来看一看ThreadLocalMap的分析。

ThreadLocalMap解析

  1. static class ThreadLocalMap {
  2. /**
  3. * The initial capacity -- MUST be a power of two.
  4. */
  5. private static final int INITIAL_CAPACITY = 16;
  6. /**
  7. * The table, resized as necessary.
  8. * table.length MUST always be a power of two.
  9. */
  10. private Entry[] table;
  11. /**
  12. * The number of entries in the table.
  13. */
  14. private int size = 0;
  15. /**
  16. * The next size value at which to resize.
  17. */
  18. private int threshold; // Default to 0
  19. }

由上面可见在ThreadLocalMap中维护着tablesize以及threshold三个属性。table是一个Entry数组主要用来保存具体的数据,sizetable的大小,而threshold这表示当table中元素数量超过该值时,table就会扩容。了解了ThreadLocalMap的结构之后,我们就来看下其set方法吧。

  1. private void set(ThreadLocal<?> key, Object value) {
  2. Entry[] tab = table;
  3. int len = tab.length;
  4. int i = key.threadLocalHashCode & (len-1);
  5. for (Entry e = tab[i];
  6. e != null;
  7. e = tab[i = nextIndex(i, len)]) {
  8. ThreadLocal<?> k = e.get();
  9. if (k == key) {
  10. e.value = value;
  11. return;
  12. }
  13. if (k == null) {
  14. replaceStaleEntry(key, value, i);
  15. return;
  16. }
  17. }
  18. tab[i] = new Entry(key, value);
  19. int sz = ++size;
  20. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  21. rehash();
  22. }

通过上面的代码分析得出,整个的设值过程如下:

  1. 通过ThreadLocal的threadLocalHashCode值定位到table中的位置i。
  2. 如果table中i这个位置是空的,那么就新创建一个Entry对象放置在i这个位置。
  3. 如果table中i这个位置不为空,则取出来i这个位置的key。
  4. 如果这个key刚好就是当前ThreadLocal对象,则直接修改该位置上Entry对象的value。
  5. 如果这个key不是当前TreadLocal对象,则寻找下一个位置的Entry对象,然后重复上述步骤进行判断(ThreadLocalMap处理hash冲突的策略时线性探测)。

对于get方法也是同样的原理从ThreadLocalMap中获取值。那么ThreadLocal是如何生成threadLocalHashCode值的呢?

  1. public class ThreadLocal<T> {
  2. private final int threadLocalHashCode = nextHashCode();
  3. private static final int HASH_INCREMENT = 0x61c88647;
  4. private static int nextHashCode() {
  5. return nextHashCode.getAndAdd(HASH_INCREMENT);
  6. }
  7. }
  8. 复制代码

可见我们在初始化一个ThreadLocal对象的时候都为其会生成一个threadLocalHashCode值,每初始化一个ThreadLocal该值就增加0x61c88647。这样就可以做到每个ThreadLocal在ThreadLocalMap中找到一个存储值的位置了。

ThreadLocal重要方法介绍

protected T initialValue()

ThreadLocal的初始化方法

  1. protected T initialValue() {
  2. return null;
  3. }

该方法会放回当前线程对应的初始值,这是一个延迟加载的方法,只有在调用get()方法时,才会触发。

当线程第一次使用get方法访问变量时,将触发此方法,如果线程之前调用了set方法,则不会为再为该线程调用initiaValue方法。

通常,每个线程只会调用一次initiaValue方法,除非再调用remove()后再次调用get方法,则会再次调用该方法。

如果在创建ThreadLocal时就想要初始化,需要使用内部类的方式实现initiaValue方法。在上文的第一种使用场景中就是这样做的,但是使用的是withInitial方法,该方法作用与initiaValue一致,但可用lambda简化初始化代码。

void set(T value)

为这个线程设置一个值,会覆盖已有的值

  1. public void set(T value) {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t); // 获取该线程的ThreadLocalMap
  4. if (map != null)
  5. map.set(this, value); // 如果map != null 将值设置到对应ThreadLocal的value
  6. else
  7. createMap(t, value); // 如果map == null,创建一个ThreadLocalMap再将值设置进去
  8. }

T get()

获取该线程中的值

  1. public T get() {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null) {
  5. ThreadLocalMap.Entry e = map.getEntry(this); // 获取当前ThreadLocal对应的Entry
  6. if (e != null) {
  7. @SuppressWarnings("unchecked")
  8. T result = (T)e.value;
  9. return result; // 返回获得的对应ThreadLocal的value
  10. }
  11. }
  12. return setInitialValue(); // 如果map == null 调用初始化方法,返回重写方法的返回值
  13. }

void remve()

删除对应线程中的值

  1. public void remove() {
  2. ThreadLocalMap m = getMap(Thread.currentThread());
  3. if (m != null)
  4. m.remove(this); // 移除对应ThreadLocal的值,不会删除线程中其他ThreadLocal的
  5. }

可以看到,不管是get、set和remove最终调用的方法都是map的get、set和remove方法。

ThreadLocal的使用注意点

内存泄露问题

每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的就可能出现内存泄露 。(在web应用中,每次http请求都是一个线程,tomcat容器配置使用线程池时会出现内存泄漏问题)

ThreadLocal笔记 - 图3

  • ThreadLocalMap的每个Entry都是一个对key的弱引用(弱引用将会在发生GC的时候被回收),同时,每个Entry都包含了一个强引用的value,。

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

  • 但是如果线程不被终止(线程持续很久)那么key对应的value就不会被回收,因为调用链还存在,如下图

ThreadLocal笔记 - 图4

  • 因为Thread和value之间的强引用还存在,导致value无法被回收,可能出现OOM。

  • JDK中的处理是,在set、remove、rehash方法中会扫描key为null的Entry,并把对应的value设置为nul,这样value的对象就可以被回收。 ThreadLocal笔记 - 图5

  • 但是如果一个ThreadLocal不被使用,那么实际上set、remove、rehash方法也不会被调用,同时线程又不停止,那么调用链会一直存在,将导致value的内存泄露。

ThreadLocal只是操作Thread中的ThreadLocalMap,每个Thread都有一个map,ThreadLocalMap是线程内部属性,ThreadLocalMap生命周期是和Thread一样的,不依赖于ThreadLocal。

ThreadLocal通过Entry保存在map中,key为Thread的弱引用(GC时会自动回收),value为存入的变量副本,一个线程不管有多少个ThreadLocal,都是通过一个ThreadLocalMap来存放局部变量的,可以再源码中看到,set值时先获取map对象,如果不存在则创建,threadLocalMap初始大小为16,当容量超过2/3时会自动扩容。

避免线value内存露方法

  • 调用remove方法,就会删除对应的Entry对象,可以避免内存泄露,所以应该在每次使用完ThreadLocal后,调用remove方法。