一、ThreadLocal介绍
一、官方介绍
从Java官方文档中的描述:ThreadLocal类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get和set方法访问)时,能够保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static 类型的,用来关联线程个线程上下文。
二、ThreadLocal线程的作用
提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量的复杂度。
总结:
1、线程并发:ThreadLocal一般是在多线程并发的场景下使用,不是多线程的环境下是没必要使用的。
2、传递数据:在同一个线程,不同的组件进行公共变量的传递可以使用ThreadLocal
3、线程隔离:每一个线程中的变量都是独立的,线程之间的变量互不影响。
三、常用方法
四、 ThreadLocal与Synchronized之间的区别
虽然两者都是用于处理多线程并发访问变量的问题,不过两者之间处理问题的角度和思路不同。
二、ThreadLocal的内部结构
一、ThreadLocal常见误解
ThreadLocal早期设计:每一个ThreadLocal都会创建一个map,然后用线程作为map的key,要存储的局部变量作为map的value,这样就能达到各个线程的局部变量隔离的效果。
二、 ThreadLocal当前设计
JDK后面优化了设计方案,在jdk8中ThreadLocal设计是:每个Thread维护一个ThreadLocalMap,这个map的key是ThreadLocal实例本身,value才是真正要存储的值Object。
1、每个Thread线程内部有一个Map(ThreadLocalMap)
2、每个Map中存储ThreadLocal变量(key)和线程的变量副本(value)
3、Thread内部内部的map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
4、对于不同线程,每次获取副本值时,别的线程并不能获取当前线程的副本值,形成了副本的隔离,互不干扰。
三、两者对比
三、ThreadLocal源码分析
ThreadLocal对外暴露的方法有以下几个:
一、Set方法
1、首先获取当前线程,并根据当前线程获取一个Map集合
2、如果获取的集合不为空,则将参数设置到map中,其中ThreadLocal作为key
3、如果为空,则给改线程创建一个新的map,并设置初始值。
二、get方法
/** 返回该线程本地变量当前线程的副本中的值。 如果变量没有当前线程的值,则首先将其初始化为调用 {@ link initialValue} 方法返回的值。 返回该线程的当前线程值-local */ public T get() { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程维护的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { // 以当前ThreadLocal为key,获取对应的存储实体。this表示当前对象ThreadLocal ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { // 这里主要是想获取当前线程,对应的ThreadLocal的值 @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } /* 初始化:有两种情况下会执行 1、map不存在时会执行,表示当前线程没有维护ThreadLocalMap 2、map存在,但是没有与当前ThreadLocal关联的值 */ return setInitialValue(); }
三、remove方法
四、initialValue方法
五、ThreadLocalMap源码分析
一、基本结构
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现的。
二、ThreadLocalMap中的成员变量
三、存储结构--Entry
四、内存泄漏与弱引用
(1)内存泄漏相关概念:
1、Memery overflow:内存溢出,没有足够的内存给申请者使用。
2、Memery leak:内存泄漏,程序中已动态分配的堆内存,由于某种原因程序未能释放或无法释放,造成系统内存的浪费,导致程序运行减慢或者系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。
(2)弱引用相关概念
java中的引用有四种类型:强、软、弱、虚。
1、强引用(Strong Reference):就是我们最常见的普通对象的应用,只要还有强应用指向一个对象,就表明对象还活着,垃圾收集器就不会回收这个对象的内存。
2、软引用(Soft Reference ):
如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;
如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
3、弱引用(Weak Reference ):垃圾收集器一旦发现有弱引用的对象,不管当前内存是否足够,都会立即回收他的内存。
4、虚引用(Phantom Reference):
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
之后会另起一篇记录
(3)如果key是强引用
假设ThreadLocalMap中的key使用了强引用, 那么会出现内存泄漏吗?
1、假设在业务代码中使用完ThreadLocal, ThreadLocal ref被回收了
2、但是因为threadLocalMap的Entry强引用了threadLocal, 造成ThreadLocal无法被回收
3、在没有手动删除Entry以及CurrentThread依然运行的前提下, 始终有强引用链threadRef → currentThread → entry, Entry就不会被回收( Entry中包括了ThreadLocal实例和value), 导致Entry内存泄漏也就是说: ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的
(4)如果key是弱引用
1、假设在业务代码中使用完ThreadLocal, ThreadLocal ref被回收了
2、由于threadLocalMap只持有ThreadLocal的弱引用, 没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收, 此时Entry中的key = null
3、在没有手动删除Entry以及CurrentThread依然运行的前提下, 也存在有强引用链threadRef → currentThread → value, value就不会被回收, 而这块value永远不会被访问到了, 导致value内存泄漏
也就是说: ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。
(5)内存泄漏的真实原因
出现内存泄漏的真实原因出改以上两种情况,
比较以上两种情况,我们就会发现:
内存泄漏的发生跟 ThreadLocalIMap 中的 key 是否使用弱引用是没有关系的。那么内存泄漏的的真正原因是什么呢?在以上两种内存泄漏的情况中.都有两个前提:
1 、没有手动删除这个 Entry
2 ·、CurrentThread 依然运行
第一点很好理解,只要在使用完下 ThreadLocal ,调用其 remove 方法翻除对应的 Entry ,就能避免内存泄漏。
第二点,由于ThreodLocalMap 是 Thread 的一个属性,被当前线程所引所以它的生命周期跟 Thread 一样长。那么在使用完 ThreadLocal 的使用,如果当前Thread 也随之执行结束, ThreadLocalMap 自然也会被 gc 回收,从根源上避免了内存泄漏。综上, ThreadLocal 内存泄漏的根源是:
由于ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏.
(6)为什么使用弱引用
无论 ThreadLocalMap 中的 key 使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。
要避免内存泄漏有两种方式:
1 .使用完 ThreadLocal ,调用其 remove 方法删除对应的 Entry
2 .使用完 ThreadLocal ,当前 Thread 也随之运行结束
相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的.
也就是说,只要记得在使用完ThreadLocal 及时的调用 remove ,无论 key 是强引用还是弱引用都不会有问题.
那么为什么 key 要用弱引用呢
事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null (也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么是会又如 value 置为 null 的.
这就意味着使用完 ThreadLocal , CurrentThread 依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收.对应value在下一次 ThreadLocaIMap 调用 set/get/remove 中的任一方法的时候会被清除,从而避免内存泄漏.
五、Hash冲突解决
Hash冲突的解决是Map中的一个重要内容。
1、构造方法ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
/** * firstKey:ThreadLocal实例 * firstValue:要保存的线程本地变量 */ ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { // 为Entry数组设置初始化值16 table = new Entry[INITIAL_CAPACITY]; // 计算索引 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 设置值 table[i] = new Entry(firstKey, firstValue); // 数组存储的个数 size = 1; // 设置阈值 setThreshold(INITIAL_CAPACITY); }构造函数首先创建一个长度为16的数组,然后计算出firstKey对应的索引,然后存储到table
中,并设置size和threshould
主要分析: int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
1、firstKey.threadLocalHashCode
private final int threadLocalHashCode = nextHashCode();private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }private static AtomicInteger nextHashCode = new AtomicInteger()private static final int HASH_INCREMENT = 0x61c88647;
&(INITIAL_CAPACITY - 1)
2、ThreadLocalMap中的Set方法
private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; // 计算索引,相当于对数组长度取余 int i = key.threadLocalHashCode & (len-1); /** 使用线性探测法查找元素 Entry e = tab[i]:取出i这个位置的Entry e != null:对取出的Entry 进行判空,不为空时继续循环 e = tab[i = nextIndex(i, len)]:长度加一 */ for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // ThreadLocal对应的key存在,则直接覆盖之前的值 if (k == key) { e.value = value; return; } // key为null,但是值不为null,说明之前的ThreadLocal对象已经被回收了,当前 // Entry是一个陈旧元素 if (k == null) { // 用新元素替换旧元素,并且进行垃圾清理,防止内存泄漏 replaceStaleEntry(key, value, i); return; } } /** ThreadLocal对应的key不存在,且没有找到陈旧元素,则在空元素的位置创建一个新的 Entry */ tab[i] = new Entry(key, value); int sz = ++size; /** cleanSomeSlots用于清除e.get()==null的元素 这种数据key关联的对象已经被回收了,如果没有 清理任何的Entry,并且当前数量达到了负载因子所定义(长度的三分之二) 那么进行rehash();执行一次全表的扫描清理工作 **/ if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }// 获取环形数组的下一个索引
private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }