作用和使用场景
-
threadLocal是线程本地储存,在一个线程中通过threadLocal.set()方法赋值一个对象T(假设是T@48533e64),在赋值后当前线程内通过threadLocal.get()方法得到的值都是T@48533e64对象,以便后续处理时用到。使用场景:跨类跨方法传递数据。假如你再A类计算得到一个值,而在C类的某个方法中要用到该值,那么需要在C类的该方法上新增一个参数用于传递该值,但是使用threadLocal后,就可以省略这个参数,直接通过threadLocal.get()获取得到。
-
每个线程设定的对象与其他线程是隔离的,线程安全控制使用场景:典型的如多个线程与数据库交互时,每个线程都拥有各自的Connections对象;再比如在对日期进行格式化的工具类中,如果用静态的SimpleDateFormat成员变量去做格式化,那么可能会发生错误,可以通过静态的ThreadLocal<SimpleDateFormat>处理解决。
内存模型图
看如下代码示例及其对应的内存模型图
public class ThreadLocalTest {
@Test
public void test() {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
Thread thread = new Thread(() -> threadLocal.set("value"));
thread.start();
}
}
ThreadLocalMap
从内存图知,ThreadLocalMap是一个容器类,key是threadLocal对象,value是我们要保存的对象,threadLocal赋值的key和value都是保存在各自线程的threadLocalMap对象中的。threadLocalMap对象底层也是entry数组(注意这个entry和hashMap中的entry不是同一个类),而与HashMap不同的是,他们解决hash冲突的方式不一样,threadLocalMap使用开放寻址法(线性探测),而hashMap使用的是链地址法。
-
开放寻址法(线性探测)
当我们往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为。
缺点:当散列表中插入的数据越来越多时,散列冲突发生的可能性就会越来越大,空闲位置会越来越少,线性探测的时间就会越来越久。极端情况下,我们可能需要探测整个散列表,所以最坏情况下的时间复杂度为 O(n),因此该方法适用于存储少量元素的hash表
为什么threadLocalMap使用线性探测方法?
答:我们在编码时,大多数情况下只设置了一个或几个threadLocal对象,不会有太多threadLocal对象,因此由用户编码而存入到threadLocalMap中的元素大多也就一个或几个而已,因此threadLocalMap存放的元素数量很少,适合使用线程探测法来解决hash冲突
-
链地址法
每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向 链表连接起来
ThreadLocal源码详解
get方法
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 得到当前线程的threadLocals属性对象(即Entry[]对象)
ThreadLocalMap map = getMap(t);
if (map != null) {
// 以threadLocal对象做为key,通过“开放定址法”去寻找对应位置的entry
// 特别注意:在寻找过程中,会对遇到的无效entry进行清除以避免内存泄漏
// 解释:"清除无效entry"即打断如内存图所示的”引用7“和”引用5“的引用
ThreadLocalMap.Entry e = map.getEntry(this);
// 若该entry对象不为null则返回该entry.value
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 若当前线程的threadLocals属性对象为null,或者未找对应的entry,返回初始化的值
return setInitialValue();
}
set方法
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 得到当前线程的threadLocals属性对象(即Entry[]对象)
ThreadLocalMap map = getMap(t);
if (map != null)
// 以threadLocal对象做为key,通过“开放定址法”去寻找对应位置的entry,若找到正确的entry,则直接对该entry.value赋值,若未找到则new一个新的entry放在正确的位置。
// 特别注意:在寻找entry的过程中,会对遇到的无效entry进行清除避免内存泄漏。必要时会进行扩容(扩容前会清除所有的无效entry再判断是否需要扩容)
// 解释:"清除无效entry"即打断如内存图所示的”引用7“和”引用5“的引用
map.set(this, value);
else
// 新建一个ThreadLocalMap对象,赋值到该线程t的threadLocals属性上,然后将key(threadLocal对象)和value对象放到map中正确的位置
createMap(t, value);
}
remove方法
public void remove() {
// 得到当前线程的threadLocals属性对象(即Entry[]对象)
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 相当于将内存图中的”引用5、引用6、引用7“全部指向null
m.remove(this);
}
内存泄漏问题
内存泄漏现象是存在无用的且一直无法被垃圾回收器GC掉的对象。而使用threadLocal涉及到的内存泄漏指的是各个线程中threadLcoalMap属性对象的entry数组引用到的一些无用的对象(即无效的entry)。这里分如下2个方面来讲
-
一、threadLocal生命周期比线程的生命周期短(同时也解释了Entry为什么要用弱引用) 根据内存图,在threadLocal生命周期结束后,"引用1"断开,threadLocal对象只会被“弱引用6”引用到,由于是弱引用,GC时,threadLocal对象将会被回收,此时该entry对应的key就是null了,对于这种entry我称呼其为无效entry。这时该无效entry以及它引用的value对象都是无用的对象,但此时还占用的内存。不过在该线程中,一旦有其他的threadLocal对象在进行get或set等操作时,根据之前【ThreadLocal源码详解】可知,该线程的threadLocalMap对象中的无效entry就有可能会被清除掉,尤其在其扩容时,无效entry一定会被清除掉。故这种情况下是不会产生内存泄漏的。
-
思考:如果"引用6"不是弱引用而是强引用,那么就无法判断threadLocalMap中哪些是无效entry对象,就不能有针对性的进行清除操作,从而导致内存泄漏了。这也是
java.lang.ThreadLocal.ThreadLocalMap.Entry
类在设计时继承WeakReference类使用弱引用的原因。
-
-
二、threadLocal生命周期比线程的生命周期长
该情况也是我们编码时的常用情况,一般将threadLocal申明为静态的常量的成员变量,供各个线程去保存自己的数据。 当线程的生命周期结束后,内存图中的”引用2“断开,此时thread对象及其直接或间接引用的对象都变成了GC Root不可达对象(在内存图中指的是“thread对象、threadLcoalMap对象、entry[]对象、entry对象、value对象),即变成了垃圾,在GC后这些垃圾对象都会被回收掉,因此这种情况也不会产生内存泄漏。
特别注意:由于我们在实际开发过程中,我们一般是在这些线程中使用threadLocal。1)处理API接口的主线程;2)线程池中获取的线程。以上2种线程都有一个特点,那就是在处理完一个请求之后线程不会立即消亡,会被放到线程池中以便处理后续的请求(即线程的复用,当然可以配置超过指定的空闲时间线程消亡)。既然不会立即消亡,那么该线程所引用到所有entry都没办法被垃圾回收器回收,此时就产生了内存泄漏;更糟糕的是,该线程在处理上一次请求时赋值到threadLocal中的数据,在下一请求处理时依然会被获取到,而这个值对于这次的请求来说是一个脏值,即读取了脏数据。因此要解决中这种情况下导致的内存泄漏和 脏读的问题,那么在线程每一次处理完请求之后都需要调用threadLocal.remove()方法。
-
内存泄漏问题总结
通过上面两种情况的分析知道,只要对应的线程消亡或者threadLocal对象消亡,不管有没有调用remvoe方法都不会发生内存泄漏问题。只有当线程和threadLocal都不消亡时才会存在内存泄漏问题,而这种情况一般都是将threadLocal对象设置为静态常量类型,在线程池中使用。而线程池,我们一般都会配置最大活动线程数(即线程池中最多活动的线程)和空闲超时时间(即空闲线程超过这个时间会消亡),随着线程池中活动线程的数量上升直到达到最大线程活动数量时,有关threadLocal所产生的内存占用也会随之上升并达到一个顶值,即使没有调用remove方法,那么产生的内存泄漏也不会无上限的增加,危害不算特别大,只是这种情况下的脏读问题是一个严重的问题,因此这种情况一定要调用remove方法避免产生脏读(同时也避免了内存泄露问题),即对于静态常量的threadLoca在线程池(不管是tomcat线程池还是自己构造的线程池)中进行操作完成后一定要调用remove方法以避免产生脏读和内存泄露