前言
前几天在京东的同学给我打了个电话,聊了下家常,技术宅的我多嘴问了最近有没有学啥? 他说最近有点忙,但抽空也看了几篇博客,他说我考考你吧,我说可以啊,他问我: ThreadLocal 使用不当会导致 OOM 吗?我不假思索的回答:会。他继续追问道:为什么? 我说:因为 ThreadLocal 和操作它的线程绑定在一起,如果操作他的线程不被销毁,与之关联的 ThreadLocal 不会被 GC 。因为使用线程大多都是通过线程池来创建的,因此只要该线程活跃,就不会被线程池销毁,如果我们使用的时候忘记调用 ThreadLocal 的 remove
方法,则 ThreadLocal 保存的值无法被 GC ,如此多了就会发生 OOM 。然后他突然问了一句:为啥 Thread
里的threadLocals
属性的key
是弱引用类型的? 这个之前我是不知道的。然后他给我解释了一下,这也是这篇文章的由来,好记性不如烂笔头,顺便验证一下他说的,也是对知识的巩固。
ThreadLocal
多个线程间共享变量,可能会造成线程不安全的问题,需要加锁来实现线程安全,但是加锁会降低系统的吞吐量。
但是有些变量就不需要线程间共享。比如数据库连接池里的连接,我们可以通过串行线程封闭技术来安全的使用连接池中的连接。一个线程A从连接池中把连接拿走,连接池保证不把该连接给别的线程,线程A同样不会把连接发布出去,用完之后返回给连接池,这样一个连接总是在一个线程中使用,不会同时被两个线程操作。线程A保存数据库连接就可以使用 ThreadLocal 来保存,可以在多个方法中获取操作数据库,用完删除即可。(生产者和消费者模式也是使用串行线程封闭技术,大家可以考虑下。)
ThreadLocal 里的数据,其它线程无法访问,只要使用者不把数据发布出去,就可以安全操作它们。我们来看看如何一个 demo 来看下 ThreadLocal 如何使用:
public class NotThreadSafe {
private ThreadLocal<Integer> count = ThreadLocal.withInitial(() -> Integer.MIN_VALUE);
public void increment() {
Integer countValue = count.get();
countValue++;
count.set(countValue);
}
public void decrement() {
Integer countValue = count.get();
countValue--;
count.set(countValue);
}
public int getValue() {
return count.get();
}
public void remove() {
count.remove();
}
public static void main(String[] args) {
NotThreadSafe notThreadSafe = new NotThreadSafe();
new Thread(() -> {
try {
notThreadSafe.increment();
System.out.println("increment i=" + notThreadSafe.getValue());
notThreadSafe.decrement();
System.out.println("decrement i=" + notThreadSafe.getValue());
} finally {
notThreadSafe.remove();
}
}).start();
}
}
ThreadLocal与Thread如何绑定
上文我说过 ThreadLocal 会与它所属的 Thread 绑定,这个绑定是什么意思呢,下面我们来看看 Thread
的一处源码:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
注释
的译文:与此线程相关的ThreadLocal值。这个映射由ThreadLocal类维护。
ThreadLocal.ThreadLocalMap
类型的 threadLocals
属性是保存与当前线程相关的ThreadLocal 实例,该map
由ThreadLocal
来维护。下面我来看看 ThreadLocalMap
到底是个什么。
先来看下官方解释:
ThreadLocalMap 是一个定制的散列映射,只适用于维护线程本地值。没有任何操作被导出到 ThreadLocal 类之外。类是包私有的,允许在类线程中声明字段。为了帮助处理非常大且长期存在的用法,哈希表条目对键使用 WeakReference 。但是,由于没有使用引用队列,所以只有在表空间不足时才会删除陈旧的条目。
ThreadLocalMap
其实就是一个散列表和HashMap
差不多,只不过是定制的,只用于维护线程本地的值。为了帮助处理非常大且长期存在的用法,哈希表条目对键使用 WeakReference
,现在大家比较关心这个散列表的键
对应着的是什么吧?我们来看看ThreadLocalMap
中的Entry
是如何定义的:
static class Entry extends WeakReference<ThreadLocal<?>> {
/**与ThreadLocal关联的值。*/
Object value;
//key 就是ThreadLocal 对象本身,而值就是大家想要保存的数据如数据库连接
Entry(ThreadLocal<?> k, Object v) {
//将k置为弱引用
super(k);
value = v;
}
}
看了源码可知:ThreadLocalMap
是以ThreadLocal
实例为健,用户要线程私有化的数据为值的散列表,并且健
还是弱引用类型的。
下面我们来讲下 ThreadLocal 如何与线程关联起来的。ThreadLocal 实例在调用 set
和 get
的时候,会先获取当前线程的threadLocals
属性,判断 threadLocals
属性是否为空,若不为空则进行获取或者添加操作,否则会创建一个 ThreadLocalMap
实例赋给当前线程的属性 threadLocals
;然后往里 put 一个键值对,当get
或 set
方法时健
都是当前ThreadLocal实例,只不过是get
时,值为ThreadLocal 中initValue
方法返回的值,默认为 null ;方法为set
时,则为调用者传进的实参。
ThreadLocal 的 get
方法:
public T get() {
Thread t = Thread.currentThread();
//获取当前线程的 threadLocals 属性
ThreadLocalMap map = getMap(t);
if (map != null) {
//若threadLocals属性不为空,以 this(当前 ThreadLocal)实例为健获取对应的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
//若已经设置过值或者有初始值就直接返回
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//当前线程的threadLocals属性为空或者没有设置过值时设置初始值
return setInitialValue();
}
/**
* 获取与给定线程相关联的ThreadLoal散列表
* @param t 当前线程
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
private T setInitialValue() {
//调用 initialValue 获取初始值默认为 null
T value = initialValue();
Thread t = Thread.currentThread();
//获取当前线程的 threadLocals 属性
ThreadLocalMap map = getMap(t);
if (map != null)
//如果已经创建与当前线程关联的 ThreadLoal 散列表,则直接设值
map.set(this, value);
else
//创建与当前线程相关的 ThreadLocal 散列表 并设值
createMap(t, value);
return value;
}
/**
* 创建与当前线程关联的 ThreadLocal 散列表,
* 并将它赋值给给定线程的 threadLocals 属性
* @param t 当前线程
* @param ThreadLocal散列表第一个Entry的初始值
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocal 中的set
方法
/**
* 向当前线程的线程私有变量设置指定的值
*/
public void set(T value) {
Thread t = Thread.currentThread();
//获取当前线程的 threadLocals 属性
ThreadLocalMap map = getMap(t);
if (map != null)
//如果已经创建与当前线程关联的 ThreadLoal 散列表,则直接设值
map.set(this, value);
else
//创建与当前线程相关的 ThreadLocal 散列表, 并设值
createMap(t, value);
}
下面我们用一张图来概括下线程,线程私有变量以及用户定义的数据之间的关系,加深我们的理解:
上图中 Entry 中的 key 是弱引用类型的,因此用户程序使用完ThreadLocal 对象之后忘记调用
remove
方法,下一次 GC 会把只有一个弱引用的ThreadLocal 回收掉,此时 key
指向 null
,则无论谁都不能访问到该key 对应的 value
对象,只要线程实例不退出就无法释放,如果value
对象占用内存很大,则可能会造成OOM。但是ThreadLocalMap 底层会对 key
为 null的value
进行清理。我们下一章讨论
后记
我们讨论了 ThreadLocal 如何使用以及其与 Thread 之间关系,下一节我们讨论下 ThreadLocalMap
的具体实现。