什么是ThreadLocal?
线程本地存储技术,能够做到线程隔离
为什么要使用ThreadLocal?
同一个线程里面,可以跨对象的去共享数据,同时保证线程安全,每个线程只访问自己的数据
怎么使用?
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set(value); 存储线程隔离的数据
threadLocal.get(); 获取当前线程存储的数据
使用场景
如果想在同一个对象中,跨多个对象的去共享数据,同时保证线程安全的情况下,使用ThreadLocal
比如:
用户登录成功后,需要将登录用户信息保存起来,以方便在系统中的任何地方都可以使用到,那么此时就可以使用ThreadLocal来实现。例如:Spring Security中的ThreadLocalSecurityContextHolderStrategy类
比如
在Tomcat中,如果我们想在 filter,controller,service之间方便的去共享数据,并且保证每个请求只能访问自己的数据,此时可以使用ThreadLocal
工作原理
每个线程对象中都有一个自己的 ThreadLocalMap 对象,每个线程负责把自己的数据存到ThreadLocalMap 对象中,如上图,tomcat给每个请求分配一个线程(调用threadLocal.set(user1)在当前线程中存放user1数据),每个线程中有一个成员变量 threadLocals,成员变量指向每个线程中的 ThreadLocalMap 对象,当调用threadLocal.get()方法时,就可以取出当前线程的资源
get方法
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();
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);
}
ThreadLocal
为什么会内存泄漏
ThreadLocalMap
使用ThreadLocal
的弱引用作为key
,如果一个ThreadLocal
没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal
势必会被回收,这样一来,ThreadLocalMap
中就会出现key
为null
的Entry
,就没有办法访问这些key
为null
的Entry
的value
,如果当前线程再迟迟不结束的话,这些key
为null
的Entry
的value
就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。根本原因是因为每个线程中的 ThreadLocalMap 生命周期和线程的生命周期是一样长的
其实,ThreadLocalMap
的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal
的get()
,set()
,remove()
的时候都会清除线程ThreadLocalMap
里所有key
为null
的value
。
但是这些被动的预防措施并不能保证不会内存泄漏:
- 使用
static
的ThreadLocal
,延长了ThreadLocal
的生命周期,可能导致的内存泄漏(参考ThreadLocal 内存泄露的实例分析)。 - 分配使用了
ThreadLocal
又不再调用get()
,set()
,remove()
方法,那么就会导致内存泄漏。
如何解决内存泄漏
在最后要被线程池回收之前,使用 threadLocal.remove() 方法,把当前threadlocalmap中的数据清空掉
key为什么要设置成弱引用?
下面我们分两种情况讨论:
- key 使用强引用:引用的
ThreadLocal
的对象被回收了,但是ThreadLocalMap
还持有ThreadLocal
的强引用,如果没有手动删除,ThreadLocal
不会被回收,导致Entry
内存泄漏。 - key 使用弱引用:引用的
ThreadLocal
的对象被回收了,由于ThreadLocalMap
持有ThreadLocal
的弱引用,即使没有手动删除,ThreadLocal
也会被回收。value
在下一次ThreadLocalMap
调用set
,get
,remove
的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap
的生命周期跟Thread
一样长,如果都没有手动删除对应key
,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal
不会内存泄漏,对应的value
在下一次ThreadLocalMap
调用set
,get
,remove
的时候会被清除。
因此,ThreadLocal
内存泄漏的根源是:由于ThreadLocalMap
的生命周期跟Thread
一样长,如果没有手动删除对应key
就会导致内存泄漏,而不是因为弱引用。
而且ThreadLocalMap在内部的set,get和扩容时都会清理掉泄漏的Entry(Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value。)内存泄漏完全没必要过于担心。
为什么value不是弱引用
先说答案:假如value被设计成弱引用,那么很有可能当你需要取这个value值的时候,取出来的值是一个null。
Map<WeakReference<Integer>, WeakReference<Integer>> map = new HashMap<>(8);
WeakReference<Integer> key = new WeakReference<>(1);
WeakReference<Integer> value = new WeakReference<>(300);
map.put(key,value);
System.out.println("put success");
Thread.sleep(1000);
System.gc();
System.out.println("get " + map.get(key).get());
// put success
// get null
使用ThreadLocal的目的就是要把这个value存储到当前线程中,并且希望在你需要的时候直接从当前线程中拿出来,那么意味着你的value除了当前线程对它持有强引用外,理论上来说,不应该再有其他强引用,否则你也不会把value存储进当前线程。但是一旦你把本应该强引用的value设计成了弱引用,那么只要jvm执行一次gc操作,你的value就直接被回收掉了。当你需要从当前线程中取值的时候,最终得到的就是null。
使用threadlocal的缺点
脏读(在一个线程中读取到了不属于自己的信息就叫做脏读)
脏读产生原因:线程池复用了线程,和这个线程相关的静态属性也复用,所以就导致了脏读
//脏读问题
private static ThreadLocal<String> threadLocal=new ThreadLocal<>();
public static void main(String[] args) {
ThreadPoolExecutor executor=new ThreadPoolExecutor(1,1,0,
TimeUnit.SECONDS,new LinkedBlockingQueue<>(1000));
for (int i=0;i<2;i++){
executor.submit(new MyThreadLocal());
}
}
static class MyThreadLocal extends Thread{
//静态变量被复用
private static boolean flag=false;
@Override
public void run() {
String tname=this.getName();
if(!flag){
//第一次执行
threadLocal.set(tname);
System.out.println(tname+"设置了"+tname);
flag=true;
}
System.out.println(tname+"得到了"+threadLocal.get());
}
}
解决脏读问题:
a.避免使用静态变量
b.使用完之后,执行remove移除
内存溢出(配合线程池使用的时候才会出现)
当一个线程使用完资源之后,没有释放资源,或者说释放资源不及时就是内存溢出
//设置最大内存的是5M
public class ThreadLocalDemo11 {
private static ThreadLocal<MyThreadLocal>threadLocal=new ThreadLocal<>();
public static void main(String[] args) {
ThreadPoolExecutor executor=new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000));
for(int i=0;i<10;i++){
executor.execute(new Runnable() {
@Override
public void run() {
MyThreadLocal myThreadLocal=new MyThreadLocal();
threadLocal.set(myThreadLocal);
System.out.println(Thread.currentThread().getName()+
"设置了值");
}
});
}
}
static class MyThreadLocal{
private byte[]bytes=new byte[1*1024*1024];
}
}
解决内存溢出:使用remove移除
内存溢出原因(配合线程池使用的时候才会出现内存溢出):
1.线程池是长生命周期,使用完后不会主动释放资源
2.Thread–>ThreadLocalMap–>Entry[ ] key,value(1MB资源) key是弱引用,value是强引用,垃圾回收器不会回收value资源