ThreadLocal 简单理解

结构理解

  • 每个Thread线程内部都只有一个ThreadLocalMap。
  • Map里面存储线程本地对象ThreadLocal(key)和线程的变量副本(value)。
  • Thread内部的Map是由ThreadLocal维护,ThreadLocal负责向map获取和设置线程的变量值
  • 一个Thread可以有多个ThreadLocal

ThreadLocal场景

  • 每个线程需要有自己单独的实例 (也可线程内部构建单独的实例,但ThreadLocal更方便)
  • 实例不被多线程共享,但需在多个方法中共享(可通过方法间形参传递实现,但ThreadLocal降低耦合)

ThreadLocal 提供了线程本地的实例。每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

① 单独实例

private static ThreadLocal<String> local = new ThreadLocal<>();

public static void main(String args[]) {

    local.set("这是在主线程中");
    println("线程名字:"+ Thread.currentThread().getName() + "---" + local.get());

    // 线程a
    new Thread(()->{
        local.set("这是在线程a中");
        println("线程名字:" + Thread.currentThread().getName() + "---" + local.get());
    }, "线程a").start();
    
    // 线程b
    new Thread(()->{
        local.set("这是在线程b中");
        println("线程名字:" + Thread.currentThread().getName() + "---" + local.get());
    }, "线程b").start();
}

输出结果:

线程名字:main---这是在主线程中
线程名字:线程a---这是在线程a中
线程名字:线程b---这是在线程b中

线程内部的ThreadLocalMap,存储了ThreadLocal为Key变量副本为Vaule的键值对。(隔离了)

② 方法参数传递

// 如下示例:执行一个大方法 SaveOrder() 中有N个小方法
public void SaveOrder(User user) {
    checkPermission(); // 检查正则验证
    doWork();          // 执行订单保存等
    saveStatus();      // 保存订单状态
    sendResponse();    // 响应保存结果
}

问题:如何在一个线程内传递状态?(假设传递就是user实例)

// 解决:直接每个方法都传递user ??
public void SaveOrder(User user) {
    checkPermission(user);
    doWork(user); // doSomething...
    saveStatus(user);
    sendResponse(user);
}

// 问题:往往一个方法又会调用其他很多方法,导致User传递到所有地方:
public void doWork(User user) {
    queryStatus(user);
    checkStatus();
    setNewStatus(user);
}

解决:ThreadLocal 可以在一个线程中传递同一个对象

在一个线程中,横跨若干方法调用,需要传递的对象,通常称之为上下文(Context)它是一种状态,可以是用户身份、任务信息等。

每个方法都加入形参传递非常麻烦,且如果调用链有无法修改源码的第三方库,User对象就传不进去

 它的典型使用方式如下:

// 通常以静态static修饰,涉及到延长生命周期,从而减少key值为null的频率产生
static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();

public void SaveOrder(user) {
    try {
        threadLocalUser.set(user);
        step1();
        step2();
    } finally {
        threadLocalUser.remove();
    }
}

// 通过设置一个User实例关联到ThreadLocal中,在移除之前,所有方法都可以随时获取到该User实例:
void step1() {
    User u = threadLocalUser.get();
    printUser();
}
void step2() {
    User u = threadLocalUser.get();
    checkUser(u.id);
}

ThreadLocal 使用注意

  • ThreadLocal表示线程的“局部变量”,它确保每个线程的ThreadLocal变量都是各自独立的;
  • ThreadLocal适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递)
  • 使用ThreadLocal要用try ... finally结构,并在finally中清除。

因为当前线程执行完相关代码后,可能会被重新放入线程池中,如果ThreadLocal没有被清除,该线程执行其他代码时,会把上一次的状态带进去

源码解析

问题: ThreadLocal是怎么把变量复制到Thread的ThreadLocalMap中的?

 当我们初始化一个线程时,其内部就去创建了一个ThreadLocalMap的Map容器待用

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null; 
}

ThreadLocalMap 被创建加载时,其静态内部类Entry也随之加载,完成初始化动作:

public class ThreadLocal<T> {
    ...
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
          super(k);
          value = v;
        }
    }
}

问题:那么它又是如何和ThreadLocal缠上关系,ThreadLocal又是如何管理键值对的关系

⭐️ set方法

public class ThreadLocal<T> {
  
