我们先了解下ThreadLocal有哪些使用场景,然后再继续看它的内部原理
一、ThreadLocal有两种典型的使用场景:
1、ThreadLocal用做保存每个线程独享的对象,它会为线程对象创建一个副本,这样每个线程都可以修改自己所有拥有的副本,不会影响其他线程的副本,确保了线程安全。
2、ThreadLocal用作每个线程内需要独立保存信息以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样,前面执行的方法在保存了这个信息之后,后续的方法就可以通过ThreadLcoal直接获取到,避免了传参,类似一个全局变量的概念。
场景1
通常用于保存线程不安全的工具类,典型的需要使用的类就是SimpleDateFormat。每个Threa内部都有自己的实例副本,且该副本只能由当前Thread访问到并使用,相当于每个线程内部的本地变量。它是独享副本,不是共用的,所以不存在多线程之间共享的问题。
二、ThreadLocal是否是用来解决共享资源的多线程访问的呢?
不是,ThreadLocal虽然可以用于解决多线程情况下的线程安全问题,但是资源不是共享的,而是每个线程独享的。
ThreadLocal相对于锁的方式解决多线程安全问题,它换了个思路,它是把各个资源变成了独享的,巧妙的避免同步操作。
ThreadLocal解决并发问题的主要思路,可以在initialValue中new出自己线程独享的资源,而多个线程之间,它们所访问的对象本事是不共享的,自然就不存在任何并发问题。
如果把ThreadLocal中的资源用static修饰,让它变成一个共享资源那么即便使用了ThreadLocal,同样也会有线程安全问题。
ThreadLocal和synchronized都能解决线程安全问题,他们的关系是什么呢
当ThreadLcoal用于解决线程安全问题时,也就是把一个对象给每个线程都生成一份独享的副本的这种场景下,ThreadLocal和synchronized都可以理解为是用来保证线程安全的手段。
**ThreadLocal解决安全问题的时候,就是把一个对象给每个线程,都生成一份独享的副本的时候,这个时候ThreadLocal和Synchronized都可以理解成保证线程安全的手段。synchronized主要是用于临街资源的分配,在同一时刻限制最多只有一个线程能访问该资源。他们相比,synchronized的效率会更低一些,但是花费的内存也更少。在这种场景下,ThreadLocal和synchronized都可以达到线程安全目的。
**
**对于ThreadLocal而言,它还有不同的使用场景,比如当ThreadLocal用于让多个类能更方便地拿到我们希望给每个线程独立保存这个信息的场景下时(比如每个线程都会对应一个用户信息,也就是user对象),这种场景下,ThreadLocal侧重的是避免传参。此时他们是两个不同维度的工具。
**
三、多个ThreadLocal在Thread中的threadlocals里是怎么存储的
在Thread类中,多个ThreadLocal是怎么存储的,ThreadLocal 、ThreadLocalMap的关系,每个Thread对象中都持有一个ThreadLocalMap类型的成员变量,key是ThreadLocal的引用。一个Thread里面只有一个ThreadLocalMap,一个ThreadLocalMap里面有很多的ThreadLocal。
ThreadLocal的源码分析
get方法
首先我们来看一下 get 方法,源码如下所示:
public T get() {
//获取到当前线程
Thread t = Thread.currentThread();
//获取到当前线程内的 ThreadLocalMap 对象,每个线程内都有一个 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//获取 ThreadLocalMap 中的 Entry 对象并拿到 Value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果线程内之前没创建过 ThreadLocalMap,就创建
return setInitialValue();
}
这是 ThreadLocal 的 get 方法,可以看出它利用了 Thread.currentThread 来获取当前线程的引用,并且把这个引用传入到了 getMap 方法里面,来拿到当前线程的 ThreadLocalMap。
然后就是一个 if ( map != null ) 条件语句,那我们先来看看 if (map == null) 的情况,如果 map == null,则说明之前这个线程中没有创建过 ThreadLocalMap,于是就去调用 setInitialValue 来创建;如果 map != null,我们就应该通过 this 这个引用(也就是当前的 ThreadLocal 对象的引用)来获取它所对应的 Entry,同时再通过这个 Entry 拿到里面的 value,最终作为结果返回。
值得注意的是,这里的 ThreadLocalMap 是保存在线程 Thread 类中的,而不是保存在 ThreadLocal 中的。
getMap方法
下面我们来看一下 getMap 方法,源码如下所示:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到,这个方法很清楚地表明了 Thread 和 ThreadLocalMap 的关系,可以看出 ThreadLocalMap 是线程的一个成员变量。这个方法的作用就是获取到当前线程内的 ThreadLocalMap 对象,每个线程都有 ThreadLocalMap 对象,而这个对象的名字就叫作 threadLocals,初始值为 null,代码如下:
ThreadLocal.ThreadLocalMap threadLocals = null;
set 方法
下面我们再来看一下 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);
}
set 方法的作用是把我们想要存储的 value 给保存进去。可以看出,首先,它还是需要获取到当前线程的引用,并且利用这个引用来获取到 ThreadLocalMap ;然后,如果 map == null 则去创建这个 map,而当 map != null 的时候就利用 map.set 方法,把 value 给 set 进去。
可以看出,map.set(this, value) 传入的这两个参数中,第一个参数是 this,就是当前 ThreadLocal 的引用,这也再次体现了,在 ThreadLocalMap 中,它的 key 的类型是 ThreadLocal;而第二个参数就是我们所传入的 value,这样一来就可以把这个键值对保存到 ThreadLocalMap 中去了。
ThreadLocalMap 类,也就是 Thread.threadLocals
下面我们来看一下 ThreadLocalMap 这个类,下面这段代码截取自定义在 ThreadLocal 类中的 ThreadLocalMap 类:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
//...
}
ThreadLocalMap 类是每个线程 Thread 类里面的一个成员变量,其中最重要的就是截取出的这段代码中的 Entry 内部类。在 ThreadLocalMap 中会有一个 Entry 类型的数组,名字叫 table。我们可以把 Entry 理解为一个 map,其键值对为:
键,当前的 ThreadLocal;
值,实际需要存储的变量,比如 user 用户对象或者 simpleDateFormat 对象等。
ThreadLocalMap 既然类似于 Map,所以就和 HashMap 一样,也会有包括 set、get、rehash、resize 等一系列标准操作。但是,虽然思路和 HashMap 是类似的,但是具体实现会有一些不同。
比如其中一个不同点就是,我们知道 HashMap 在面对 hash 冲突的时候,采用的是拉链法。它会先把对象 hash 到一个对应的格子中,如果有冲突就用链表的形式往下链,如下图所示:
但是 ThreadLocalMap 解决 hash 冲突的方式是不一样的,它采用的是线性探测法。如果发生冲突,并不会用链表的形式往下链,而是会继续寻找下一个空的格子。这是 ThreadLocalMap 和 HashMap 在处理冲突时不一样的点。
以上就是本节课的内容。
总结:Thread、 ThreadLocal 和 ThreadLocalMap 这三个非常重要的类的关系:一个 Thread 有一个 ThreadLocalMap,而 ThreadLocalMap 的 key 就是一个个的 ThreadLocal,它们就是用这样的关系来存储并维护内容的。
四、内存泄露-为何每次用完ThreadLocal都要调用remove()
内存泄露指当某一个对象不再有用的时候,占用的内存却不能被回收
通常情况下,如果一个对象不再有用,那么垃圾回收器GC,就应该把这部分内存给清理掉
否则,如果对象没有用却一直不能被回收,会导致可用内存越来越少,最后会发生内存不够用的错误。
Key的泄露
线程在访问ThreadLocal之后,会在它的ThreadLocalMap里面的Entry中去维护该ThreadLocal变量与具体实例的映射。
强引用不能gc
弱引用不影响gc,弱引用,如果这个对象只能被引用关联(没有任何强引用关联)那么这个对象就可以被回收,所以弱引用不会阻止gc。
ThreadLocalMap的每个Entry都是一个对key的弱引用,但是这个Entry包含了一个对value的强引用。
Value的泄露
在执行ThreadLocal的set、remove、rehash等方法时,它都会扫描key为null的Entry,如果发现entry的key为null,代表它所对应的value也没有作用了
所以它就会把对应的value置为null,这样value对象就可以被正常回收了。
假设这个ThreadLocal已经不被使用了,
那么实际上set、remove、rehash方法也不会被调用
与此同时,如果这个线程又一致存活,不终止的话,那么刚才的那个调用链就一直存在,也就导致了value的内存泄露。
如何避免内存泄露
调用ThreadLocal的remove方法
调用这个方法就可以删除对应的value对象,可以避免内存泄露。