在学习ThreadLocal之前需要先去网上了解一下,然后带着问题来学习,首先我这里整理一些常见的ThreadLocal问题:
- 什么是ThreadLocal?
- 为什么要有 ThreadLocal?应用场景
- ThreadLocal的哈希冲突是如何解决的?
- ThreadLocal的内存泄露与线程不安全?
- ThreadKey为什么要用弱引用?
一、概念
作用
ThreadLocal的作用: 用于跨方法的参数传递,特别是在Spring的事务处理和链路跟踪中大量使用。 ThreadLocal的实现原理: 每个线程管理自己的副本变量,这个副本变量本质上是一个map。
接口类
ThreadLocal 类接口很简单,只有 4 个方法,我们先来了解一下: • void set(Object value) 设置当前线程的线程局部变量的值。
• public Object get() 该方法返回当前线程所对应的线程局部变量。
• public void remove() 将当前线程局部变量的值删除, 目的是为了减少内存的占用, 该方法是 JDK 5.0 新增的方法。需 要指出的是,当线程结束后,对应该线程的局部变量将自动 被垃圾回收, 所以显式调用该方法清除 线程的局部变量并不是必须的操作, 但它 可以加快内存回收的速度。
• protected Object initialValue() 返回该线程局部变量的初始值,该方法是一个 protected 的方法, 显然是为 了让子类覆盖而设计 的。这个方法是一个延迟调用方法, 在线程第 1 次调用 get() 或 set(Object)时才执行,并且仅执行 1 次。ThreadLocal 中的缺省实现直接返回一 个 null。
为什么要有 ThreadLocal
ThreadLocal和Synchonized的异同性
ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。
但是ThreadLocal与synchronized有本质的区别:
1、Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
2、Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
一句话理解ThreadLocal,threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。
二、实现原理
实现分析
怎么实现 ThreadLocal,既然说让每个线程都拥有自己变量的副本,最容易 的方式就是用一个 Map 将线程的副本存放起来, Map 里 key 就是每个线程的唯 一性标识,比如线程 ID ,value 就是副 本值, 实现起来也很简单:
/**
* 类说明:自己实现的ThreadLocal
*/
public class MyThreadLocal<T> {
/*存放变量副本的map容器,以Thread为键,变量副本为value*/
private Map<Thread,T> threadTMap = new HashMap<>();
public synchronized T get(){
return threadTMap.get(Thread.currentThread());
}
public synchronized void set(T t){
threadTMap.put(Thread.currentThread(),t);
}
}
可以看到 ThreadLocal 的性能远超类似 synchronize 的锁实现 ReentrantLock, 比我们后面要学的 AtomicInteger 也要快很多,即使我们把 Map 的实现更换为Java 中专为并发设计的 ConcurrentHashMap 也不太可能达到这么高的性能。
怎么样设计可以让 ThreadLocal 达到这么高的性能呢?
最好的办法则是让变 量副本跟随着线程本 身, 而不是将变量副本放在一个地方保存, 这样就可以在存 取时避开线程之间的竞争。
同时,因为每个线程所拥有的变量的副本数是不定的, 有些线程可能有一个, 有些线程可能有 2 个甚至更多, 则线程内部存放变量副本需要一个容器, 而且容 器要支持快速存取, 所以在每个线 程内部都可以持有一个 Map 来支持多个变量 副本,这个 Map 被称为 ThreadLocalMap。
具体实现
上面先取到当前线程,然后调用 getMap 方法获取对应的 ThreadLocalMap。ThreadLocalMap 是一 个声明在 ThreadLocal 的静态内部类, 然后 Thread 类中有一 个这样类型成员变量,也就是 ThreadLocalMap 实例化是在 Thread 内部,所以 getMap 是直接返回 Thread 的这个成员。 看下 ThreadLocal 的内部类 ThreadLocalMap 源码,这里其实是个标准的 Map 实现,内部有一个元素类型为 Entry 的数组, 用以存放线程可能需要的多个副本 变量。
可以看到有个 Entry 内部静态类,它继承了 WeakReference,总之它记录了 两个信息, 一个是 ThreadLocal类型, 一个是 Object 类型的值。 getEntry 方法 则是获取某个 ThreadLocal 对应的值, set 方法就是更新或赋值相应的 ThreadLocal 对应的值。
回顾我们的 get 方法, 其实就是拿到每个线程独有的 ThreadLocalMap 然后再用 ThreadLocal 的当前实例,拿到 Map 中的相应的 Entry,然后就可 以拿到相应的值返回 出去。当然, 如果 Map 为空, 还会先进行 map 的创建, 初 始化等工作。
三、使用场景
存储用户Session
数据库连接,处理数据库事务
数据跨层传递(controller,service, dao)
Spring使用ThreadLocal解决线程安全问题
四 引发的问题
1、Hash 冲突的解决
开放定址法
基本思想是,出现冲突后按照一定算法查找一个空位置存放,根据算法的不 同又可以分为线性探 测再散列、二次探测再散列、伪随机探测再散列。 线性探测再散列即依次向后查找,二次探测再散列, 即依次向前后查找, 增量为 1 、2 、3 的 二次方,伪随机,顾名思义就是随机产生一个增量位移。 ThreadLocal 里用的则是线性探测再散列
链地址法
这种方法的基本思想是将所有哈希地址为 i 的元素构成一个称为同义词链的 单链表, 并将单链表的头指针存在哈希表的第 i 个单元中, 因而查找、插入和删 除主要在同义词链 中进行。链地址法适用于经常进行插入和删除的情况。 Java 里的 HashMap 用的就是链地址法,为了避免 hash 洪水攻击,1.8 版本开始还引 入了红黑树。
再哈希法
这种方法是同时构造多个不同的哈希函数: Hi=RH1(key) i=1 ,2 ,… ,k 当哈希地址 Hi=RH1 (key)发生冲突时,再计算 Hi=RH2(key)……,直到冲突 不再产生。这种方法不易产生聚集,但增 开放定址法: 链地址法: 再哈希法: 加了计算时间。
建立公共溢出区
这种方法的基本思想是: 将哈希表分为基本表和溢出表两部分, 凡是和基本 表发生冲突的元 素, 一律填入溢出表。
2、内存泄漏的现象
根据我们前面对 ThreadLocal 的分析,我们可以知道每个 Thread 维护一个 ThreadLocalMap,这个 映射表的 key 是 ThreadLocal 实例本身, value 是真正需 要存储的 Object,也就是说 ThreadLocal 本 身并不存储值, 它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。仔细观察 ThreadLocalMap,这个 map 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
3、错误使用 ThreadLocal 导致线程不安全
总结
-
JVM 利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。
-
JVM 利用调用 remove 、get 、set 方法的时候,回收弱引用。
-
当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,而不再去调用 remove、 get 、set 方法,那 么将导致内存泄漏。
-
使用线程池+ ThreadLocal 时要小心, 因为这种情况下, 线程是一直在不断的 重复运行的,从而 也就造成了 value 可能造成累积的情况。
blog.csdn.net/qq_41787812… blog.csdn.net/u010445301/…
TULING