ThreadLocal = 本地线程?
一、定义
threadlocal
是jdk
包提供的,从名字来看,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).
大致能够总结出:
-
treadlocal
可以给我们提供一个线程内的局部变量,而且这个变量与一般的变量还不同,它是每个线程独有的,与其他线程互不干扰的; -
threadlocal
与普通变量的区别在于:每个使用该变量的线程都会初始化一个完全独立的实例副本。threadlocal
变量通常被private static
修饰。当一个线程结束时,它所使用的所有threadlocal
相对的实例副本都会被回收; - 简单说
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 threadlocal
的api
threadlocal
定义了四个方法:
-
get()
:返回此线程局部变量当前副本中的值; -
set(t value)
:将线程局部变量当前副本中的值设置为指定值; -
initialvalue()
:返回此线程局部变量当前副本中的初始值; -
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
类型的变量,分别是threadlocals
和inheritablethreadlocals
,而threadlocalmap
是一个定制化的hashmap
,专门用来存储线程本地变量。在默认情况下,每个线程中的这两个变量都为null
,只有当前线程第一次调用threadlocal
的set()
或者get()
方法时才会创建它们。
threadlocal
就是一个工具壳,它通过set()
方法把value
值放入调用线程的threadlocals
里面并存放起来,当调用线程调用它的get()
方法时,再从当前线程的threadlocals
变量里面将其拿出来使用。如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的
threadlocals
变量里面,所以当不需要使用本地变量时可以通过调用threadlocal
变量的remove()
方法,从当前线程的threadlocals
里面删除该本地变量。
另外thread
里面的threadlocals
被设计为map
结构是因为每个线程可以关联多个threadlocal
变量。
原理小结
- 每个
thread
维护着一个threadlocalmap
的引用; -
threadlocalmap
是threadlocal
的内部类,用entry
来进行存储; - 调用
threadlocal
的set()
方法时,实际上就是往threadlocalmap
设置值,key
是threadlocal
对象,值是传递进来的对象; - 调用
threadlocal
的get()
方法时,实际上就是往threadlocalmap
获取值,key
是threadlocal
对象; -
threadlocal
本身并不存储值,它只是作为一个key
来让线程从threadlocalma
p获取value
。
三、使用场景
3.1 threadlocal
的作用
- 保存线程上下文信息,在任意需要的地方可以获取.
由于threadlocal
的特性,同一线程在某地方进行设置,在随后的任意地方都可以获取到。从而可以用来保存线程上下文信息。
- 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失.
3.2 场景一:独享对象
每个线程需要一个独享对象(通常是工具类,典型需要使用的类有simpledateformat
和random
)
这类场景阿里规范里面也提到了:
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
的好处
- 达到线程安全的目的;
- 不需要加锁,执行效率高;
- 更加节省内存,节省开销;
- 免去传参的繁琐,降低代码耦合度。
四、问题
4.1 内存泄漏问题
内存泄露:某个对象不会再被使用,但是该对象的内存却无法被收回
- 正常情况
当thread
运行结束后,threadlocal
中的value
会被回收,因为没有任何强引用了。
- 非正常情况
当thread
一直在运行始终不结束,强引用就不会被回收,存在以下调用链
thread-->threadlocalmap-->entry(key为null)-->value
因为调用链中的 value
和 thread
存在强引用,所以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()
方法返回值使用包装类型。