https://mp.weixin.qq.com/s/SEzGM5wPyL066Y3fWAeWjA

1.为什么使用ThreadLocal。

为了解决线程安全问题,JDK出现了很多技术手段,比如:使用synchronized或Lock,给访问公共资源的代码上锁,保证了代码的原子性。
但在高并发的场景中,如果多个线程同时竞争一把锁,这时会存在大量的锁等待,可能会浪费很多时间,让系统的响应时间一下子变慢。
因此,JDK提供了另外一种用空间换时间的新思路:ThreadLocal。核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。

  1. @Service
  2. public class ThreadLocalService {
  3. private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
  4. public void add() {
  5. //将数据设置到当前线程中
  6. threadLocal.set(1);
  7. doSamething();
  8. //从当前线程中获取数据
  9. Integer integer = threadLocal.get();
  10. }
  11. }

2.ThreadLocal原理

ThreadLocal的内部有一个静态的内部类叫:ThreadLocalMap。

  1. public class ThreadLocal<T> {
  2. ...
  3. public T get() {
  4. //获取当前线程
  5. Thread t = Thread.currentThread();
  6. //获取当前线程的成员变量ThreadLocalMap对象
  7. //return t.threadLocals; ThreadLocal.ThreadLocalMap threadLocals = null;
  8. ThreadLocalMap map = getMap(t);
  9. if (map != null) {
  10. //根据threadLocal对象从map中获取Entry对象
  11. ThreadLocalMap.Entry e = map.getEntry(this);
  12. if (e != null) {
  13. @SuppressWarnings("unchecked")
  14. //获取保存的数据
  15. T result = (T)e.value;
  16. return result;
  17. }
  18. }
  19. //初始化数据
  20. return setInitialValue();
  21. }
  22. private T setInitialValue() {
  23. //获取要初始化的数据
  24. T value = initialValue();
  25. //获取当前线程
  26. Thread t = Thread.currentThread();
  27. //获取当前线程的成员变量ThreadLocalMap对象
  28. ThreadLocalMap map = getMap(t);
  29. //如果map不为空
  30. if (map != null)
  31. //将初始值设置到map中,key是this,即threadLocal对象,value是初始值
  32. map.set(this, value);
  33. else
  34. //如果map为空,则需要创建新的map对象
  35. createMap(t, value);
  36. return value;
  37. }
  38. public void set(T value) {
  39. //获取当前线程
  40. Thread t = Thread.currentThread();
  41. //获取当前线程的成员变量ThreadLocalMap对象
  42. ThreadLocalMap map = getMap(t);
  43. //如果map不为空
  44. if (map != null)
  45. //将值设置到map中,key是this,即threadLocal对象,value是传入的value值
  46. map.set(this, value);
  47. else
  48. //如果map为空,则需要创建新的map对象
  49. createMap(t, value);
  50. }
  51. static class ThreadLocalMap {
  52. ...
  53. }
  54. ...
  55. }

ThreadLocal的get方法、set方法和setInitialValue方法,其实最终操作的都是ThreadLocalMap类中的数据。

  1. static class ThreadLocalMap {
  2. static class Entry extends WeakReference<ThreadLocal<?>> {
  3. Object value;
  4. //k为弱引用
  5. Entry(ThreadLocal<?> k, Object v) {
  6. super(k);
  7. value = v;
  8. }
  9. }
  10. ...
  11. private Entry[] table;
  12. ...
  13. }

ThreadLocalMap里面包含一个静态的内部类Entry,该类继承于WeakReference类,说明Entry是一个弱引用
ThreadLocalMap内部还包含了一个Entry数组,其中:Entry = ThreadLocal+value。
ThreadLocalMap被定义成了Thread类的成员变量
image.png
在每个Thread类中,都有一个ThreadLocalMap的成员变量,该变量包含了一个Entry数组,该数组真正保存了ThreadLocal类set的数据。
Entry是由threadLocal和value组成,其中threadLocal对象是弱引用,在GC的时候,会被自动回收。而value就是ThreadLocal类set的数据。
image.png
除了Entry的key对ThreadLocal对象是弱引用,其他的引用都是强引用。
需要特别说明的是,上图中ThreadLocal对象画到了堆上,在实际的业务场景中不一定在堆上。如果ThreadLocal被定义成了static的,ThreadLocal的对象是类共用的,可能出现在方法区。

3.为什么使用ThreadLocal作为key。

在应用中,如果一个线程只使用一个ThreadLocal对象,那么使用Thread做key也未尝不可。
实际情况中,一个线程中很可能使用多个ThreadLocal对象。如果使用Thread做key就有问题。因此,不能使用Thread做key,而应该改成用ThreadLocal对象做key,这样才能通过具体ThreadLocal对象的get方法,轻松获取到想要的ThreadLocal对象。
image.png

4.Entry的key为什么是弱引用。

Entry的key,传入的是ThreadLocal对象,使用了WeakReference对象,即被设计成了弱引用。
假设设计为强引用
image.png
由上图可以知,ThreadLocal对象有两个强引用(ThreadLocal变量、key)。导致后果,即使ThreadLocal变量设置为null,key仍然对ThreadLocal对象保持引用。ThreadLocal无法被释放。
强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。
ThreadLocal对象和ThreadLocalMap都将不会被GC回收,于是产生了内存泄露问题。
为解决,ThreadLocal变量生命周期结束后(即ThreadLocal变量赋值为null),ThreadLocal对象可以被回收,采用弱引用。
key是弱引用,当ThreadLocal变量指向null之后,在GC做垃圾清理的时候,key会被自动回收,其值也被设置成null。
image.png

