ThreadLocal 你真的会用吗?
前言
前几天在京东的同学给我打了个电话,聊了下家常,技术宅的我多嘴问了最近有没有学啥? 他说最近有点忙,但抽空也看了几篇博客,他说我考考你吧,我说可以啊,他问我: 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 的具体实现。
请先 后发表评论~