    public void set(T value) {
        // 1.获取当前线程Thread
        Thread t = Thread.currentThread();
        // 2.拿到当前线程Thread 内部的ThreadLocalMap容器
        ThreadLocalMap map = getMap(t);
        // 3.最后就把变量副本给丢进去。
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value); // map不存在就创建,并把值丢进去
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

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

当我们在Thread内部调用set方法时:

  1. 第一步会去获取当前线程Thread
  2. 然后拿到当前线程Thread 内部的ThreadLocalMap容器
  3. 最后就把变量副本给丢进去。

没了…懂了吗,ThreadLocal(就是个维护线程内部变量的工具!)只是在Set的时候去操作了Thread内部的ThreadLocalMap,将变量拷贝到Thread内部的Map容器中,Key:当前的ThreadLocal,Value:变量的副本

⭐️ get方法

public T get() {
    Thread t = Thread.currentThread();
    // 1.获取当前线程的ThreadLocalMap对象 
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 2.从map中根据this(当前的threadlocal对象)获取线程存储的Entry节点。 
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 3.从Entry节点获取存储的对应Value副本值返回。 
        if (e != null)
            return (T)e.value;
    }
    
    return setInitialValue(); // map为空的话返回初始值null,即线程变量副本为null
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

private T setInitialValue() {
    T value = initialValue(); // map为空的话返回初始值null,即线程变量副本为null
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

// map为空的话返回初始值null,即线程变量副本为null。
protected T initialValue() {
    return null;
}

⭐️ 设置初始值

private T setInitialValue() {
  // 1. initialValue():protected方法,默认返回 null。典型用法中常常重载该方法
  T value = initialValue(); 
  Thread t = Thread.currentThread();
  // 2. 拿到该线程Thread对应的 ThreadLocalMap 对象
  ThreadLocalMap map = getMap(t);
  // 3.map不为null,该ThreadLocal对象与对应实例初始值的映射添加进该线程的 ThreadLocalMap中
  if (map != null)
    map.set(this, value);
  // 4.map为null,先创建该 ThreadLocalMap 对象再将映射添加其中。
  else
    createMap(t, value);
  return value;
}

无需考虑 ThreadLocalMap 的线程安全问题:因为每个线程有且只有一个 ThreadLocalMap 对象,并且只有该线程自己可以访问它

ThreadLocal 常见面试题

① 什么是ThreadLocal? 用来解决什么问题的?

ThreadLocal是一个:在多线程中为每一个线程创建单独的变量副本 的类;解决了变量在线程之间隔离,而在方法或类间共享的场景

② 说说你对ThreadLocal的理解?

ThreadLocal 提供了线程本地的实例。 它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。由于在每个线程中都创建了副本,故要考虑对资源的消耗,比如内存的占用会比不使用要大

ThreadLocal就是提供给每个线程操作变量的工具类,做到了线程之间的变量隔离目的

③ ThreadLocal是如何实现线程隔离的?

主要用到了线程Thread 中的 ThreadLocalMap,每个线程有且只有一个ThreadLocalMap,且只有当前线程能够访问它,故此就实现了隔离

④ ThreadLocal是怎么把变量复制到Thread的ThreadLocalMap中的?

当我们初始化一个线程时,其内部就去创建了一个ThreadLocalMap的Map容器待用

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null; 
}

ThreadLocalMap 被创建加载时,其静态内部类Entry也随之加载,完成初始化动作:

public class ThreadLocal<T> {
    ...
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
          super(k);
          value = v;
        }
    }
}

⑤ 那么它又是如何和ThreadLocal缠上关系,ThreadLocal又是如何管理键值对的关系

  • set()方法用于保存当前线程的副本变量值。
  • get()方法用于获取当前线程的副本变量值。
  • initialValue()为当前线程初始副本变量值。
  • remove()方法移除当前线程的副本变量值。

set 方法解析:

public class ThreadLocal<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);
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

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

当我们在Thread内部调用set方法时:

  1. 第一步会去获取调用当前方法的线程Thread
  2. 然后拿到当前线程内部的ThreadLocalMap容器
  3. 最后就把变量副本给丢进去。

没了…懂了吗,ThreadLocal(就是个维护线程内部变量的工具!)只是在Set的时候去操作了Thread内部的ThreadLocalMap,将变量拷贝到Thread内部的Map容器中,Key:当前的ThreadLocal,Value:变量的副本

get 方法解析:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

