1、ThreadLocal的使用场景

典型场景1

每个线程需要独享的对象(通常是工具类,如SimpleDateFormat和Random)

每个线程内拥有自己的实例副本,不共享

  1. package com.imooc.thread_demo.threadlocal;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Getter;
  4. import java.text.ParseException;
  5. import java.text.SimpleDateFormat;
  6. import java.util.Date;
  7. import java.util.HashMap;
  8. import java.util.Map;
  9. import java.util.concurrent.ExecutorService;
  10. import java.util.concurrent.Executors;
  11. import java.util.stream.IntStream;
  12. /**
  13. * @Author: zhangjx
  14. * @Date: 2020/10/12 20:07
  15. * @Description:
  16. */
  17. public class SimpleDateFormatThreadLocalUtil {
  18. @AllArgsConstructor
  19. @Getter
  20. enum SdfEnums{
  21. UTC_FORMAT("yyyy-MM-dd'T'HH:mm:ss'Z'"),
  22. CST_FORMAT("yyyy-MM-dd HH:mm:ss"),
  23. DATE_FORMAT("yyyy-MM-dd");
  24. private String sdf;
  25. }
  26. /**
  27. * 存放不同的日期模板格式的sdf的Map
  28. */
  29. private volatile static Map<String, ThreadLocal<SimpleDateFormat>> sdfMap = new HashMap<>();
  30. private static SimpleDateFormat getSdf(final String pattern) {
  31. ThreadLocal<SimpleDateFormat> sdf = sdfMap.get(pattern);
  32. if (sdf == null) {
  33. synchronized (SimpleDateFormatThreadLocalUtil.class) {
  34. sdf = sdfMap.get(pattern);
  35. if (sdf == null) {
  36. sdf = ThreadLocal.withInitial(() -> new SimpleDateFormat(pattern));
  37. sdfMap.put(pattern, sdf);
  38. }
  39. }
  40. }
  41. return sdf.get();
  42. }
  43. public static String format(Date date, SdfEnums sdfEnums) {
  44. return getSdf(sdfEnums.getSdf()).format(date);
  45. }
  46. public static Date parse(String dateStr, SdfEnums sdfEnums){
  47. try {
  48. return getSdf(sdfEnums.getSdf()).parse(dateStr);
  49. } catch (ParseException e) {
  50. e.printStackTrace();
  51. return null;
  52. }
  53. }
  54. public static void main(String[] args) {
  55. ExecutorService executorService = Executors.newFixedThreadPool(10);
  56. IntStream.range(0, 1000).forEach(e -> {
  57. executorService.execute(new Runnable() {
  58. @Override
  59. public void run() {
  60. try {
  61. Thread.sleep(1000);
  62. } catch (InterruptedException e) {
  63. e.printStackTrace();
  64. }
  65. System.out.println(Thread.currentThread().getName() + " :" + format(new Date(), SdfEnums.CST_FORMAT));
  66. }
  67. });
  68. });
  69. }
  70. }

典型场景2

每个线程需要保存全局变量(例如在拦截器中获取用户信息),避免参数传递

  1. public class UserContextHolder {
  2. public static final ThreadLocal<Long> USER_HOLDER = new InheritableThreadLocal<>();
  3. public static void setUserId(Long userId){
  4. USER_HOLDER.set(userId);
  5. }
  6. public static Long getUserId(){
  7. return USER_HOLDER.get();
  8. }
  9. public static void remove(){
  10. USER_HOLDER.remove();
  11. }
  12. }

2、使用ThreadLocal的好处

  • 达到线程安全

  • 不需要加锁,提高执行效率

  • 更高效的利用内存,节省开销

  • 避免繁琐的传参

3、ThreadLocal原理

ThreadLocal提供了一种方式,可以让线程在操作共享变量时,复制该共享变量的一个副本到线程自己的栈空间,以后就操作这个副本空间来代替共享空间。

Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系
image.png

每个 Thread 对象中都持有一个 ThreadLocalMap 类型的成员变量,ThreadLocalMap 自身类似于是一个 Map,里面会有一个个 key value 形式的键值对,key 就是 ThreadLocal 的引用,value是我们希望 ThreadLocal 存储的内容,例如 user 对象等。

