并发编程基础_03

举报
kwan的解忧杂货铺 发表于 2024/08/07 21:25:45 2024/08/07
【摘要】 三.ThreadLocal 介绍 1.说说 ThreadLocal?ThreadLocal 是 Java 中的一个类,用于在多线程环境下维护线程本地变量。在 Java 中,每个线程都有自己的线程栈,用于存储局部变量,而 ThreadLocal 提供了一种线程级别的变量,每个线程都可以独立地访问和修改它,而不会影响其他线程的访问。 2.ThreadLocal 要点使用 ThreadLocal...

三.ThreadLocal 介绍

1.说说 ThreadLocal?

ThreadLocal 是 Java 中的一个类,用于在多线程环境下维护线程本地变量。在 Java 中,每个线程都有自己的线程栈,用于存储局部变量,而 ThreadLocal 提供了一种线程级别的变量,每个线程都可以独立地访问和修改它,而不会影响其他线程的访问。

2.ThreadLocal 要点

使用 ThreadLocal 有以下几个要点:

  1. 独立副本:每个 ThreadLocal 对象在每个线程中都会保存一个独立的副本。当线程访问 ThreadLocal 对象时,实际上是在操作当前线程自己的副本,因此线程之间互不干扰。
  2. 初始值:ThreadLocal 提供了初始值的设置,可以通过覆盖initialValue()方法来设置初始值。当线程第一次访问 ThreadLocal 对象时,如果没有设置初始值,initialValue()方法将被调用来提供默认值。
  3. 内存泄漏:使用 ThreadLocal 时要注意内存泄漏的问题。由于 ThreadLocal 中的对象只在当前线程中有效,如果线程一直存在而 ThreadLocal 没有被清理,那么其中的对象也不会被释放,可能导致内存泄漏。因此,在不再需要使用 ThreadLocal 时,应当调用remove()方法来清理其中的对象,或者使用 Java 8 引入的ThreadLocal.remove()方法。
  4. 用途:ThreadLocal 在某些场景下非常有用,特别是在多线程环境下需要保存线程特有的状态或数据时。常见的应用场景包括线程池、Web 应用的用户会话信息、数据库连接管理等。

3.ThreadLocal 的原理?

Thread 类中有一个 threadLocals 和一个 inheritableThreadLocals,它们都是 ThreadLocalMap 类型的变量,而 ThreadLocalMap 是一个定制化的 HashMap.在默认情况下,每个线程中的这两个变量都为 null,只有当前线程第一次调用 ThreadLocal 的 set 或者 get 方法时才会创建它们.

其实每个线程的本地变量不是存放在 ThreadLocal 实例里面,而是存放在调用线程的 threadLocals 变量里面.也就是说,ThreadLocal 类型的本地变量存放在具体的线程内存空间中。ThreadLocal 就是一个工具壳,它通过 set 方法把 value 值放入调用线程的 threadLocals 里面并存放起来,当调用线程调用它的 get 方法时,再从当前线程的 threadLocals 变量里面将其拿出来使用.如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的 threadLocals 变量里面,所以当不需要使用本地变量时可以通过调用 ThreadLocal 变量的 remove 方法,从当前线程的 threadLocals 里面删除该本地变量。

Thread 里面的 threadLocals 为何被设计为 map 结构,很明显是因为每个线程可以关联多个 ThreadLocal 变量

在 Thread 类中有以下变量

ThreadLocalMap 是 ThreadLocal 的一个静态内部类.

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class.*/
 ThreadLocal.ThreadLocalMap threadLocals = null;

/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
* Inheritable 可继承的
*/
 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  • ThreadLocal 中在 set 操作时,key 为当前 ThreadLocal 对象。
  • ThreadLocal 会为每个线程都创建一个 ThreadLocalMap,对应程序中的 t.threadLocals = new ThreadLocalMap(this, firstValue),ThreadLocalMap 为当前线程的属性。
  • 通过对每个线程创建一个 ThreadLocalMap 实现本地副本。当取值时,实际上就是通过 key 在 map 中取值,当然此时的 key 为 ThreadLocal 对象,而 map 为每个线程独有的 map,从而实现变量的互不干扰。

4.ThreadLocal 中 set 方法?

public class ThreadLocalTest {
  public static void main(String[] args){
    ThreadLocal<String> t1 = new ThreadLocal<>();
    t1.set("1");
    t1.set("2");
    System.out.println(t1.get());
  }
}
//输出结果 2