protected T initialValue() {
    return null;
}
  • 获取当前线程的ThreadLocalMap对象
  • 从map中根据this(当前的threadlocal对象)获取线程存储的Entry节点。
  • 从Entry节点获取存储的对应Value副本值返回。
  • map为空的话返回初始值null,即线程变量副本为null。

⑥ 既然ThreadLocal只是个维护线程内部变量的工具,为什么不直接用ThreadLocalMap?

ThreadLocalMap属于ThreadLocal的静态内部类,map的方法都在ThreadLocal类中调用

⑦ ThreadLocalMap使用ThreadLocal的弱引用作为key,为什么不使用强引用?

  • key强引用:实际开发中,不需要ThreadLocal了,如果线程仍在运行,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal 就不会被GC回收,易发生内存泄漏

    内存泄漏:本该被回收的对象不能被回收,而停留在堆内存中占有内存(那块内存也不能再访问)

  • key弱引用:弱引用就不存在此问题,由于ThreadLocalMap持有的ThreadLocal是弱引用,即使没有手动删除,也会被GC回收(key = null);使用弱引用对于 threadlocal 对象而言是不会发生内存泄漏的

    Java8中,ThreadLocal的get()、set()、remove()方法调用时,会清除掉线程ThreadLocalMap中所有Entry中 Key为null的Value,并将整个Entry设置为null,利于下次内存回收。(前提是key是弱引用)

    虽key被回收了, 但ThreadLocalMap的value还是强引用,只有thead线程退出以后,value的强引用链条才会断掉(但如果是线程池呢?线程没有销毁,不会被回收,Value一直存在:内存泄漏)

ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用

⑧ 降低内存泄漏的风险

Java8中,ThreadLocal的get()、set()、remove()方法,会清除掉线程ThreadLocalMap中所有Entry中 Key为null的Value,并将整个Entry设置为null,故当线程使用完ThreadLocal后,建议手动调用remove方法清除

 ⑨ 还有哪些使用ThreadLocal的应用场景?

  • 每个线程需要有自己单独的实例 (也可线程内部构建单独的实例,但ThreadLocal更方便)
  • 实例不被多线程共享,但需在多个方法中共享(可通过方法间形参传递实现,但ThreadLocal降低耦合)

⑩ 为什么JDK建议ThreadLocal定义为 static?

  • static 避免重复创建实例,浪费内存

    被static修饰为类变量,该类的所有实例都共享此变量 ,类加载时就完成了内存的分配和初始化在内存中只有一个副本,所有此类的实例对象(该线程内定义的)都可以操控这个变量。从而避免创建重复,造成内存浪费。

    避免重复创建ThreadLocal所关联的对象:ThreadLocal<Cat> local = new ... 中的 Cat 对象

  • 当static时,延长ThreadLocal 生命周期,避免被回收

    ThreadLocalMap的key是弱引用,如外部未强引用ThreadLocal,会被系统GC回收,key为null无法访问value,线程不结束,无用的value无法回收造成内存泄漏。static保证ThreadMap的key存在强引用,线程生命期内始终有值,而不被回收,从而通过ThreadLocal的弱引用访问到Entry中的value值。如在线程池下,不用ThreadLocal了,必须 (1) remove 或 (2) 手动 ThreadLocal ref = null

ThreadLocal 内存泄漏的原因:

实心箭头表示强引用,空心箭头表示弱引用

从上图中可以看出,threadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。

但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

永远无法回收,造成内存泄漏。

HashCode 计算

ThreadLocalMap中没有采用传统的调用ThreadLocal的hashcode方法(继承自object的hashcode),而是调用nexthashcode,源码如下:

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
 //1640531527 能够让hash槽位分布相当均匀
private static final int HASH_INCREMENT = 0x61c88647; 
private static int nextHashCode() {
      return nextHashCode.getAndAdd(HASH_INCREMENT);
}

Hash冲突

ThreadLocalMap解决Hash冲突的方式:简单的步长加1或减1及线性探测,寻找下一个相邻的位置

/**
 * Increment i modulo len.
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}
/**
 * Decrement i modulo len.
 */
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如有大量不同的ThreadLocal对象放入map中时容易发生冲突。

建议每个线程只存一个变量(一个ThreadLocal)就不存在Hash冲突的问题,如果一个线程要保存set多个变量,则需创建多个ThreadLocal,但也易增加Hash冲突的可能。


 

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值