ThreadLocal源码

  1. public T get() {
  2. Thread t = Thread.currentThread();
  3. //获取到当前线程内的 ThreadLocalMap 对象
  4. ThreadLocalMap map = getMap(t);
  5. if (map != null) {
  6. ThreadLocalMap.Entry e = map.getEntry(this);
  7. if (e != null) {
  8. @SuppressWarnings("unchecked")
  9. T result = (T)e.value;
  10. return result;
  11. }
  12. }
  13. return setInitialValue();
  14. }
  15. /**
  16. *返回当前线程对应的初始值 延时加载
  17. */
  18. private T setInitialValue() {
  19. T value = initialValue();
  20. Thread t = Thread.currentThread();
  21. ThreadLocalMap map = getMap(t);
  22. if (map != null)
  23. map.set(this, value);
  24. else
  25. createMap(t, value);
  26. return value;
  27. }
  28. /**
  29. * 获取当前线程的ThreadLocalMap,将当前ThreadLocal对象作为key,存储信息作为value存储进map
  30. */
  31. public void set(T value) {
  32. Thread t = Thread.currentThread();
  33. ThreadLocalMap map = getMap(t);
  34. if (map != null)
  35. map.set(this, value);
  36. else
  37. createMap(t, value);
  38. }
  39. ThreadLocalMap getMap(Thread t) {
  40. return t.threadLocals;
  41. }

ThreadLocalMap源码

成员变量

  1. /**
  2. * 初始容量 —— 必须是2的冥
  3. * The initial capacity -- MUST be a power of two.
  4. */
  5. private static final int INITIAL_CAPACITY = 16;
  6. /**
  7. * 存放数据的table,数组长度必须是2的冥
  8. * The table, resized as necessary.
  9. * table.length MUST always be a power of two.
  10. */
  11. private Entry[] table;
  12. /**
  13. * 数组里面entrys的个数,可以用于判断table当前使用量是否超过负因子
  14. * The number of entries in the table.
  15. */
  16. private int size = 0;
  17. /**
  18. * 进行扩容的阈值,表使用量大于它的时候进行扩容。
  19. * The next size value at which to resize.
  20. */
  21. private int threshold; // Default to 0
  22. /**
  23. * 扩容的阙值设置为数组长度的2/3
  24. * Set the resize threshold to maintain at worst a 2/3 load factor.
  25. */
  26. private void setThreshold(int len) {
  27. threshold = len * 2 / 3;
  28. }

存储结构——Entry

  1. static class Entry extends WeakReference<ThreadLocal<?>> {
  2. /** The value associated with this ThreadLocal. */
  3. Object value;
  4. Entry(ThreadLocal<?> k, Object v) {
  5. super(k);
  6. value = v;
  7. }
  8. }

Entry继承WeakReference,使用弱引用,可以将ThreadLocal对象的生命周期和线程生命周期解绑,持有对ThreadLocal的弱引用,可以使得ThreadLocal在没有其他强引用的时候被回收掉,这样可以避免因为线程得不到销毁导致ThreadLocal对象无法被回收。

4、 ThreadLocal使用注意点

4.1 内存泄漏问题

ThreadLocal可能引起内存泄漏问题,ThreadLocalMap中Entry的key是弱引用,在下一次垃圾回收时能够被gc,但是值为强引用,无法被gc,如果线程被复用一直运行则value无法释放,JDK已经考虑到了这个问题在set,remove,rehash方法中会扫描key为null的Entry,并把对应的value设置为null,从而使value对象可以被回收。
但是如果一个ThreadLocal不再被使用,不再调用set,remove,rehash方法,同时线程不中止,那么调用链一直存在,导致value内存泄漏,阿里规约规定使用ThreadLocal完毕后必须释放。

  1. private void resize() {
  2. Entry[] oldTab = table;
  3. int oldLen = oldTab.length;
  4. int newLen = oldLen * 2;
  5. Entry[] newTab = new Entry[newLen];
  6. int count = 0;
  7. for (int j = 0; j < oldLen; ++j) {
  8. Entry e = oldTab[j];
  9. if (e != null) {
  10. ThreadLocal<?> k = e.get();
  11. if (k == null) {
  12. e.value = null; // Help the GC
  13. } else {
  14. int h = k.threadLocalHashCode & (newLen - 1);
  15. while (newTab[h] != null)
  16. h = nextIndex(h, newLen);
  17. newTab[h] = e;
  18. count++;
  19. }
  20. }
  21. }
  22. setThreshold(newLen);
  23. size = count;
  24. table = newTab;
  25. }

4.2 空指针问题

如果没有set直接get会返回null,但是如果存的使包装类,返回的基本类型,会有自动拆箱的过程,会空指针异常。

如下get方法就有空指针异常可能

  1. public class UserContextHolder {
  2. public static final ThreadLocal<Long> USER_HOLDER = new InheritableThreadLocal<>();
  3. public static void setUserId(Long userId){
  4. USER_HOLDER.set(userId);
  5. }
  6. public static long getUserId(){
  7. return USER_HOLDER.get();
  8. }
  9. public static void remove(){
  10. USER_HOLDER.remove();
  11. }
  12. }

4.3 共享对象问题

如果ThreadLoacl存放的本身就是共享对象,如static对象,那么多线程ThreadLocal.get()还是获取的这个共享对象本身,依然存在并发访问问题。

5、Spring中的ThreadLocal实例

  • DateTimeContextHolder

  • RequestContextHolder