目录
一、ThreadLocal的作用
背景:多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步
问题:同步的措施一般是加锁,但加锁会在一定程度上增加系统的复杂度以及影响系统的性能。
解决:多线程间共享变量的线程安全,ThreadLocal应运而生。当创建一个ThreadLocal变量时,访问这个变量的每个线程都有这个变量的一个本地副本,当多个线程操作这个变量时,实际上就是操作自己本地内存里面的变量,从而避免了线程安全问题
二、ThreadLocal使用
讲完了理论的东西,我们来通过下面的例子体会下ThreadLocal的神奇之处吧
public class Test {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
threadLocal.set("本地变量1");
print("thread1");
});
Thread thread2 = new Thread(() -> {
threadLocal.set("本地变量2");
print("thread2");
});
thread1.start();
thread2.start();
}
public static void print(String thread){
System.out.println(thread+":"+threadLocal.get());
}
}
执行结果如下:
现象:不同线程之间的ThreadLocal的值是独立开
本质:ThreadLocal的set 与get 方法,可以理解为其实就是操作当前线程的一个里面一个属性变量
三、ThreadLocal原理
1、对Thread对象的里面关键属性变量的理解->ThreadLocal.ThreadLocalMap
一个实例对象中实例成员字段的内容肯定是这个对象独有的,ThreadLocal线程本地变量作为一个Thread类的成员字段,
ThreadLocal.ThreadLocalMap threadLocals = null;
是一个在ThreadLocal中定义的Map对象,保存了该线程中的所有本地变量。ThreadLocalMap中的Entry的定义如下:
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;
}
}
2、ThreadLocal常用方法
- set(T value):设置线程本地变量的内容。
- get():获取线程本地变量的内容。
- remove():移除线程本地变量。注意在线程池的线程复用场景中在线程执行完毕时一定要调用remove,避免在线程被重新放入线程池中时被本地变量的旧状态仍然被保存。
3、ThreadLocal.set方法原理
set方法源码如下:
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方法的源码如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
map.set方法的源码如下:
/**
* 往map中设置ThreadLocal的关联关系
* set中没有使用像get方法中的快速选择的方法,因为在set中创建新条目和替换旧条目的内容一样常见,
* 在替换的情况下快速路径通常会失败(对官方注释的翻译)
*/
private void set(ThreadLocal<?> key, Object value) {
// map中就是使用Entry[]数据保留所有的entry实例
Entry[] tab = table;
int len = tab.length;
// 返回下一个哈希码,哈希码的产生过程与神奇的0x61c88647的数字有关
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
// 已经存在则替换旧值
e.value = value;
return;
}
if (k == null) {
// 在设置期间清理哈希表为空的内容,保持哈希表的性质
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
// 扩容逻辑
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
4、ThreadLocal.get方法原理
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 获取ThreadLocal对应保留在Map中的Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 获取ThreadLocal对象对应的值
T result = (T)e.value;
return result;
}
}
// map还没有初始化时创建map对象,并设置null,同时返回null
return setInitialValue();
}
5、ThreadLocal.remove方法原理
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
// 键在直接移除
if (m != null) {
m.remove(this);
}
}
6、类结构图
7、ThreadLocal的设计
- 每个Thread线程内部都有一个Map(ThreadLocalMap::threadlocals);
- Map里面存储ThreadLocal对象(key)和线程的变量副本(value);
- Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置变量值;
- 对于不同的线程,每次获取副本值时,别的线程不能获取当前线程的副本值,就形成了数据之间的隔离
8、使用ThreadLocal好处
- 保存每个线程绑定的数据,在需要的地方可以直接获取,避免直接传递参数带来的代码耦合问题;
- 各个线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。
四、ThreadLocal内存泄露问题
内存泄露问题:指程序中动态分配的堆内存由于某种原因没有被释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢或者系统奔溃等严重后果。内存泄露堆积将会导致内存溢出。
① TreadLocal使用弱引用原因:如果强引用的话会导致 ThreadLocal无法回收
② 使用完ThreadLocal必须remove原因:由于ThreadLocalMap中任然存在Current Thread Ref这个强引用,因此Entry中value的值任然无法清除。还是存在内存泄露的问题。(线程重复利用情况或用到线程池)
五、ThreadLocal的应用场景
场景一:在重入方法中替代参数的显式传递
例如:在Spring的@Transaction事务声明的注解中就使用ThreadLocal保存了当前的Connection对象,避免在本次调用的不同方法中使用不同的Connection对象。
场景二:全局存储用户信息
例如:可以尝试使用ThreadLocal替代Session的使用,当用户要访问需要授权的接口的时候,可以现在拦截器中将用户的Token存入ThreadLocal中;之后在本次访问中任何需要用户用户信息的都可以直接冲ThreadLocal中拿取数据
六、总结
① ThreadLocal更像是对其他类型变量的一层包装,通过ThreadLocal的包装使得该变量可以在线程之间隔离和当前线程全局共享。
② 线程的隔离性和变量的线程全局共享性得益于在每个Thread类中的threadlocals字段。(从类实例对象的角度抽象的去看Java中的线程!!!)
③ ThreadLocalMap中Entry的Key不管是否使用弱引用都有内存泄露的可能。引起内存泄露主要在于ThreadLocal对象和Entry中的Value对象,因此要确保每次使用完之后都remove掉Entry!