看下 set 方法的源代码:先获取当前线程 t,然后以 t 为 key 获取当前 ThreadLocalMap.如果 Map 存在则设置,注意设置的 key 为 this,this 代表当前对象,key 不变,所以 value 会被覆盖.如果 map 不存在则进行 createMap。

public void set(T value){
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null){
    map.set(this, value);
  } else {
    createMap(t, value);
  }
}

void createMap(Thread t, T firstValue){
  t.threadLocals = new ThreadLocalMap(this, firstValue);
}

5.ThreadLocal 继承?

public class ThreadLocalTest2 {
  public static void main(String[] args){
    ThreadLocal<String> t1 = new ThreadLocal<>();
    t1.set("1");
    new Thread(new Runnable(){
      @Override
      public void run(){
        System.out.println("我是子线程,t1:"+ t1.get());
      }
    }).start();
    System.out.println("我是主线程,t1:"+ t1.get());
  }
}
//我是主线程,t1:1
//我是子线程,t1:null

也就是说,同一个 ThreadLocal 变量在父线程中被设置值后,在子线程中是获取不到的。根据之前题目的线程私有的介绍,这应该是正常现象,因为在子线程 thread 里面调用 get 方法时当前线程为 thread 线程,而这里调用 set 方法设置线程变量的是 main 线程,两者是不同的线程,自然子线程访问时返回 null.

6.子线程访问主线程变量?

有没有办法让子线程访问到主线程的 ThreadLocal 变量

InheritableThreadLocal 继承自 ThreadLocal,其提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量.下面看一下 InheritableThreadLocal 的代码。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
  protected T childValue(T parentValue){
    return parentValue;
  }
  ThreadLocalMap getMap(Thread t){
    return t.inheritableThreadLocals;
  }
  void createMap(Thread t, T firstValue){
    t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
  }
}

这是 InheritableThreadLocal 的全部代码,他继承了 ThreadLocal,并复写了三个方法.

  • 一个是 getMap,获取一个新的 map,
  • 一个是 ceateMap,创建一个新的 ThreadLocalMap,并赋值给 inheritableThreadLocals
  • 一个是 childValue,返回父线程的值

7.InheritableThreadLocal?

InheritableThreadLocal 是如何让子线程可以访问在父线程中设置的本地变量的

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
  protected T childValue(T parentValue){
    return parentValue;
  }
  ThreadLocalMap getMap(Thread t){
    return t.inheritableThreadLocals;
  }
  void createMap(Thread t, T firstValue){
    t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
  }
}

从代码上看,getMapcreateMap 没什么稀奇的,无非是创建和获取.这不是原理所在。除了 getMapcreateMap,只能来看看 childValue 这个方法了.我们看到代码逻辑是 return parentValue;为了说清楚 childValue 这个方法,我们得先看 ThreadLocalMap 构造方法:从代码可以看出,初始化的时候进行了判断,如果父类的 inheritableThreadLocals 不为空,则进行 createInheriteMap 方法创建,继续点进去看.发现在该函数内部把父线程 inhritblThreadLocal 的值复制到新的 ThreadLocalMap 对象

private ThreadLocalMap(ThreadLocalMap parentMap){
  Entry[] parentTable = parentMap.table;
  int len = parentTable.length;
  setThreshold(len);
  table = new Entry[len];

  for (int j = 0; j < len; j++){
    Entry e = parentTable[j];
    if (e != null){
      @SuppressWarnings("unchecked")
      ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
      if (key != null){
        //重点此处调用了 childValue,返回 parent的value,
        //在该函数内部把父线程 inhritblThreadLocal 的值复制到新的 ThreadLocalMap 对象
        Object value = key.childValue(e.value);
        Entry c = new Entry(key, value);
        int h = key.threadLocalHashCode &(len -1);
        while (table[h]!= null)
          h = nextIndex(h, len);
        table[h]= c;
        size++;
      }
    }
  }
}

总结:InheritableThreadLocal 类通过重写代码.getMap 和 createMap 让本地变量保存到了具体线程的 inheritableThreadLocals 变量里面,那么线程在通过 InheritableThreadLocal 类实例的 set 或者 get 方法设置变量时,就会创建当前线程的 inheritableThreadLocals 变量.当父线程创建子线程时,构造函数会把父线程中 inheritableThreadLocals 变量里面的本地变量复制一份保存到子线程的 inheritableThreadLocals 变量里面。

public class InheritableThreadLocalTest {

