当前位置: 首页 > news >正文

Java并发编程第8讲——ThreadLocal详解

ThreadLocal无论是在项目开发还是面试中都会经常碰到,它的重要性可见一斑,本篇文章就从ThreadLocal的使用、实现原理、核心方法的源码、内存泄漏问题等展开介绍一下。

一、什么是ThreadLocal

ThreadLocal是java.lang下面的一个类,在JDK 1.2版本加入,作者是Josh Bloch(集合大神)和Doug Lea(并发大神)

它提供了一种线程局部变量的方式,线程局部变量是指每个线程都拥有自己独立的变量副本,互不干扰,通过ThreadLocal,可以方便地在多线程环境下共享数据,同时不需要考虑线程安全性,这也是解决并发问题的途径之一。

例如:在web开发中,可以使用ThreadLocal来保存用户的登录信息,以便每个线程都能够独立地获取和修改自己的登录信息,避免了线程之间的干扰。

二、ThreadLocal的使用

ThreadLocal有四个方法,分别为:

  • protected T initialValue():返回此线程局部变量的初始值。

  • pubulic T get(): 返回当前线程局部变量的当前线程副本的值。如果这是线程第一次调用该方法,则创建并初始化此副本。

  • public void set(T value):将此线程局部变量的当前线程的副本设置为指定的值。

  • public void remove():移除此线程局部变量的当前线程的值。

下面使用ThreadLocal来模拟用户登录信息的场景:

ThreadLocal工具类:

public class CurrentUserHolder {public static ThreadLocal<User> threadLocal=new ThreadLocal<>();
​public static void setUser(User user){threadLocal.set(user);}
​public static User getUser(){if (Objects.nonNull(threadLocal.get())) {return threadLocal.get();}throw new RuntimeException("当前用户信息为空!");}
​public static void clearUser(){threadLocal.remove();}
}

User实体类:

@Data
public class User {private String name;private Integer age;
}

测试:

public class Test {public static void main(String[] args) {//用户登录User user = new User();user.setName("小黑子");user.setAge(18);//将用户信息保存在ThreadLocal中CurrentUserHolder.setUser(user);//在其它方法中,可以通过ThreadLocal获取用户信息User localUser = CurrentUserHolder.getUser();System.out.println(localUser);//输出:User(name=小黑子, age=18)//用户操作完成后,可以remove掉CurrentUserHolder.clearUser();}
}

ps:由于ThreadLocal是基于线程的,所以在不同的线程中,通过ThreadLocal获取的用户信息是独立的,这在多线程环境下非常有用,可以避免线程之间的数据混乱和冲突。

三、ThreadLocal的实现原理

直接上图!下图中基本描述出了ThreadThreadLocalMapThreadLocal三者之间的关系。

解释一下:

  • ThreadLocal中用于保存线程的独有变量的数据结构是一个内部类:ThreadLocalMap,也是k-v结构,key就是当前ThreadLocal对象,value就是我们要保存的值。

  • Thread类中维护了两个ThreadLocalMap成员变量,threadLocals和inheritableThreadLocals,它们的默认值是null,类型为ThreadLocal.ThreadLocalMap,也就是ThreadLocal类的一个静态内部类ThreadLocalMap,感兴趣的可以去看一下源码。

四、核心源码

4.1 ThreadLocalMap内部类

在静态内部类ThreadLocalMap中,维护了一个数据结构类型为Entry的数组,源码如下:

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}

从源码中我们可以看到,Entry继承了一个ThreadLocal类型的弱引用并将其作为key,value为Object类型(也就是我们需要保存的值)

我们再来看一下它的成员变量:

//数组的默认初始化容量
private static final int INITIAL_CAPACITY = 16;
//Entry数组,大小必须为2的幂
private Entry[] table;
//数组内部元素个数
private int size = 0;
//数组扩容阈值,默认为0,创建ThreadLocalMap对象后会被重新设置
private int threshold; 

是不是有点熟悉,这几个变量和HashMap中的变量很类似,功能也类似。

最后看一下它的构造方法:

/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);
}

注释翻译过来大概就是,该构造方法是懒加载的,只有我们创建一个Entry对象并需要放入到Entry数组的时候才会去初始化数组。

4.2 set()方法

接下来我们就介绍一下ThreadLocal常用的一些方法吧,首先看一下set()方法:

public void set(T value) {//获取当前线程Thread t = Thread.currentThread();//获取当前线程的ThreadLocalMap对象ThreadLocalMap map = getMap(t);if (map != null)// 如果map存在,则将当前ThreadLocal对象作为key,value作为value放入map中map.set(this, value);else// 如果map不存在,则创建一个新的ThreadLocalMap对象,并新建一个Entry放入该ThreadLocalMap, 调用set方法的ThreadLocal和传入的value作为该Entry的key和valuecreateMap(t, value);
}

解释:

  • 获取当前线程,拿到当前Thread的ThreadLocalMap对象。

  • 如果map存在,则将当前ThreadLocal对象作为key,value作为value放入map中。

  • 如果map不存在,则创建一个新的ThreadLocalMap对象,并新建一个Entry放入该ThreadLocalMap, 调用set方法的ThreadLocal和传入的value作为该Entry的key和value。

4.3 get()方法

源码如下:

public T get() {//获取当前线程Thread t = Thread.currentThread();//获取当前线程的ThreadLocalMap对象ThreadLocalMap map = getMap(t);if (map != null) {//map存在,通过this(当前ThreadLocal)获取EntryThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")//Entry不为空,返回该Entry的value值T result = (T)e.value;return result;}}//map不存在,调用setInitialValue()方法设置初始值return setInitialValue();
}

