简介
ThreadLocal 很容易让人顾名思义,想当然地认为是一个“本地线程”。其实,ThreadLocal 并不是一个 Thread,而是 Thread 的局部变量,所以有人觉得它命名为
ThreadLocalVariable更容易让人理解一些。
ThreadLocal 很多地方叫做线程本地变量,也有些地方叫做线程本地存储。它是用来提供线程级别变量,变量只对当前线程可见,该变量对其他线程而言是隔离的。相比与“使用锁控制共享变量访问顺序”的解决方案。ThreadLocal 通过空间换时间的方案,在每个线程中都创建了一个副本,规避了竞争问题,因为每个线程都有属于自己的变量。
ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。 ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景
ThreadLocal与SynChronized的区别
ThreadLocal其实与线程绑定的一个变量。ThreadLocal和Synchronized都用于解决多线程并发访问
但是两者有本质的区别
- Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离
- Synchronized是利用锁的机制,使变量或代码块在某一时刻只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的不是同一个对象,这样就隔离了多个线程对数据的共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享
一句话理解ThreadLocal,Threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。
ThreadLocal原理
首先从原码看起
public void set(T value) {
//1、获取当前线程
Thread t = Thread.currentThread();
//2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
//则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 初始化thradLocalMap 并赋值
createMap(t, value);
}
从上面的代码可以看出,ThreadLocal set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。那么ThreadLocalMap又是什么呢,还有createMap又是怎么做的,我们继续往下看。
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
可看出ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用(发生GC即被回收)。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。
ThreadLocal的get方法
public T get() {
//1、获取当前线程
Thread t = Thread.currentThread();
//2、获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//3、如果map数据为空,
if (map != null) {
//3.1、获取threalLocalMap中存储的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
return setInitialValue();
}
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;
}
ThreadLocal的remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
remove方法,直接将ThrealLocal 对应的值从当前Thread中的ThreadLocalMap中删除。为什么要删除,这涉及到内存泄露的问题。
实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。(如下图,虚线是弱引用而实线是强引用)
ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。举个例字,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。
ThreadLocal常见的使用场景
ThreadLocal 使用的场景
- 每个线程需要有自己单独的实例,线程间数据隔离
- 实例需要在多个方法中共享,但不希望被多线程共享
- 进行事务操作,用于存储线程事务信息
- 数据库连接,session会话管理
- 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束
1)存储用户Session(项目中用到)
一个简单的用ThreadLocal来存储Session的例子:
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
在现在的系统设计中,前后端分离已基本成为常态,分离之后如何获取用户信息就成了一件麻烦事,通常在用户登录后, 用户信息会保存在Session或者Token中。这个时候,我们如果使用常规的手段去获取用户信息会很费劲,拿Session来说,我们要在接口参数中加上HttpServletRequest对象,然后调用 getSession方法,且每一个需要用户信息的接口都要加上这个参数,才能获取Session,这样实现就很麻烦了。
在实际的系统设计中,我们肯定不会采用上面所说的这种方式,而是使用ThreadLocal,我们会选择在拦截器的业务中, 获取到保存的用户信息,然后存入ThreadLocal,那么当前线程在任何地方如果需要拿到用户信息都可以使用ThreadLocal的get()方法 (异步程序中ThreadLocal是不可靠的)
在实际的系统设计中,我们肯定不会采用上面所说的这种方式,而是使用ThreadLocal,我们会选择在拦截器的业务中, 获取到保存的用户信息,然后存入ThreadLocal,那么当前线程在任何地方如果需要拿到用户信息都可以使用ThreadLocal的get()方法 (异步程序中ThreadLocal是不可靠的)
当用户登录后,会将用户信息存入Token中返回前端,当用户调用需要授权的接口时,需要在header中携带Token,然后拦截器中解析Token,获取用户信息,调用自定义的类存入ThreadLocal中,当请求结束的时候,将ThreadLocal存储数据清空(这一点很重要会产生内存泄漏),中间的过程无需再关注如何获取用户信息,只需要使用工具类的get方法即可
第二种场景是:数据库连接、处理数据库事务
第三种场景:数据跨层传递(controller、service、dao)
每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)
比如说一个用户系统,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就会依次调用若干个service方法,这些方法可能是分布在不同的类中,这个例子和session有点像
第四种场景:Spring使用ThreadLocal解决线程问题
一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的"状态对象"采用ThreadLocal封装,让他们也成为线程安全的"状态性对象",因此有状态的Bean就能够以singleton的方式在多线程中正常工作了
一般的Web应用划分为展现层、服务层、持久层,在不同的层中编写对应的逻辑,下层通过接口向上层开发功能调用。在一般情况下从请求到返回响应所经过的所有程序调用都同属于一个线程
这样用户就可以根据需啊哟,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的
ThreadLocal使用示例
import java.util.stream.IntStream;
/** @author Strive */
public class DemoThreadLocal {
public static void main(String[] args) {
ThreadLocal<String> local = new ThreadLocal<>();
IntStream.range(0, 5)
.forEach(
x ->
new Thread(
() -> {
local.set(Thread.currentThread().getName() + ":" + x);
System.out.println(
"线程:"
+ Thread.currentThread().getName()
+ "的 local 值:"
+ local.get());
})
.start());
}
}
运行结果
线程:Thread-0的 local 值:Thread-0:0
线程:Thread-4的 local 值:Thread-4:4
线程:Thread-3的 local 值:Thread-3:3
线程:Thread-2的 local 值:Thread-2:2
线程:Thread-1的 local 值:Thread-1:1
| 方法名 | 注释 |
|---|---|
| void set(Object value) | 设置当前线程的线程局部变量的值。 |
| Object get() | 该方法返回当前线程所对应的线程局部变量。 |
| void remove() | 将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是 JDK 5.0新增的方法。 |
| protected Object initialValue() | 返回该线程局部变量的初始值,该方法是一个 protected 的方法,显然是为了让子类覆盖而设计的。还没有set的情况下,调用 get 则返回 null。 |
总结一下
-
当我们定义一个
ThreadLocal变量时,其实就是在定义一个Key。 -
当我们调用
set(v)方法时,就是以当前ThreadLocal变量为key,传入参数为value,向ThreadLocal.ThreadLocalMap存数据。- 当我们调用
get()方法时,就是以当前ThreadLocal变量为key,从ThreadLocal.ThreadLocalMap取对应的数据。
- 当我们调用
ThreadLocal和线程同步机制
首先聊聊他们是干嘛的,ThreadLocal 和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。
同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
ThreadLocal 从另一个角度来解决多线程的并发访问。ThreadLocal 会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进 ThreadLocal。
概括起来说,对于多线程资源共享的问题,同步机制采用了 “以时间换空间” 的方式,而 ThreadLocal 采用了 “以空间换时间” 的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
扩展
ThreadLocalMap的Hash冲突解决方法
采用线性探测的方式,根据 key 计算 hash 值,如果出现冲突,则向后探测,当到哈希表末尾的时候再从0开始,直到找到一个合适的位置。
这种算法也决定了 ThreadLocalMap不适合存储大量数据。
ThreadLocalMap的扩容问题
ThreadLocalMap初始大小为 16,加载因子为 2/3,当 size 大于 threshold时,就会进行扩容。
扩容时,新建一个大小为原来数组长度的两倍的数组,然后遍历旧数组中的 entry 并将其插入到新的hash数组中,在扩容的时候,会把 key 为 null 的 Entry的 value 值设置为 null,以便内存回收,减少内存泄漏问题。
ThreadLocal内存泄漏问题
首先我们要知道啥是内存泄漏呢?简单的说,就是东西放在内存里面,但你忘记它放哪里了,它占着一块内存,但是不能回收。当这样的东西越来越多,内存就吃紧,最终导致服务器宕机。
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
看注释:Note that null keys (i.e. entry.get()* == null) mean that the key is no longer referenced, so the entry can be expunged from table。
意思就是:如果 key threadlocal 为 null 了,这个 entry 就可以清除了。ThreadLocal 是一个弱引用,当为 null 时,会被当成垃圾回收。
重点来了,突然我们 ThreadLocal 是 null 了,也就是要被垃圾回收器回收了,但是此时我们的 ThreadLocalMap(thread 的内部属性)生命周期和 Thread 的一样,它不会回收,这时候就出现了一个现象。那就是 ThreadLocalMap 的 key 没了,但是 value 还在,这就造成了内存泄漏。
解决办法:使用完 ThreadLocal 后,执行 remove 操作,避免出现内存溢出情况。
所以,如同 lock 的操作 最后要执行解锁操作一样,ThreadLocal使用完毕一定记得执行remove 方法,清除当前线程的数值。
如果不 remove 当前线程对应的 VALUE, 就会一直存在这个值。
使用了线程池,可以达到“线程复用”的效果。但是归还线程之前记得清除 ThreadLocalMap,要不然再取出该线程的时候,ThreadLocal 变量还会存在。这就不仅仅是内存泄露的问题了,整个业务逻辑都可能会出错。