Java线程中的ThreadLocal原理及源码解析
作者:外星喵
ThreadLocal介绍
ThreadLocal,线程本地变量,ThreadLocal 的作用是为每个线程保存一份局部变量的引用,实现多线程之间的数据隔离,从而避免了线程不安全情况的发生。这个变量保存的值只在线程的生命周期内起作用,通过使用它减少了将执行上下文信息传递到每个方法的需要。
如果多个线程同时在一个对象/实例上执行,它们将共享这个实例变量,如果不使用ThreadLocal,就需要在每个方法上传递参数,去跨对象共享这些变量,同时还会导致线程不安全的问题。
许多框架使用 ThreadLocals 来维护与当前线程相关的一些上下文。例如,当前事务存储在 ThreadLocal 中时,您不需要通过每个方法调用将其作为参数传递,以防堆栈中的某个人需要访问它。Web 应用程序可能会将有关当前请求和会话的信息存储在 ThreadLocal 中,以便应用程序可以轻松访问它们。
ThreadLocal 原理
ThreadLocals 是一种全局变量(尽管由于它们仅限于一个线程而稍微不那么邪恶),因此在使用它们时应该小心以避免不必要的副作用和内存泄漏。
每个Thread对象,专门用一个ThreadLocalMap来存储自己的私有对象。ThreadLocalMap实际上就跟我们常用的HashMap类似,存储在那里的Key-Value形式的数据。
ThreadLocal在每次获取或设置操作时,都先通过Thread.currentThread()方法来获取当前线程,再从当前线程中获取ThreadLocalMap。而实际上,保存的值是通过ThreadLocalMap来存储的。
ThreadLocal对象可以是多线程共享,但ThreadLocalMap对象却是一个线程独享的,每个线程对象,创建一个自己专属的ThreadLocalMap,与其他Thread对象创建的ThreadLocalMap不存在一个单一的关系。
当多个Thread对象共同访问同一个ThreadLocal对象时,threadLocal只是作为ThreadLocalMap的Key存在,而不是作为变量的存储位置。threadLocal的set(方法和get()方法涉及的值是存储为ThreadLocalMap的值而ThreadLocalMap是每个线程专属的,互不相同的。这就是为什么同ThreadLocal被多线程同时访问,ThreadLocal的值却互不干扰的原理。
ThreadLocalMap
ThreadLocalMap该类的核心部分是Entry class,它扩展了WeakReference. 它确保如果当前线程退出,它将被自动垃圾收集。这就是为什么它使用ThreadLocalMap而不是简单的HashMap. 它将当前ThreadLocal及其值作为Entry类的参数传递,所以当我们想要获取值时,我们可以从 中获取它table.
- 每个线程中都有一个自己的 ThreadLocalMap 类对象,可以将线程自己的对象保持到其中, 各管各的,线程可以正确的访问到自己的对象。
- 将一个共用的 ThreadLocal 静态实例作为 key,将不同对象的引用保存到不同线程的 ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取 得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
- ThreadLocalMap其实就是线程里面的一个属性,它在Thread类中定义
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal使用场景
代替参数的显式传递
当我们在写API接口的时候,通常Controller层会接受来自前端的入参,当这个接口功能比较复杂的时候,可能我们调用的Service层内部还调用了 很多其他的很多方法,通常情况下,我们会在每个调用的方法上加上需要传递的参数。
但是如果我们将参数存入ThreadLocal中,那么就不用显式的传递参数了,而是只需要ThreadLocal中获取即可。
全局存储用户信息
在现在的系统设计中,前后端分离已基本成为常态,分离之后如何获取用户信息就成了一件麻烦事,通常在用户登录后, 用户信息会保存在Session或者Token中。这个时候,我们如果使用常规的手段去获取用户信息会很费劲,拿Session来说,我们要在接口参数中加上HttpServletRequest对象,然后调用 getSession方法,且每一个需要用户信息的接口都要加上这个参数,才能获取Session,这样实现就很麻烦了。 当请求到来时,可以将当前Session信息存储在ThreadLocal中,在请求处理过程中可以随时使用Session信息,每个请求之间的Session信息互不影响。当请求处理完成后通过remove方法将当前Session信息清除即可。
解决线程安全问题
在Spring的Web项目中,我们通常会将业务分为Controller层,Service层,Dao层, 我们都知道@Autowired注解默认使用单例模式,那么不同请求线程进来之后,由于Dao层使用单例,那么负责数据库连接的Connection也只有一个, 如果每个请求线程都去连接数据库,那么就会造成线程不安全的问题,Spring是如何解决这个问题的呢?
在Spring项目中Dao层中装配的Connection肯定是线程安全的,其解决方案就是采用ThreadLocal方法,当每个请求线程使用Connection的时候, 都会从ThreadLocal获取一次,如果为null,说明没有进行过数据库连接,连接后存入ThreadLocal中,如此一来,每一个请求线程都保存有一份 自己的Connection。于是便解决了线程安全问题
ThreadLocal源码
以下是ThreadLocal的get()、set()、remove()方法的代码
/** * 返回当前线程的 this 副本中的值 * 线程局部变量。如果变量没有值 * 当前线程,首先初始化为返回值 * 通过调用 {@link #initialValue} 方法。 * * @return 这个线程本地的当前线程的值 */ public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); } /** * 设置这个线程局部变量的当前线程的副本 * 到指定值。大多数子类将不需要 * 重写此方法,仅依赖于 {@link #initialValue} * 设置线程局部变量值的方法。 * * @param value 要存储在当前线程的副本中的值。 */ public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } /** * 删除此线程本地的当前线程的值 * 多变的。如果此线程局部变量随后 * {@linkplain #get read} 被当前线程读取,其值为 * 通过调用其 {@link #initialValue} 方法重新初始化, * 除非它的值是当前线程的 {@linkplain #set set} * 在过渡期。这可能会导致多次调用 * 当前线程中的 <tt>initialValue</tt> 方法。 * * @自 1.5 */ public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
ThreadLocal内存溢出问题
内存溢出问题模拟
在执行main方法前,先使用“-Xmx50m”的参数来配置一下 Idea,它表示将程序运行的最大内存设置为 50m,如果程序的运行超过这个值就会出现内存溢出的问题
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadLocalOOMExample { /** * 定义一个 10m 大的类 */ static class MyTask { // 创建一个 10m 的数组(单位转换是 1M -> 1024KB -> 1024*1024B) private byte[] bytes = new byte[10 * 1024 * 1024]; } // 定义 ThreadLocal private static ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>(); // 主测试代码 public static void main(String[] args) throws InterruptedException { // 创建线程池 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100)); // 执行 10 次调用 for (int i = 0; i < 10; i++) { // 执行任务 executeTask(threadPoolExecutor); Thread.sleep(1000); } } /** * 线程池执行任务 * @param threadPoolExecutor 线程池 */ private static void executeTask(ThreadPoolExecutor threadPoolExecutor) { // 执行任务 threadPoolExecutor.execute(new Runnable() { @Override public void run() { System.out.println("创建对象"); // 创建对象(10M) MyTask myTask = new MyTask(); // 存储 ThreadLocal taskThreadLocal.set(myTask); // 将对象设置为 null,表示此对象不在使用了 myTask = null; } }); } }
原因分析
由于每个线程 Thread 都拥有一个数据存储容器 ThreadLocalMap,当执行 ThreadLocal.set 方法执行时,会将要存储的值放到 ThreadLocalMap 容器中。而ThreadMap 中有一个 Entry[] 数组用来存储所有的数据,而 Entry 是一个包含 key 和 value 的键值对,其中 key 为 ThreadLocal 本身,而 value 则是要存储在 ThreadLocal 中的值。
也就是说它们之间的引用关系是这样的:Thread -> ThreadLocalMap -> Entry -> Key,Value,因此当我们使用线程池来存储对象时,因为线程池有很长的生命周期,所以线程池会一直持有 value 值,那么垃圾回收器就无法回收 value,所以就会导致内存一直被占用,从而导致内存溢出问题的发生。
解决方案
严格来讲内存溢出并不是 ThreadLocal 的问题,而是因为没有正确使用 ThreadLocal 所带来的问题。想要避免 ThreadLocal 内存溢出的问题,只需要在使用完 ThreadLocal 后调用 remove 方法即可。
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class App { /** * 定义一个 10m 大的类 */ static class MyTask { // 创建一个 10m 的数组(单位转换是 1M -> 1024KB -> 1024*1024B) private byte[] bytes = new byte[10 * 1024 * 1024]; } // 定义 ThreadLocal private static ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>(); // 测试代码 public static void main(String[] args) throws InterruptedException { // 创建线程池 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100)); // 执行 n 次调用 for (int i = 0; i < 10; i++) { // 执行任务 executeTask(threadPoolExecutor); Thread.sleep(1000); } } /** * 线程池执行任务 * @param threadPoolExecutor 线程池 */ private static void executeTask(ThreadPoolExecutor threadPoolExecutor) { // 执行任务 threadPoolExecutor.execute(new Runnable() { @Override public void run() { System.out.println("创建对象"); try { // 创建对象(10M) MyTask myTask = new MyTask(); // 存储 ThreadLocal taskThreadLocal.set(myTask); // 其他业务代码... } finally { // 释放内存 taskThreadLocal.remove(); } } }); } }
到此这篇关于Java线程中的ThreadLocal原理及源码解析的文章就介绍到这了,更多相关ThreadLocal原理及源码内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!