ThreadLocal = 本地线程?

1、定义

ThreadLocalJDK包提供的,从名字来看,ThreadLocal意思就是本地线程的意思。html

1.1 是什么?

要想知道他是个啥,咱们看看ThreadLocal的源码(基于JDK 1.8)中对这个类的介绍:java

This class provides thread-local variables.  These variables differ from
their normal counterparts in that each thread that accesses one (via its
{@code get} or {@code set} method) has its own, independently initialized
copy of the variable.  {@code ThreadLocal} instances are typically private
static fields in classes that wish to associate state with a thread (e.g.,
a user ID or Transaction ID).

大体可以总结出:git

  1. TreadLocal能够给咱们提供一个线程内的局部变量,并且这个变量与通常的变量还不一样,它是每一个线程独有的,与其余线程互不干扰的;
  2. ThreadLocal 与普通变量的区别在于:每一个使用该变量的线程都会初始化一个彻底独立的实例副本。ThreadLocal 变量一般被private static修饰。当一个线程结束时,它所使用的全部 ThreadLocal 相对的实例副本都会被回收;
  3. 简单说ThreadLocal就是一种以空间换时间的作法,在每一个Thread里面维护了一个ThreadLocal.ThreadLocalMap把数据进行隔离,每一个线程的数据不共享,天然就没有线程安全方面的问题了.

1.2 示例

一言不合上代码!github

//建立ThreadLocal变量
private static ThreadLocal<String> localParam = new ThreadLocal<>();

@Test
public void threadLocalDemo() {
    //建立2个线程,分别设置不一样的值
    new Thread(() -> {
        localParam.set("Hello 风尘博客!");
        //打印当前线程本地内存中的localParam变量的值
        log.info("{}:{}", Thread.currentThread().getName(), localParam.get());
    }, "T1").start();
    new Thread(() -> {
        log.info("{}:{}", Thread.currentThread().getName(), localParam.get());
    }, "T2").start();
}
  • 结果:
... T1:Hello 风尘博客!
... T2:null

打印结果证实,T1线程中设置的值没法在T2取出,证实变量ThreadLocal在各个线程中数据不共享。api

1.3 ThreadLocalAPI

ThreadLocal定义了四个方法:安全

  1. get():返回此线程局部变量当前副本中的值;
  2. set(T value):将线程局部变量当前副本中的值设置为指定值;
  3. initialValue():返回此线程局部变量当前副本中的初始值;
  4. remove():移除此线程局部变量当前副本中的值。
  • set()initialValue()区别
名称 set() initialValue()
定义 为这个线程设置一个新值 该方法用于设置初始值,而且在调用get()方法时才会被触发,因此是懒加载。可是若是在get()以前进行了set()操做,这样就不会调用
区别 若是对象生成的时机不禁咱们控制的时候使用 set() 方式 对象初始化的时机由咱们控制的时候使用initialValue() 方式

2、实现原理

ThreadLocal有一个特别重要的静态内部类ThreadLocalMap,该类才是实现线程隔离机制的关键。dom

  • 每一个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面,也就是说:ThreadLocal类型的本地变量存放在具体的线程内存空间中。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  • Thread类中有两个ThreadLocalMap类型的变量,分别是threadLocalsinheritableThreadLocals,而ThreadLocalMap是一个定制化的Hashmap,专门用来存储线程本地变量。在默认状况下,每一个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocalset()或者get()方法时才会建立它们。

风尘博客

  • ThreadLocal就是一个工具壳,它经过set()方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get()方法时,再从当前线程的threadLocals变量里面将其拿出来使用。ide

  • 若是调用线程一直不终止,那么这个本地变量会一直存放在调用线程的threadLocals变量里面,因此当不须要使用本地变量时能够经过调用ThreadLocal变量的remove()方法,从当前线程的threadLocals里面删除该本地变量。工具

另外Thread里面的threadLocals被设计为Map结构是由于每一个线程能够关联多个ThreadLocal变量。post