需要特别注意的地方是

  1. key为null的条件是,ThreadLocal变量指向null,并且key是弱引用。如果ThreadLocal变量没有断开对ThreadLocal的强引用,即ThreadLocal变量没有指向null,GC不会把弱引用的key回收。
  2. 如果当前ThreadLocal变量指向null了,并且key也为null了,但如果没有其他ThreadLocal变量触发get、set或remove方法,也会造成内存泄露。 ```java public static void main(String[] args) { Object object = new Object(); WeakReference weakReference1 = new WeakReference<>(object); System.out.println(weakReference1.get()); System.gc(); System.out.println(weakReference1.get());

    object=null; System.gc(); System.out.println(weakReference1.get()); }

    java.lang.Object@6f496d9f java.lang.Object@6f496d9f null

    1. 由此可见,如果强引用和弱引用同时关联一个对象,那么对象是不会被GC回收。也就是说这种情况下Entrykey,一直都不会为null,除非强引用主动断开引用。<br />**Entryvalue为什么不设计成弱引用?**<br />value只可能只是被Entry引用,有可能没被其他变量引用。如果将value改成了弱引用,有被GC贸然回收的风险,可能会导致业务系统出现异常。
    2. <a name="eQpqg"></a>
    3. # 5.ThreadLocal仍然有内存泄露风险?
    4. Entry对象中的key设置成弱引用,并且使用getsetremove方法清理keynullvalue值,**并不能彻底解决内存泄露问题。**<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/13013491/1654498896811-83db7fb3-fb04-4e75-a75e-77c8a593a3a1.png#clientId=u99f051fc-1a34-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=247&id=ucda199d8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=370&originWidth=1003&originalType=binary&ratio=1&rotation=0&showTitle=false&size=345162&status=done&style=none&taskId=u3b94465b-c767-4cb3-8a59-e17c7135639&title=&width=668.6666666666666)<br />假如ThreadLocalMap中存在很多key为null的Entry,但后面的程序,一直都没有调用过有效的ThreadLocal的get、set或remove方法。<br />那么,Entry的value值一直都没被清空。<br />就会存在这样一条强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> value -> Object。<br />其结果就是:Entry和ThreadLocalMap将会长期存在下去,会导致内存泄露
    5. <a name="c6y91"></a>
    6. # 6.如何解决内存泄露问题?
    7. 使用完ThreadLocal对象之后,调用ThreadLocal对象的remove方法。<br />remove方法中会把Entry中的keyvalue都设置成null,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。
    8. <a name="BqlDr"></a>
    9. # 7. ThreadLocal是如何定位数据的?
    10. ThreadLocalMap对象底层是用Entry数组保存数据的,如何定位Entry数组数据的?<br />getsetremove中,使用int i = key.threadLocalHashCode & (len-1);定位。如果hash冲突,则向后移动一个槽位。
    11. <a name="OsSSI"></a>
    12. # 8.父子线程如何共享数据?
    13. ThreadLocal都是在一个线程中保存和获取数据的。实际工作中,有可能是在父子线程中共享数据的。即在父线程中往ThreadLocal设置了值,在子线程中能够获取到。<br />**需要使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类**。<br />在它的init方法中会将父线程中往ThreadLocal设置的值,拷贝一份到子线程中。<br />存在一个缺陷:当父线程修改数值后,子线程感知不到变化。
    14. <a name="Pd89g"></a>
    15. # 9.线程池共享数据。
    16. 使用TransmittableThreadLocal
    17. ```xml
    18. <dependency>
    19. <groupId>com.alibaba</groupId>
    20. <artifactId>transmittable-thread-local</artifactId>
    21. <version>2.11.0</version>
    22. <scope>compile</scope>
    23. </dependency>

    TransmittableThreadLocal与TtlExecutors.getTtlExecutorService配合使用。

    1. public void run() {
    2. Map<TransmittableThreadLocal<?>, Object> copied = (Map)this.copiedRef.get();
    3. if (copied != null && (!this.releaseTtlValueReferenceAfterRun || this.copiedRef.compareAndSet(copied, (Object)null))) {
    4. Map backup = TransmittableThreadLocal.backupAndSetToCopied(copied);
    5. try {
    6. this.runnable.run();
    7. } finally {
    8. TransmittableThreadLocal.restoreBackup(backup);
    9. }
    10. } else {
    11. throw new IllegalStateException("TTL value reference is released after run!");
    12. }
    13. }
    1. 把当时的ThreadLocal做个备份,然后将父类的ThreadLocal拷贝过来。
    2. 执行真正的run方法,可以获取到父类最新的ThreadLocal数据。
    3. 从备份的数据中,恢复当时的ThreadLocal数据。

      10.ThreadLocal用途?

    4. 在spring事务中,保证一个线程下,一个事务的多个操作拿到的是一个Connection。

    5. 在hiberate中管理session。
    6. 在JDK8之前,为了解决SimpleDateFormat的线程安全问题。
    7. 获取当前登录用户上下文。
    8. 临时保存权限数据。
    9. 使用MDC保存日志信息。