ThreadLocal可以解决多线程中的什么问题,原理是什么
多线程访问同一个资源时,需要加锁,只有一个线程访问,才能保证操作的原子性。ThreadLocal保证原子性的原理跟锁不一样,是把资源分成N份,每个线程一份,这样就不需要考虑加锁了,ThreadLocal只有当前线程才能访问到相关资源。
原来看到一篇文章,将锁和ThreadLocal的区别写的很生动,文章地址记不得了,先简单说说:你有两个孩子,都喜欢小兔子玩偶,家里就一个。这时候两个孩子就相当于两个线程,小兔子玩偶就是资源, 两个小朋友都想玩,就是竞争资源。
- 加锁:孩子的爸爸过来,要求一个孩子玩10分钟,然后轮流玩,中间大人还需要管控顺序,保证每个孩子只能玩10分钟,大人能累死,小朋友估计也少不了哭。
- ThreadLocal:家里有两个孩子,直接买两个兔子玩偶,一个小朋友一个,这样就不需要考虑小朋友争抢了,大人也轻松了。
当然ThreadLocal的应用场景比较少,但是也可以解决一部分的多线程问题。
常见使用场景:
- CurrentUser,mvc架构中,需要获取当前操作用户,就可以利用ThreadLocal作为User资源,因为每个线程只可能有一个登录用户。
- SimpleDateFormate是线程不安全的,那么可以用ThreadLocal包装SimpleDateFormate,这样保证只有当前线程才能访问内部的SimpleDateFormate。
- 参数传递(局部缓存):尤其是针对老旧功能的修改,例如:原来订单商品是没有服务商品概念的,现在需要添加服务商品,要么修改原有的接口方法,或者入参的结构,但是也可以用ThreadLocal存储响应的服务商品,这样所有的方法中,都可以获取服务商品信息。其实也是局部缓存,缓存了整个线程全流程的共享数据。
自定义ThreadLocal
ThreadLocal设置的数据,只对当前线程有效,第一个想到的是Map,key=线程,value=设置的值。
public class MyThreadLocal<T> {
private Map<Thread, T> localMap = new HashMap<>();
public T set(T t) {
return localMap.put(Thread.currentThread(), t);
}
public T remove() {
return localMap.remove(Thread.currentThread());
}
public T get() {
return localMap.get(Thread.currentThread());
}
}
public class CurrentUserMyThreadLocal {
private static MyThreadLocal<User> USER_TL = new MyThreadLocal<User>();
public static User set(User user) {
return USER_TL.set(user);
}
public static User get() {
return USER_TL.get();
}
public static User remove() {
return USER_TL.remove();
}
public static void print() {
USER_TL.print();
}
}
public class MyThreadLocalDemo {
public static void main(String[] args) {
CurrentUserMyThreadLocal.set(new User(1L, "测试"));
test();
int i = 0;
while (i < 100000) {
i++;
}
System.out.println(CurrentUserMyThreadLocal.get());
}
private static void test() {
System.out.println(CurrentUserMyThreadLocal.get());
new Thread(new Runnable() {
@Override
public void run() {
CurrentUserMyThreadLocal.set(new User(2L, "test2"));
}
}).start();
}
}
通过MyThreadLocalDemo,设置后,在其他方法中也是可以获取的。
缺点:
- 如果存在很多MyThreadLocal变量,会有很多个HashMap存在,会浪费一部分的内存。
- 无法保证只有当前线程能访问。
- 当线程退出时,会导致map中部分key=null的情况,存在内存泄漏。
ThreadLocal的数据结构
ThreadLocal用法
public class ThreadLocalDemo {
private static ThreadLocal<User> userTL = new ThreadLocal<User>();
public static void main(String[] args) {
userTL.set(new User());
}
}
查看userTL.set方法。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
可以看到数据存储在ThreadLocalMap的结构中。但是key却不是想想中的Thread,而是this,从方法调用链上说,this就是threadLocal变量。
ThreadLocalMap
ThreadLocalMap的来源:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
通过上述的getMap发现ThreadLocalMap源自Thread,但是ThreadLocalMap又是ThreadLocal的内部类,但是ThreadLocalMap的key又是ThreadLocal实例,这是很奇怪的引用链。
示例:
ThreadLocalMap为什么是Map
正常情况下,想要通过Thread传递数据,可以直接定义一个Object变量即可,。如下:
class Thread {
private Object localVar;
public void set(Object var) {
this.localVar = var;
}
public Object get(){
return localVar;
}
}
通过set初始化,通过get使用。但是当需要传递多个value时,通过localVar持有变量就存在覆盖。因此当需要传递多个值时,并且按需获取时,最好的数据结构就是Map,因此进化为:
class Thread {
private Map<String,Object> localMap;
public void set(String key,Object var) {
localMap.put(key,var);
}
public Object get(String key){
return localMap.get(key);
}
}
ThreadLocalMap的key=ThreadLocal
上述确认,利用Thread传递数据,必须是通过map持有数据,才能做到按需获取。map作为value的存储,就涉及到key的问题。
- 字符串作为map的key:
就像上述的实例一样,使用时需要:
public static void main(String[] args) {
Thread.currentThread.set("currentUser",currentUser);
Thread.currentThread.get("currentUser");
}
设置时,需要手写key,获取时还需要手写key,这就存在手动输错的可能,当然也可以利用常量表示字符串key。
- 对象作为map的key:
刚才使用String字符串作为key,在get时,需要手动再输入一遍,因此可以使用对象作为Map的key。例如:
class ThreadLocal {
}
class Thread {
private Map<ThreadLocal, Object> localMap;
public void set(ThreadLocal key, Object var) {
localMap.put(key, var);
}
public Object get(ThreadLocal key) {
return localMap.get(key);
}
public static void main(String[] args) {
ThreadLocal tl = new ThreadLocal();
Thread.currentThread.set(tl, currentUser);
Thread.currentThread.get(tl);
}
}
定义ThreadLocal变量,这样set和get时,只需要tl作为key即可。还是有一些缺陷:1 当前Thread的Map没有办法处理泛型,需要进行类型转换;2 set和get时仍然繁琐。
- 对象作为key,同时作为获取vaue的入口
上述对象作为key时,获取value时,代码仍然有点冗余,是否可以考虑对象既作为map的key,同时也作为getValue的发起方。
class ThreadLocal<V> {
public void set(V v){
Thread thread = Thread.currentThread;
thread.map.put(this,v);
}
public V get(){
Thread thread = Thread.currentThread;
return thread.map.get(this);
}
public static void main(String[] args) {
ThreadLocal<CurrentUser> tl = new ThreadLocal();
tl.set(new CurrentUser(1L,"test"));
tl.get();
}
}
通过上述优化,就可以很简洁的使用ThreadLocal,其实通过上述变化,也能发现一个问题,ThreadLocal其实可以看做持有value的一个变量,如下所示:
class ThreadLocal<V> {
private V value;
public void set(V v) {
this.value = v;
}
public V get() {
return value;
}
}
只不过ThreadLocal并没有实际持有value,因为ThreadLocal如果持有value,就需要考虑多个Thread的value区分,因此ThreadLocal只提供了Thread.map的访问出入口。
ThreadLocalMap为什么是ThreadLocal的内部类
通过上述分析,Thread.map的访问出入口是ThreadLocal,因此ThreadLocalMap就不允许通过常规的get和set进行访问了,因此相关方法必须是私有,但是set和get的私有,也导致ThreadLocal也无法访问,而内部类的private方法可以被外部类访问,因此ThreadLocalMap是ThreadLocal的内部类。
1 把Thread中的map必须进行包装,不允许通过正常map的get和set方法访问,上述方法private。2 包装后的map类型,必须是ThreadLocal的内部类(外部类可以访问内部类的私有方法)。
经过这样处理:Thread的ThreadLocalMap变量,负责存储所有的value,ThreadLocal作为ThreadLocalMap的set/get入口,ThreadLocal作为ThreadLocalMap的key,简化ThreadLocalMap value的set、get操作。
ThreadLocal传递数据,其实有两种方式:
value分散:指一个Thread需要传递的值,分散在Map中。即需要一个Map,key=Thread,value=Map(key=业务key,value=数据)。
value聚集:Thread提供一个Map(key=ThreaLocal,value=数据),存放需要该Thread传递的数据,通过ThreadLocal对外暴露访问。
ThreadLocal会产生内存泄漏?
在ThreadLocal的数据结构中,已经画出了相关的引用图例,发现Thread.ThreadLocalMap中的key=ThreadLocal,但是却对key做了弱引用(gc时,如果对象被弱引用持有,那么是可以直接回收),为什么不用强引用。
ThreadLocalMap的回收
- 线程消亡:线程消亡时,ThreadLocalMap会被设置为null,此时就不存在ThreadLocal的内存泄漏。存储都没有了,哪来的泄漏。
- 线程长时间存活:主要是线程池内的线程,这些线程存活时间很长,ThreadLocalMap会长时间存在,因此就会存在一个引用链:ThreadLocalRef->ThreadLocalMap->Entry->key->ThreadLocal。
ThreadLocalMap强引用。
因此如果key->ThreadLocal存在强引用时,就会导致ThreadLocal对象无法回收,因为GC可达,但是ThreadLocal变量缺已经被回收。尤其是ThreadLocal作为局部变量,或者普通属性时。例如:
局部变量:
public static void main(String[] args) {
ThreadLocal<CurrentUser> tl = new ThreadLocal();
tl.set(new CurrentUser(1L,"test"));
tl.get();
}
普通属性:
public class BizService {
private ThreadLocal<User> userTL = new ThreadLocal();
}
上述ThreadLocal的用法,也存在内存泄漏,因为ThreadLocal变量(指针)随着方法结束或者BizService被回收,而被回收,但是ThreadLocal对象,因为ThreadLocalMap的强引用而存活,但是我们已经没有ThreadLocal对象的访问入口(变量已经被回收),因此导致Entry也无法被正常访问,那么此时就会导致Entry和ThreadLocal的内存泄漏有。
综上所述:ThreadLocalMap的key为强引用时,尤其是作为局部变量和普通属性时,会导致ThreadLocal和Entry的内存泄漏。
ThreadLocalMap key 弱引用
弱引用:当一个对象仅被弱引用持有,那么该对象gc时,会被回收,再次通过弱引用获取该对象,返回null。
因此如果ThreadLocalMap的key为弱引用,当ThreadLocal为局部变量和普通属性时,不会影响ThreadLocal对象的回收。但是仍然存在内存泄漏。
因为ThreadLocalMap的key为弱引用,那么上述情况gc后,会导致Entry的key=null,同样也是因为无法通过ThreadLocal(被gc掉了)访问Entry,导致Entry的内存泄漏。
为了解决上述问题:ThreadLocal在set和get操作时,添加了这对Entry中key=null情况的排查,当key=null时,释放持有的value,减少内存泄漏的风险。
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);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
综上所述:ThreadLocalMap的弱引用,只是减少内存泄漏的风险,但是不能避免内存泄漏,举个极端的例子:ThreadLocal整个声明周期中,就只有set和get各一次,这种情况下,无法触发ThreadLocal的自动清除key=null的机制,那么内存泄漏仍然存在。
ThreadLocal正确使用
实际情况下,我们都一般都不把ThreadLocal作为局部变量和普通属性,一般都是作为静态变量。
public class CurrentUser {
private static ThreadLocal<User> CURRENT_USER = new ThreadLocal<>();
public static User get() {
return CURRENT_USER.get();
}
public static void set(User user) {
CURRENT_USER.set(user);
}
public static void remove() {
CURRENT_USER.remove();
}
}
上述功能中,ThreadLocal作为静态变量,作为GCRoot,正常情况不会被gc,那么ThreadLocalMap key=弱引用,其实没有什么实际效果,因为ThreadLocal变量强引用ThreadLocal对象,key不会为null,那么就不会触发ThreadLocal的get、set自清理机制。
因此上述情况,会存在内存浪费的情况,尤其是线程长时间存活。例如:线程执行业务A时,用到了CurrentUser,执行业务B时,用到了CurrentOrg,甚至以后都不会执行业务A,那么CurrentUser在ThreadLocalMap中的数据存储就是无用的,浪费内存的。
因此当Thread执行业务前,需要set ThreadLocal,防止数据污染,执行完业务后,要手动remove,方式内存浪费。
数据污染:因为线程存在重复执行同一个业务方法,但是不同业务方法的ThreadLocal value不一样,如果不重置ThreadLocal,存在Thread访问到上次执行时,设置的ThreadLocal,导致数据污染。
ThreadLocalMap value 弱引用
如果value也如同key一样,设置为软引用,当ThreadLocal设置的value,有可能被gc回收掉,导致get时为null。
public static void main(String[] args) throws Exception {
Map<WeakReference<Integer>, WeakReference<Integer>> map = new HashMap<>(8);
WeakReference<Integer> key = new WeakReference<>(666);
WeakReference<Integer> value = new WeakReference<>(777);
map.put(key,value);
System.out.println("put success");
Thread.sleep(1000);
System.gc();
System.out.println("get " + map.get(key).get());
}
输出时:get null
总结
ThreadLocal 内存泄漏的根源是:由于ThreadLocalMap 的生命周期跟 Thread 一样长,而 Thread与ThreadLocal 的生命周期不一样长, 如果没有手动删除对应 key 就会导致内存泄漏。
ThreadLocalMap的hash冲突
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
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();
}
通过线性探测解决hash冲突,首先根据hashCode与数组长度&运算获取下标,从下标开始,循环遍历整个数组,直到找到为空的。
缺点:当ThreadLocal冲突较多时,可能存在O(n)的遍历,同时移除某一个元素,需要同步前移动该元素的后续的元素。
ThreadLocal的应用场景
替代参数的显式传递(Thread上下文)
例如:methodA已经有4个参数了,现在需要进行添加另外的参数,直接在methodA中添加第五个参数,影响面大,需要修改方法的入参(或者重构),因此可以通过ThreadLocal存储变量数据,方法中直接获取使用。
局部缓存信息(Thread生命周期内部)
上述举例中,存在CurrentUser、CurrentOrg,这些都可以认为是局部缓存信息。
绑定事务、链接。
进行事务操作时,将事务与线程绑定,保证事务的唯一性。
绑定链接也是相同的原理,一一对应。