欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

ThreadLocal = 本地线程?

程序员文章站 2022-05-29 07:57:16
一、定义 是`JDK ThreadLocal`意思就是本地线程的意思。 1.1 是什么? 要想知道他是个啥,我们看看 的源码(基于 )中对这个类的介绍: 大致能够总结出: 1. 可以给我们提供一个线程内的局部变量,而且这个变量与一般的变量还不同,它是每个线程独有的,与其他线程互不干扰的; 1. 与普 ......

一、定义

threadlocaljdk包提供的,从名字来看,threadlocal意思就是本地线程的意思。

1.1 是什么?

要想知道他是个啥,我们看看threadlocal的源码(基于jdk 1.8)中对这个类的介绍:

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).

大致能够总结出:

  1. treadlocal可以给我们提供一个线程内的局部变量,而且这个变量与一般的变量还不同,它是每个线程独有的,与其他线程互不干扰的;
  2. threadlocal 与普通变量的区别在于:每个使用该变量的线程都会初始化一个完全独立的实例副本。threadlocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 threadlocal 相对的实例副本都会被回收;
  3. 简单说threadlocal就是一种以空间换时间的做法,在每个thread里面维护了一个threadlocal.threadlocalmap把数据进行隔离,每个线程的数据不共享,自然就没有线程安全方面的问题了.

1.2 示例

一言不合上代码!

//创建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在各个线程中数据不共享。

1.3 threadlocalapi

threadlocal定义了四个方法:

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

二、实现原理

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

  • 每个线程的本地变量不是存放在threadlocal实例里面,而是存放在调用线程的threadlocals变量里面,也就是说:threadlocal类型的本地变量存放在具体的线程内存空间中。
threadlocal.threadlocalmap threadlocals = null;
threadlocal.threadlocalmap inheritablethreadlocals = null;
  • thread类中有两个threadlocalmap类型的变量,分别是threadlocalsinheritablethreadlocals,而threadlocalmap是一个定制化的hashmap,专门用来存储线程本地变量。在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用threadlocalset()或者get()方法时才会创建它们。

ThreadLocal = 本地线程?

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

  • 如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的threadlocals变量里面,所以当不需要使用本地变量时可以通过调用threadlocal变量的remove()方法,从当前线程的threadlocals里面删除该本地变量。

另外thread里面的threadlocals被设计为map结构是因为每个线程可以关联多个threadlocal变量。

原理小结

  1. 每个thread维护着一个threadlocalmap的引用;
  2. threadlocalmapthreadlocal的内部类,用entry来进行存储;
  3. 调用threadlocalset()方法时,实际上就是往threadlocalmap设置值,keythreadlocal对象,值是传递进来的对象;
  4. 调用threadlocalget()方法时,实际上就是往threadlocalmap获取值,keythreadlocal对象;
  5. threadlocal本身并不存储值,它只是作为一个key来让线程从threadlocalmap获取value

三、使用场景

3.1 threadlocal的作用

  • 保存线程上下文信息,在任意需要的地方可以获取.

由于threadlocal的特性,同一线程在某地方进行设置,在随后的任意地方都可以获取到。从而可以用来保存线程上下文信息。

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

3.2 场景一:独享对象

每个线程需要一个独享对象(通常是工具类,典型需要使用的类有simpledateformatrandom

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

ThreadLocal = 本地线程?

3.3 场景二:当前信息需要被线程内的所有方法共享

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

ThreadLocal = 本地线程?

演示(完整演示见文末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.1 内存泄漏问题

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

  • 正常情况

thread运行结束后,threadlocal中的value会被回收,因为没有任何强引用了。

  • 非正常情况

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

thread-->threadlocalmap-->entry(key为null)-->value

因为调用链中的 valuethread 存在强引用,所以value无法被回收,就有可能出现oom

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

调用remove()方法,就会删除对应的entry对象,可以避免内存泄漏,所以使用完threadlocal后,要调用remove()方法。

ThreadLocal = 本地线程?

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 一次解决老大难问题

四、技术交流

github 示例代码

  1. github