什么是Threadlocal?
ThreadLocal意为线程本地变量,用于解决多线程并发时访问共享变量的问题。
所谓的共享变量指的是在堆中的实例、静态属性和数组; 对于共享数据的访问受Java的内存模型JMM的控制
下图为java内存模型图
主内存中(堆)主要就是存放的各种共享变量,并且每个线程都会有自己单独的本地内存也叫做工作者内存,当一个线程要使用主存的共享变量的时候,线程会复制一份到自己的本地内存中,当线程修改了共享变量后,就会通过JMM的管理控制写回到主内存中。
但是在多线程下的时候,多个线程同时对共享变量进行修改,而且线程之间是不可见的,所以就会出现线程安全问题,即是数据不一致的问题。一般的解决方案是给访问的共享变量的代码加锁synchronized或者Lock
,但是会对性能消耗比较大,当然也可以使用volatile关键字,CAS+原子类也是可以的,但是今天在这里主要讲解在JDK1.2中引入了ThreadLocal类
,来修饰共享变量,使每个线程都单独拥有一份共享变量,这样就可以做到线程之间对于共享变量的隔离问题
锁和ThreadLocal使用场景还是有区别的
synchronized(锁) | ThreadLocal | |
---|---|---|
原理 | 同步机制采用了时间换空间的方式,只提供一份变量,让不同线程排队访问(临界区排队) | 采用空间换时间的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不相干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
ThreadLocal的使用
一般都会将ThreadLocal声明成一个静态字段,同时初始化如下:
swift
public static ThreadLocal<Object> threadLocal = new ThreadLocal<>();
其中Object就是原本堆中共享变量的数据。
例如,有个User对象需要在不同线程之间进行隔离访问,可以定义ThreadLocal如下:
ini
static ThreadLocal<User> threadLocal = new ThreadLocal<>();
常用的方法
set(T value):
设置线程本地变量的内容。get()
:获取线程本地变量的内容。remove()
:移除线程本地变量。注意在线程池的线程复用场景中在线程执行完毕时一定要调用remove,避免在线程被重新放入线程池中时被本地变量的旧状态仍然被保存。
csharp
public class threadlocalTest {
//创建线程本地变量
static ThreadLocal<User> threadLocal = new ThreadLocal<>();
//添加
public void add(User user) {
threadLocal.set(user);
}
//获取和删除
public void getvalue() {
User user = threadLocal.get();
// 使用
//进行一些操作
// 使用完清除
threadLocal.remove();
}
}
ThreadLocal的原理
hreadLocal
可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量 就行了,做到了线程之间互相隔离, 相比于synchronized的做法是用空间来换时间。
ThreadLocal
有一个静态内部类ThreadLocalMap
,ThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对的能力。
弱引用的目的
是为了防止内存泄露 ,如果是强引用那么ThreadLocal对象除非线程结束否则始终无法被回收,弱引用则会在下一次GC的时候被回收。但是这样还是会存在内存泄露的问题,假如key和ThreadLocal对象被回收之后,entry中就存在key为null,但是value有值的entry对象,但是永远没办法被访问到,同样除非线程结束运行。
但是只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上是不会出现这个问题的
ThreadLocal::set方法的原理
scss
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的threadLocals字段
ThreadLocalMap map = getMap(t);
// 判断线程的threadLocals是否初始化了
if (map != null) {
map.set(this, value);
} else {
// 没有则创建一个ThreadLocalMap对象进行初始化
createMap(t, value);
}
}
createMap方法的源码如下:
javascript
void createMap(Thread t, T firstValue) {
//为当前线程初始化threadLocalszi
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
可以看到他首先调用了Thread.currentThread()
获取当先线程然后调用了getMap()获取到线程对象,后面就是为线程对象set指定的值可以发现set()源码非常简单,主要是ThreadLocalMap
需要我们注意,直接看看getMap!
javascript
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
java
class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
JDK8之后,每个Thread维护一个ThreadLocalMap对象
,这个Map的key是ThreadLocal实例本身,value是存储的值要隔离的变量 ,是泛型,可以看到每个线程Thread都会维护自己的threadLocal变量,所以每次使用ThreadLoacl.get() 方法的时候都是从自己的线程里面拿到自己的ThreadLocals变量,这样是拿不到其他线程的变量,从而实现了数据隔离
源码可以看出threadLocals是ThreadLocal里面的ThreadLocalMap,那么ThreadLocalMap底层结构长什么样呢?
ThreadLocalMap
在ThreadLocal
中定义的Map对象,保存了该线程中的所有本地变量。ThreadLocalMap中的Entry的定义如下
是继承了WeakReference
(弱引用)
scala
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;
// key为一个ThreadLocal对象,v就是我们要在线程之间隔离的对象
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
//可以看出是一个数组
private Entry[] table;
从上面源码可以看到是使用的数组来进行保存变量的
那么用了数组怎么解决Hash冲突问题呢?
在ThreadLocalMap的set方法中,会根据ThreadLocal对象的hash值,定位到指定位置,然后判断该位置key和Entry对象是否和get的key一样,一样就直接赋值在索引上,如果为null这则初始化一个Entry对象放在索引上
ThreadLocal::get方法的原理
ini
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
如果调用返回值不为null,则返回它的value值,如果为空则执行设置初始值setInitiaValue()
方法,这里主要看ThreadLocalMap的getEntry()方法
vbnet
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
getEntry()的实现逻辑就是拿到key的hash值,然后判断此处是否等于key,如果是则返回这个Entry对象,如果不是则执行getEntryAfterMiss()方法。
getEntryAfterMiss():
法就是一直往下查找,直到找到对应的位置。
应用场景
1、在重入方法中替代参数的显式传递
假如在我们的业务方法中需要调用其他方法,同时其他方法都需要用到同一个对象时,可以使用ThreadLocal替代参数的传递或者static静态全局变量。这是因为使用参数传递造成代码的耦合度高,使用静态全局变量在多线程环境下不安全。当该对象用ThreadLocal包装过后,就可以保证在该线程中独此一份,同时和其他线程隔离。
例如在Spring的@Transaction事务声明的注解中就使用ThreadLocal保存了当前的Connection对象,避免在本次调用的不同方法中使用不同的Connection对象。
2、全局存储用户信息
可以尝试使用ThreadLocal替代Session的使用,当用户要访问需要授权的接口的时候,可以先在拦截器中将用户的Token存入ThreadLocal中;之后在本次访问中任何需要用户信息的都可以直接向ThreadLocal中拿取数据。
3、解决线程安全问题
依赖于ThreadLocal本身的特性,对于需要进行线程隔离的变量可以使用ThreadLocal进行封装。
总结
- ThreadLocal更像是对其他类型变量的一层包装,通过ThreadLocal的包装使得该变量可以在
线程之间隔离
和当前线程全局共享
- ThreadLocalMap中Entry的Key不管是否使用弱引用 都有内存泄露的可能。引起内存泄露主要在于ThreadLocal对象和Entry中的Value对象,因此要确保每次使用完之后都remove掉Entry!