解释:

  • 通过当前线程获取ThreadLocalMap:

    • 如果map存在,则通过当前ThreadLocal获取对应的Entry,若Entry不为空,返回该Entry的value值。

    • 如果map不存在,则调用setInitialValue()方法设置初始值。

  • setInitialValue():

    • 根据initalValue()方法获取value值,默认值为null,可以重写该方法。

    • 通过当前线程获取ThreadLocalMap对象。

    • map存在,设置当前值为上述value,不存在则创建新的ThreadLocalMap,并将值设置为value。

4.4 remove()方法

源码如下:

public void remove() {//根据当前线程获取ThreadLocalMap对象ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)//存在,执行remove方法m.remove(this);
}

解释:

  • 根据当前线程获取ThreadLocalMap对象,存在则执行remove()方法。remove(this)方法中,将ThreadLocal作为key来删除对应的Entry。

五、内存泄漏问题

5.1 分析

读到这,相信你对ThreadLocal的基本原理有了更深一步的理解,我们把上图补全,从堆栈视角看一下它们之间的引用关系。

我们可以看到,ThreadLocal对象,有两个引用,一个是栈上的ThreadLocal引用,一个是ThreadLocalMap中Key对它的引用。如果栈上的ThreadLocal引用不再使用了,那么ThreadLocal对象因为还有一条引用链在,所以会导致它无法回收,久而久之就会OOM。

这就是我们所说的ThreadLocal的内存泄漏问题,为了解决这个问题,ThreadLocalMap使用了弱引用,就是上述我们说过的Entry数组:

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}

可以看出,ThreadLocal的引用k通过构造方法传递给了Entry类的父类WeakReference的构造方法,那么可以理解为ThreadLocalMap中的键是ThreadLocal的弱引用。

穿插一下Java中的四大引用:

  • 强引用:Java中默认的引用类型,只要引用还存在,即便OOM也不会被回收。

  • 软引用:内存不足时,将会被干掉。

  • 弱引用:无论内存充足与否,只要执行GC,就会被干掉。

  • 虚引用:最弱的一种引用,存在意义就是为了将关联虚引用的对象在被GC掉之后收到一个通知。

如果用了弱引用,那么ThreadLocal对象就可以在下次GC的时候被回收掉了。

这样做可以很大程度上避免了因为ThreadLocal的使用而导致的OOM问题,但也无法彻底避免

我们可以看到,虽然key是弱引用,但是value是强引用,而且它的生命周期是和Thread一样的,也就是说,只要Thread还在,那么这个对象就无法被回收。

那么,什么情况下,Thread会一直在呢,那就是线程池,这就导致value一直无法被回收。

5.2 如何解决

ThreadLocalMap底层使用数组来保存元素,使用“线性探测法”来解决hash冲突,在每次调用ThreadLocal的get、set、remove方法时,内部会实际调用ThreadLocalMap的get、set、remove等操作,而ThreaLocalMap的每次set、get、remove时,都会对key为null的Entry进行清除(expungeStateEntry()方法,将Entry的value清空,等下次GC就会被回收)。

所以,当我们一个ThreadLocal用完后,就手动remove一下,就可以在下次GC时,把Entry清理掉。

5.3 总结

上述我们分了两种情况来看ThreadLocal内存泄漏问题:

  • key使用强引用:引用ThreadLocal的对象被回收了,但是ThreadLocalMap持有ThreadLocal的强引用,如果没有手动remove,ThreadLocal不会被回收,导致Entry内存泄漏。

  • key使用弱引用:引用ThreadLocal被回收,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动remove,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动remove,就会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal被清理后key为null,对应的value在下一次ThreadLocalMap调用set、get、remove的时候可能会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期和Thread一样长,如果没有手动remove就会导致内存泄漏,而不是因为弱引用。

End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。

http://www.lryc.cn/news/171534.html

相关文章:

  • 2023复旦大学计算机科学技术(网络空间安全)保研记录
  • linux系统通过docker安装python的jieba,如何找到jieba路径替换分词文件
  • Python Functions-函数
  • 【人工智能】机器学习的入门与提升
  • WEB漏洞原理之---【XMLXXE利用检测绕过】
  • element-table排序icon没有点亮
  • 传统的经典问题 Java 的 Interface 是干什么的
  • Linux 文件 目录管理
  • QT信号槽实现原理
  • 7-7 求鸡兔数量
  • CTF 全讲解:[SWPUCTF 2022 新生赛]webdog1__start
  • 聊天机器人
  • 肖sir__mysql之综合题练习__013
  • 阿里云服务器部署安装hadoop与elasticsearch踩坑笔记
  • Golang 中 int 类型和字符串类型如何相互转换?
  • **20.迭代器模式(Iterator)
  • 计算机视觉与深度学习 | 视觉里程计理论
  • Go网络请求中配置代理
  • 【ArcGIS】基本概念-矢量空间分析
  • OSCP系列靶场-Esay-Moneybox保姆级
  • [python 刷题] 128 Longest Consecutive Sequence
  • SpringMVC之JSON数据返回与异常处理机制
  • 【第四阶段】kotlin语言的定义类和field关键字学习
  • OpenResty使用漏桶算法实现限流
  • Activiti源码跟踪之模型Model操作
  • C#-WinForm-发送邮件
  • Springboot整合jdbc和Mybatis
  • 日常生活中的常用命令及操作
  • 【C++杂货铺】国庆中秋特辑——多态由浅入深详细总结
  • MongoDB基础详解