  public static void main(String[] args){
    InheritableThreadLocal<String> t1 = new InheritableThreadLocal<>();
    t1.set("1");
    new Thread(new Runnable(){
      @Override
      public void run(){
        System.out.println("我是子线程,t1:"+ t1.get());
      }
    }).start();
    System.out.println("我是主线程,t1:"+ t1.get());
  }
}
//我是主线程,t1:1
//我是子线程,t1:1

说说 InheritableThreadLocal 的使用场景?

情况还是蛮多的,比如子线程需要使用存放在 ThreadLocal 变量中的用户登录信息,再比如一些中间件需要把统一的 id 追踪的整个调用链路记录下来.其实子线程使用父线程中的 ThreadLocal 方法有多种方式,比如创建线程时传入父线程中的变量,并将其复制到子线程中,或者在父线程中构造一个 map 作为参数传递给子线程,但是这些都改变了我们的使用习惯,所以在这些情况下 InheritableThreadLocal 就显得比较有用。

8.为什么不用 HashMap?

1.在设计 ThreadLocal 时,参考 jdk 都是鉴于效率性能优先。ThreadLocalMap 对 ThreadLocal 场景做了优化,这些场景是特定的,而不一定适用于原先的 HashMap 适用的场景。

ThreadLocalMap 是由一个个 Entry 键值对组成,key 是 ThreadLocal 对象,value 为线程变量的副本。每一个 Thread 都有一个 ThreadLocalMap 类型的成员变量 threadLocals ,它存储本线程中所 有 ThreadLocal 对象及其对应的值。并且他是一个 WeakReference 弱引用,当没有指向 key 的强引用后,该 key 就会被垃圾回收器回收。如果不是弱引用,那么因为这个 Map 的强引用导致这个线程的 ThreadLocalMap 对应的 ThreadLocal key 一直不能被回收。

2.减少哈希碰撞,如果你的程序需要多个 ThreadLocal,并且每个线程都使用了 这些 ThreadLocal。 Entry[] table 的大小一直是 2 的 n 次方,这样根据 Hash 值放入的时候,取余变成对于 2 的 n 次方 -1 取与运算。Hash 值计算是开放地址法,每新建一个 ThreadLocal 则将全局的 nextHashCode + 0x61c88647,这个魔法数保证了大部分情况下无论 Entry[] table 扩容到什么程度,都可以保证生成的 Hash 值 对于目前 table 大小的值 - 1 取与运算落入尽量不同的位置,减少哈希碰撞,增加效率。

那如果 ThreadLocal 发生内存泄漏,原因一定是弱引用吗?

ThreadLocal 发生内存泄漏的关键在于,ThreadLocalMap 和 Thread 有相同的生命周期,当外部没有强引用指向 ThreadLocal 时,在 ThreadLocalMap 里面的 key 就会被移除,而 value 还存在着强引用,只有当 Thread 退出线程后,value 的强引用才会断,如果线程一直不结束的话,j 这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链。

9.this 和 t 区别?

public void set(T value){
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null){
    map.set(this, value);
  } else {
    createMap(t, value);
  }
}

void createMap(Thread t, T firstValue){
  t.threadLocals = new ThreadLocalMap(this, firstValue);
}

thist都是对象引用,但是指向的对象不同。

this是一个关键字,表示当前对象的引用。在上述代码中,this指的是ThreadLocal对象,即当前正在调用set方法的ThreadLocal对象。

t是一个局部变量,代表当前线程的引用。在上述代码中,t通过Thread.currentThread()方法获取当前线程的引用。

因此,thist指向的是不同的对象,this指向ThreadLocal对象,t指向当前线程对象。

10.key 弱引用

ThreadLocalMap 的 Entry 中的 key 使用的是对 ThreadLocal 对象的弱引用,这在避免内存泄漏方面是一个进步,因为如果是强引用,即使其他地方没有对 ThreadLocal 对象的引用,ThreadLocalMap 中的 ThreadLocal 对象还是不会被回收,而如果是弱引用则 ThreadLocal 引用是会被回收掉的。

但是对应的 value 还是不能被回收,这时候 ThreadLocalMap 里面就会存在 key 为 null 但是 value 不为 null 的 entry 项,虽然 ThreadLocalMap 提供了 set、get 和 remove 方法,可以在一些时机下对这些 Entry 项进行清理,但是这是不及时的,也不是每次都会执行,所以在一些情况下还是会发生内存漏,因此在使用完毕后及时调用 remove 方法才是解决内存泄漏问题的王道。

创建一个 ThreadLocal 变量后,每个线程都会复制一个变量到自己的本地内存,如下图所示。

image-20231021174620487

11.TransmittableThreadLocal?

