ThreadLocal全面解析
目录
- 一、ThreadLocal的介绍
- 1、简介
- 2、基本使用
- 3、ThreadLocal与synchronized的区别
- 二、ThreadLocal的内部结构
- 1、jdk早期设计
- 2、JDK8设计
- 3、内存泄露
- 三、ThreadLocal的核心方法源码
- 1、set方法
- 2、get方法
- 3、initialValue方法
- 4、withInitial方法
- 5、remove方法
- 6、子类InheritableThreadLocal类
- 四、ThreadLocalMap源码分析
- 1、Hash算法
- 2、Hash冲突
- 总结
一、ThreadLocal的介绍
1、简介
- ThreadLocal类用来提供线程内部的局部变量
- 这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量
- ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文
使用场景及作用
- 线程并发: 在多线程并发的场景下
- 传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
- 线程隔离: 每个线程的变量都是独立的,不会互相影响
2、基本使用
ThreadLocal的常用方法
方法声明 描述 public class ThreadLocal 创建ThreadLocal对象 public void set(T value) 设置当前线程绑定的局部变量 public T get() 获取当前线程绑定的局部变量 public void remove() 移除当前线程绑定的局部变量 多线程下普通变量与ThreadLocal的代码对比
- 开启多个线程,每个线程获取自己线程set的值
public class MyDemo { // ThreadLocal本地线程变量 private ThreadLocal tl = new ThreadLocal(); // 普通对象变量 private String content; public static void main(String[] args) { MyDemo demo = new MyDemo(); for (int i = 0; i { String data = Thread.currentThread().getName() + "的数据"; // 普通变量方式: demo.content = data; System.out.print(""); System.out.println("普通变量获取到数据: " + Thread.currentThread().getName() + "--->" + demo.content); // ThreadLocal方式: // demo.tl.set(data); // System.out.print(""); // System.out.println("ThreadLocal获取到数据: " + Thread.currentThread().getName() + "--->" + demo.tl.get()); }); thread.setName("线程" + i); thread.start(); } } }
普通变量方式:
- 多个线程在访问同一个变量的时候出现的异常,线程间的数据没有隔离
ThreadLocal方式:
- 解决了多线程之间数据隔离的问题
3、ThreadLocal与synchronized的区别
synchronized ThreadLocal 原理 同步机制采用以时间换空间的方式, 只提供了一份变量,让不同的线程排队访问 ThreadLocal采用以空间换时间的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰 侧重点 多个线程之间访问资源的同步 多线程中让每个线程之间的数据相互隔离 二、ThreadLocal的内部结构
1、jdk早期设计
- 每个ThreadLocal都创建一个Map
- 然后用线程作为Map的key
- 要存储的局部变量作为Map的value
- 这样就能达到各个线程的局部变量隔离的效果
JDK最早期的ThreadLocal确实是这样设计的,但现在早已不是了
2、JDK8设计
- Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap
- ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)
- 每个线程在往ThreadLocal里放值的时候,都会往自己线程的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离
- ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构
- ThreadLocalMap中的Entry,它的key是ThreadLocal k ,继承自WeakReference, 也就是我们常说的弱引用类型
ThreadLocal设计改良的好处
- 这样设计之后每个Map存储的Entry数量就会变少
- 之前的存储数量由Thread的数量决定(每个Thread作为key)
- 现在是由ThreadLocal的数量决定(ThreadLocal作为线程内map的key)
- 在实际运用当中,往往ThreadLocal的数量要少于Thread的数量
- 当Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用
3、内存泄露
- 不再会被使用的对象或者变量引用的内存不能被回收,就是内存泄露
为什么要用弱引用?
public void function01(){ ThreadLocal tl = new ThreadLocal(); tl.set(2021); tl.get(); }
- 新建了一个ThreadLocal对象,t1是强引用指向ThreadLocal对象
- 调用set()方法后新建一个Entry,通过源码可知Entry对象里的k是弱引用指向ThreadLocal对象
- 当function01方法执行完毕后,栈帧销毁强引用tl也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象
- 若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏
- 若这个key引用是弱引用,threadlocal就可以顺利被gc回收,此时Entry中的key=null
- 在没有手动删除这个Entry以及当前线程依然运行的前提下,也存在有强引用链
- value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏
- 在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的
- 只要记得在使用完ThreadLocal及时的调用remove,其实就不用走以上步骤了
三、ThreadLocal的核心方法源码
1、set方法
- 首先获取当前线程,并根据当前线程获取一个Map
- 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
- 如果Map为空,则给该线程创建 Map,并设置初始值
// 设置当前线程对应的ThreadLocal的值 public void set(T value) { // 获取当前线程对象 Thread t = Thread.currentThread(); // 获取此线程对象中维护的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); // 判断map是否存在 if (map != null){ // 存在则调用map.set设置此实体entry map.set(this, value); } else { // 1)当前线程Thread 不存在ThreadLocalMap对象 // 2)则调用createMap进行ThreadLocalMap对象的初始化 // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中 createMap(t, value); } } /** * 获取当前线程Thread对应维护的threadLocals * ThreadLocal.ThreadLocalMap threadLocals = null; */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; } // 创建当前线程Thread对应维护的ThreadLocalMap void createMap(Thread t, T firstValue) { //这里的this是调用此方法的threadLocal t.threadLocals = new ThreadLocalMap(this, firstValue); }
2、get方法
- 首先获取当前线程, 根据当前线程获取一个Map
- 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry的value
- Map为空则通过initialValue函数(创建ThreadLocal时重写的方法,不重写则返回空)获取初始值value,然后用ThreadLocal的引用和value创建一个新的Map
// 返回当前线程中保存ThreadLocal的值 public T get() { // 获取当前线程对象 Thread t = Thread.currentThread(); // 获取此线程对象中维护的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); // 如果此map存在 if (map != null) { // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e ThreadLocalMap.Entry e = map.getEntry(this); // 对e进行判空 if (e != null) { // 获取存储实体 e 对应的 value值 // 即为我们想要的当前线程对应此ThreadLocal的值 T result = (T)e.value; return result; } } /* 初始化 : 有两种情况有执行当前代码 第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象 第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry */ return setInitialValue(); } // 初始化 private T setInitialValue() { // 调用initialValue获取初始化的值 // 此方法可以被子类重写, 如果不重写默认返回null T value = initialValue(); // 获取当前线程对象 Thread t = Thread.currentThread(); // 获取此线程对象中维护的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); // 判断map是否存在 if (map != null){ // 存在则调用map.set设置此实体entry map.set(this, value); } else{ // 1)当前线程Thread 不存在ThreadLocalMap对象 // 2)则调用createMap进行ThreadLocalMap对象的初始化 // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中 createMap(t, value); } // 返回设置的值value return value; }
3、initialValue方法
- 在set方法还未调用而先调用了get方法时才执行,并且仅执行1次
- 如果想要一个除null之外的初始值,可以重写此方法,protected方法,只能子类去覆盖
/** * 返回当前线程对应的ThreadLocal的初始值 * 此方法的第一次调用发生在,当线程通过get方法访问此线程的ThreadLocal值时 * 除非线程先调用了set方法,在这种情况下,initialValue 才不会被这个线程调用。 * 通常情况下,每个线程最多调用一次这个方法。 * *
这个方法仅仅简单的返回null {@code null}; * 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值, * 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法 * 通常, 可以通过匿名内部类的方式实现 * * @return 当前ThreadLocal的初始值 */ protected T initialValue() { return null; }
示例:
private static ThreadLocal tl = new ThreadLocal() { // 重写initialValue方法,设置默认值为false @Override protected Boolean initialValue() { return Boolean.FALSE; } };
4、withInitial方法
- JDK8新增,支持Lambda表达式,和ThreadLocal重写的initialValue()效果一样
public static ThreadLocal withInitial(Supplier firstKey, Object firstValue) { // 默认创建容量为16的Entry数组 table = new Entry[INITIAL_CAPACITY]; // 任何数 & (table.length-1)的结果也一定在[0, table.length-1]范围,而且&的运算效率高 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 创建Entry对象,key为ThreadLocal对象(弱引用),value为设置的值 table[i] = new Entry(firstKey, firstValue); size = 1; // 设置数组容量阈值 setThreshold(INITIAL_CAPACITY); } // 默认容量为16,threshold阈值为10,数组table内存在的对象数量size超过10,就会扩容 private void setThreshold(int len) { threshold = len * 2 / 3; } } }
- 每当创建一个ThreadLocal对象,这个ThreadLocal.nextHashCode这个值就会增长 0x61c88647,这个值很特殊,它是斐波那契数也叫黄金分割数
- hash增量为这个数字,带来的好处就是hash分布非常均匀
测试:数据会平均散落到所有位置上
2、Hash冲突
- 虽然ThreadLocalMap中使用了黄金分割数来作为hash计算因子,大大减少了Hash冲突的概率,但是仍然会存在冲突
- 如果冲突了,就会通过nextIndex方法再次计算哈希值,线性探测法(不断加1)
private void set(ThreadLocal key, Object value) { ThreadLocal.ThreadLocalMap.Entry[] tab = table; int len = tab.length; //计算索引(重点代码,刚才分析过了) int i = key.threadLocalHashCode & (len-1); /** * 使用线性探测法查找元素(重点代码) */ for (ThreadLocal.ThreadLocalMap.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 是一个陈旧(stale)的元素 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(table[index])可以被置null。 * 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行 * rehash(执行一次全表的扫描清理工作) */ if (!cleanSomeSlots(i, sz) && sz >= threshold) // 这里table会扩容 rehash(); } /** * 获取数组的下一个索引 */ private static int nextIndex(int i, int len) { return ((i + 1
- 首先还是根据key计算出索引 i,然后查找i位置上的Entry
- 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值
- 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry
- 不断循环检测,直到遇到Entry为null的地方,如果没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1
总结
- ThreadLocal并不解决线程间共享数据的问题
- ThreadLocal适用于变量在线程间隔离且在方法间共享的场景
- ThreadLocal通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
- 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
- ThreadLocalMap的Entry对ThreadLocal的引用头弱引用,避免了ThreadLocal对象无法被回收的问题者会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键头为null的Entry
- JDK8新增,支持Lambda表达式,和ThreadLocal重写的initialValue()效果一样
- 不再会被使用的对象或者变量引用的内存不能被回收,就是内存泄露
- 这样设计之后每个Map存储的Entry数量就会变少
- 解决了多线程之间数据隔离的问题
- 多个线程在访问同一个变量的时候出现的异常,线程间的数据没有隔离
- 开启多个线程,每个线程获取自己线程set的值
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理!
部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理!
图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!