- ThreadLocal 的作用及应用场景
- 使用场景
- 原理分析
- ThreadLocalMap 的底层结构
- 内存泄露产生的原因
- 解决 Hash 冲突
- 使用 ThreadLocal 时对象存在哪里?
ThreadLocal 的作用以及应用场景
ThreadLocal
算是一种并发容器吧,因为他的内部是有 ThreadLocalMap
组成,ThreadLocal
是为了解决多线程情况下变量不能被共享的问题,也就是多线程共享变量的问题。
ThreadLocal
和 Lock
以及 Synchronized
的区别是:ThreadLocal
是给每个线程分配一个变量(对象),各个线程都存有变量的副本,这样每个线程都是使用自己(变量)对象实例,使线程与线程之间进行隔离;而 Lock
和 Synchronized
的方式是使线程有顺序的执行。
举一个简单的例子:目前有 100 个学生等待签字,但是老师只有一个笔,那老师只能按顺序的分给每个学生,等待 A 学生签字完成然后将笔交给 B 学生,这就类似 Lock
,Synchronized
的方式。而 ThreadLocal
是,老师直接拿出一百个笔给每个学生;再效率提高的同事也要付出一个内存消耗;也就是以空间换时间的概念
使用场景
Spring 的事务隔离就是使用 ThreadLocal
和 AOP 来解决的;主要是 TransactionSynchronizationManager
这个类;
解决 SimpleDateFormat
线程不安全问题;
当我们使用 SimpleDateFormat
的 parse()
方法的时候,parse()
方法会先调用 Calendar.clear()
方法,然后调用 Calendar.add()
方法,如果一个线程先调用了 add()
方法,然后另一个线程调用了 clear()
方法;这时候 parse()
方法就会出现解析错误;如果不信我们可以来个例子:
1 | public class SimpleDateFormatTest { |
这里我们只启动了 50 个线程问题就会出现,其实看巧不巧,有时候只有 10 个线程的情况就会出错:
1 | Exception in thread "Thread-40" java.lang.NumberFormatException: For input string: "" |
其实解决这个问题很简单,让每个线程 new 一个自己的 SimpleDateFormat
,但是如果 100 个线程都要 new100 个 SimpleDateFormat
吗?
当然我们不能这么做,我们可以借助线程池加上 ThreadLocal
来解决这个问题:
1 | public class SimpleDateFormatTest { |
这样就优雅的解决了线程安全问题;
解决过度传参问题;例如一个方法中要调用好多个方法,每个方法都需要传递参数;例如下面示例:
1 | void work(User user) { |
用了 ThreadLocal
之后:
1 | public class ThreadLocalStu { |
每个线程内需要保存全局变量(比如在登录成功后将用户信息存到 ThreadLocal
里,然后当前线程操作的业务逻辑直接 get 取就完事了,有效的避免的参数来回传递的麻烦之处),一定层级上减少代码耦合度。
- 比如存储 交易 id 等信息。每个线程私有。
- 比如 aop 里记录日志需要 before 记录请求 id,end 拿出请求 id,这也可以。
- 比如 jdbc 连接池(很典型的一个
ThreadLocal
用法) - …. 等等….
原理分析
上面我们基本上知道了 ThreadLocal
的使用方式以及应用场景,当然应用场景不止这些这只是工作中常用到的场景;下面我们对它的原理进行分析;
我们先看一下它的 set()
方法;
1 | public void set(T value) { |
是不是特别简单,首先获取当前线程,用当前线程作为 key, 去获取 ThreadLocalMap
, 然后判断 map 是否为空,不为空就将当前线程作为 key, 传入的 value 作为 map 的 value 值;如果为空就创建一个 ThreadLocalMap
, 然后将 key 和 value 方进去;从这里可以看出 value 值是存放到 ThreadLocalMap
中;
然后我们看看 ThreadLocalMap
是怎么来的?先看下 getMap()
方法:
1 | //在Thread类中维护了threadLocals变量,注意是Thread类 |
这就能解释每个线程中都有一个 ThreadLocalMap
,因为 ThreadLocalMap
的引用在 Thread 中维护;这就确保了线程间的隔离;
我们继续回到 set()
方法,看到当 map 等于空的时候 createMap(t, value);
1 | void createMap(Thread t, T firstValue) { |
这里就是 new 了一个 ThreadLocalMap
然后赋值给 threadLocals
成员变量;ThreadLocalMap
构造方法:
1 | ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { |
这里写有个大概的印象,后面对 ThreadLocalMap
内部结构还会进行详细的讲解;
下面我们再去看一下 get()
方法:
1 | public T get() { |
get()
方法和 set()
都比较容易理解,如果 map 等于空的时候或者 entry 等于空的时候我们看看 setInitialValue()
方法做了什么事:
1 | private T setInitialValue() { |
下面我们再去看一下 ThreadLocal
中的 initialValue()
方法:
1 | protected T initialValue() { |
设置初始值,由子类去实现;就例如我们上面的例子,重写 ThreadLocal
类中的 initialValue()
方法:
1 | private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>() { |
createMap()
方法和上面 set()
方法中 createMap()
方法同一个,就不过多的叙述了;剩下还有一个 removve()
方法
1 | public void remove() { |
源码的讲解就到这里,也都比较好理解,下面我们看看 ThreadLocalMap
的底层结构
ThreadLocalMap 的底层结构
上面我们已经了解了 ThreadLocal
的使用场景以及它比较重要的几个方法;下面我们再去它的内部结构;经过上的源码分析我们可以看到数据其实都是存放到了 ThreadLocal
中的内部类 ThreadLocalMap
中;而 ThreadLocalMap
中又维护了一个 Entry 对象,也就说数据最终是存放到 Entry 对象中的;
1 | static class ThreadLocalMap { |
Entry 的构造方法是以当前线程为 key, 变量值 Object 为 value 进行存储的;在上面的源码中 ThreadLocalMap
的构造方法中也涉及到了 Entry;看到 Entry 是一个数组;初始化长度为 INITIAL_CAPACITY = 16;
因为 Entry 继承了 WeakReference
,在 Entry 的构造方法中,调用了 super(k)
方法就会将 threadLocal
实例包装成一个 WeakReferenece
。这也是 ThreadLocal
会产生内存泄露的原因;
内存泄露产生的原因
如图所示存在一条引用链:Thread Ref->Thread->ThreadLocalMap->Entry->Key:Value
,经过上面的讲解我们知道 ThreadLocal
作为 Key, 但是被设置成了弱引用,弱引用在 JVM 垃圾回收时是优先回收的,就是说无论内存是否足够弱引用对象都会被回收;弱引用的生命周期比较短;当发生一次 GC 的时候就会变成如下:
TreadLocalMap
中出现了 Key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value, 如果线程迟迟不结束(也就是说这条引用链无意义的一直存在)就会造成 value 永远无法回收造成内存泄露;如果当前线程运行结束 Thread,ThreadLocalMap
,Entry 之间没有了引用链,在垃圾回收的时候就会被回收;但是在开发中我们都是使用线程池的方式,线程池的复用不会主动结束;所以还是会存在内存泄露问题;
解决方法也很简单,就是在使用完之后主动调用 remove()
方法释放掉;
解决 Hash 冲突
记得在大学学习数据结构的时候学习了很多种解决 hash 冲突的方法;例如:
线性探测法(开放地址法的一种): 计算出的散列地址如果已被占用,则按顺序找下一个空位。如果找到末尾还没有找到空位置就从头重新开始找;
二次探测法(开放地址法的一种)
链地址法:链地址是对每一个同义词都建一个单链表来解决冲突,HashMap 采用的是这种方法;
多重 Hash 法: 在 key 冲突的情况下多重 hash, 直到不冲突为止,这种方式不易产生堆积但是计算量太大;
公共溢出区法: 这种方式需要两个表,一个存基础数据,另一个存放冲突数据称为溢出表;
上面的图片都是在网上找到的一些资料,和大学时学习时的差不多我就直接拿来用了;也当自己复习了一遍;
介绍了那么多解决 Hash 冲突的方法,那 ThreadLocalMap
使用的哪一种方法呢?我们可以看一下源码:
1 | private void set(ThreadLocal<?> key, Object value) { |
从这里我们可以看出使用的是线性探测的方式来解决 hash 冲突!
源码中通过 nextIndex(i, len)
方法解决 hash 冲突的问题,该方法为 ((i + 1 < len) ? i + 1 : 0);
,也就是不断往后线性探测,直到找到一个空的位置,当到哈希表末尾的时候还没有找到空位置再从 0 开始找,成环形!
使用 ThreadLocal 时对象存在哪里?
在 java 中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有变量,而堆内存中的变量对所有线程可见,可以被所有线程访问!
那么 ThreadLocal
的实例以及它的值是不是存放在栈上呢?其实不是的,因为 ThreadLocal
的实例实际上也是被其创建的类持有,(更顶端应该是被线程持有),而 ThreadLocal
的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。