简介:

  • 解决线程池之间 ThreadLocal 本地变量传递的问题
  • 继承了 InheritableThreadLocal,是解决父子线程的关键
  • InheritableThreadLocal 解决父子线程的问题,它是在线程创建的时候进行复制上下文的。那么对于线程池的已经创建完了就无从下手了,所以在线程提交的时候要进行上下文的复制。这就是 TransmittableThreadLocal 想要解决的问题

InheritableThreadLocal 是在 new Thread 对象的时候复制父线程的对象到子线程的.是只有在创建的时候才拷贝,只拷贝一次,然后就放到线程中的 inheritableThreadLocals 属性缓存起来。由于使用了线程池,该线程可能会存活很久甚至一直存活,那么 inheritableThreadLocals 属性将不会看到父线程的本地变量的变化.

holder 这是一个 TTL 类型的对象,持有一个全局的 WeakMap(weakMap 的 key 是弱引用,同 TL 一样,也是为了解决内存泄漏的问题),里面存放了 TTL 对象 并且重写了 initialValue 和 childValue 方法,尤其是 childValue,可以看到在即将异步时父线程的属性是直接作为初始化值赋值给子线程的本地变量对象(TTL)的.

TransmittableThreadLocal 使用场景:

  • 分布式跟踪系统
  • 日志收集记录系统上下文
  • Session 级 Cache
  • 应用容器或上层框架跨应用代码给下层 SDK 传递信息

12.如何使用 ThreadLocal

public class ThreadLocalExample {
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>() {
        @Override
        protected Integer initialValue() {
            return 0; // 设置初始值为0
        }
    };

    public static void main(String[] args) {
        Runnable task = () -> {
            int value = threadLocal.get(); // 获取当前线程的ThreadLocal副本
            System.out.println(Thread.currentThread().getName() + ": " + value);

            threadLocal.set(value + 1); // 修改当前线程的ThreadLocal副本
        };

        // 创建并启动多个线程
        Thread thread1 = new Thread(task, "Thread-1");
        Thread thread2 = new Thread(task, "Thread-2");
        thread1.start();
        thread2.start();
    }
}

//输出结果
//Thread-1: 0
//Thread-2: 0

这表明每个线程都拥有独立的 ThreadLocal 副本,并且可以独立地进行读取和修改操作。

13.ThreadLocal 信息丢失原因

同一线程内部 threadlocal 信息丢失了 可能的原因有哪些?

在同一线程内部,ThreadLocal 信息丢失通常是由以下一些原因导致的:

  1. 未正确设置 ThreadLocal 值:最常见的原因是没有正确设置ThreadLocal的值或没有在需要的地方设置值。确保在每个线程中正确地调用set方法来设置ThreadLocal的值。

  2. 未正确清除 ThreadLocal 值:如果不需要的话,确保在使用完ThreadLocal值后,调用remove方法将其从当前线程中清除。如果不清除,可能会导致内存泄漏或数据污染。

  3. 线程池和线程重用:如果您在使用线程池时不小心共享了ThreadLocal变量,或者线程重用了ThreadLocal变量,可能导致线程间的ThreadLocal数据混乱。确保在线程池中适当地初始化和清除ThreadLocal变量。

  4. 异常或非正常流程:在某些情况下,如果线程在异常或非正常流程中退出,可能会导致ThreadLocal的数据丢失。确保您的代码在异常情况下能够正确清理ThreadLocal数据。

  5. 使用 ThreadLocal 的注意事项:在多线程编程中,ThreadLocal应该谨慎使用。不合理的使用可能导致资源泄漏、内存占用过高等问题。请确保只在需要在线程间传递数据的情况下使用它,而不是用它作为全局变量的替代品。

  6. 序列化问题ThreadLocal的默认实现在序列化时可能会导致数据丢失。如果需要在多个线程之间传递ThreadLocal数据,并且您的应用程序需要序列化,考虑使用InheritableThreadLocal或自定义ThreadLocal的子类来处理序列化问题。

ThreadLocal的信息丢失通常是由于不正确的使用或未考虑到特殊情况导致的。在使用ThreadLocal时,确保正确地设置和清理值,避免在不同线程之间共享ThreadLocal,并小心处理异常情况。此外,了解ThreadLocal的工作原理和使用场景也是确保它正常工作的关键。

14.创建 ThreadLocal 对象

//创建线程安全的DateFormat
public final static ThreadLocal<DateFormat> formatter = withInitial(() -> new SimpleDateFormat("dd-MMM-yyyy"));

源码:

//可以看到参数是Supplier,表示不要参数,返回一个值
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。