原理小结

  1. 每一个Thread维护着一个ThreadLocalMap的引用;
  2. ThreadLocalMapThreadLocal的内部类,用Entry来进行存储;
  3. 调用ThreadLocalset()方法时,实际上就是往ThreadLocalMap设置值,keyThreadLocal对象,值是传递进来的对象;
  4. 调用ThreadLocalget()方法时,实际上就是往ThreadLocalMap获取值,keyThreadLocal对象;
  5. ThreadLocal自己并不存储值,它只是做为一个key来让线程从ThreadLocalMap获取value

3、使用场景

3.1 ThreadLocal的做用

  • 保存线程上下文信息,在任意须要的地方能够获取.

因为ThreadLocal的特性,同一线程在某地方进行设置,在随后的任意地方均可以获取到。从而能够用来保存线程上下文信息。

  • 线程安全的,避免某些状况须要考虑线程安全必须同步带来的性能损失.

3.2 场景一:独享对象

每一个线程须要一个独享对象(一般是工具类,典型须要使用的类有SimpleDateFormatRandom

这类场景阿里规范里面也提到了:

风尘博客

3.3 场景二:当前信息须要被线程内的全部方法共享

每一个线程内须要保存全局变量(例如在拦截器中获取用户信息),可让不一样方法直接使用,避免参数传递的麻烦。

演示(完整演示见文末Github

  • User.java
@Data
public class User {
    private String userName;

    public User() {

    }

    public User(String userName) {
        this.userName = userName;
    }
}
  • UserContextHolder.java
public class UserContextHolder {

    public static ThreadLocal<User> holder = new ThreadLocal<>();

}
  • Service1.java
public class Service1 {

    public void process() {
        User user = new User("Van");
        //将User对象存储到 holder 中
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}
  • Service2.java
public class Service2 {

    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service2拿到用户名: " + user.getUserName());
        new Service3().process();
    }
}
  • Service3.java
public class Service3 {

    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用户名: " + user.getUserName());
    }
}
  • 测试方法
@Test
public void threadForParams() {
    new Service1().process();
}
  • 结果打印
Service2拿到用户名: Van
Service3拿到用户名: Van

3.4 使用ThreadLocal的好处

  1. 达到线程安全的目的;
  2. 不须要加锁,执行效率高;
  3. 更加节省内存,节省开销;
  4. 免去传参的繁琐,下降代码耦合度。

4、问题

4.1 内存泄漏问题

内存泄露:某个对象不会再被使用,可是该对象的内存却没法被收回

  • 正常状况

Thread运行结束后,ThreadLocal中的value会被回收,由于没有任何强引用了。

  • 非正常状况

Thread一直在运行始终不结束,强引用就不会被回收,存在如下调用链

Thread-->ThreadLocalMap-->Entry(key为null)-->value

由于调用链中的 valueThread 存在强引用,因此value没法被回收,就有可能出现OOM

如何避免内存泄漏(阿里规范)

调用remove()方法,就会删除对应的Entry对象,能够避免内存泄漏,因此使用完ThreadLocal后,要调用remove()方法。

4.2 ThreadLocal的空指针问题

  • ThreadLocalNPE.java
public class ThreadLocalNPE {

    ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();

    public void set() {
        longThreadLocal.set(Thread.currentThread().getId());
    }

    /**
     * 当前返回值为基本类型,会报空指针异常,若是改为包装类型Long就不会出错
     * @return
     */
    public long get() {
        return longThreadLocal.get();
    }
}
  • 空指针测试
@Test
public void threadLocalNPE() {
    ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
    //若是get方法返回值为基本类型,则会报空指针异常,若是是包装类型就不会出错
    System.out.println(threadLocalNPE.get());
}

若是get()方法返回值为基本类型,则会报空指针异常;若是是包装类型就不会出错。这是由于基本类型和包装类型存在装箱和拆箱的关系,因此,咱们必须将get()方法返回值使用包装类型。

4.3 参考文章

  1. 不再学Threadlocal了,看这一篇就忘不掉了(万字总结)
  2. 使用 ThreadLocal 一次解决老大难问题

4、技术交流

Github 示例代码

  1. 风尘博客:https://www.dustyblog.cn
  2. 风尘博客-掘金
  3. 风尘博客-博客园
  4. Github
  5. 公众号

    风尘博客
相关文章
相关标签/搜索