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

ThreadLocal 详解——这一次彻底掌握

大家介绍ThreadLocal这个工具类,相信大家都或多或少听说过用过这个工具类,但是这个工具类还是有难点的,包括它的用法,实际上没那么容易。

文章目录

  • 1 两大使用场景——ThreadLocal的用途
    • 1.1 典型场景一:每个线程需要有一个独享的对象
    • 1.2 典型场景二:每个线程内需要保存的全局变量
  • 2 使用ThreadLocal带来的好处
  • 3 主要方法介绍
    • 3.1 initialValue
    • 3.2 set
    • 3.3 get
    • 3.4 remove
  • 4 ThreadLocal原理
  • 5 ThreadLocal 注意点
    • 5.1 内存泄漏
      • 5.1.1 key 发生内存泄漏
      • 5.1.1 value 发生内存泄漏
      • 5.1.3 ThreadLocal 内存泄漏分析
    • 5.1 空指针异常
    • 5.2 共享对象
    • 5.3 Spring框架中使用ThreadLocal
    • 5.3.1 DateTimeContextHolder
    • 5.3.2 RequestContextHolder

1 两大使用场景——ThreadLocal的用途

1.1 典型场景一:每个线程需要有一个独享的对象

对于一些工具类对象,由于其中很多方法不是线程安全的,如果每个线程都共用同一个工具类对象,就会产生线程安全问题。其中最典型的工具类就是SimpleDateFormat和Random,它们的对象有很多方法都是线程不安全的,所以用ThreadLocal来做比较合适。

该厂就想强调的是每个线程有自己的实例副本,不共享,调用这个对的方法也相当于单线程调用,并不会产生线程安全问题。举个例子,只有一本教材,每个同学都在这个教材上做笔记,就会出现问题,现在每个同学发一本教材,同学们分别在自己的教材上做笔记就没有问题。下面以SimpleDateFromat为例,来看看为什么要用ThreadLocal。

public class SimpleDataFormatDemo01 {public static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");public static void main(String[] args) {Thread t1 = new Thread(()-> System.out.println(date(10)));Thread t2 = new Thread(()-> System.out.println(date(100000)));t1.start();t2.start();}public static String date(int seconds){// 从 1970-01-01 00:00:00 开始计时,GMT时间// 中国是东八区,等于GMT计时 + 8Date date = new Date(seconds * 1000);String format = sdf.format(date);return format;}
}

控制台输出如下:
在这里插入图片描述
可以看到虽然两个线程传入的参数不一样,但是输出的日期都是一样的,说明这个方法在并发访问下有问题,不是线程安全的。这是由于所有线程都共用一个SimpleDataFormat对象,造成的。

可以通过加锁来解决并发访问线程安全问题,但是加锁会导致并发访问的效率降低,如果线程数量很多,只能排队一个一个来执行工具方法。那有没有更好的解决方法呢?

这里我们可以使用ThreadLocal,让每一个线程使用一个ThreadLocal实例,但是如果线程数量很多,会不会造成空间的浪费?这个问题我们可以通过线程池解决,利用线程池控制线程的数量,复用线程来执行大量的任务,既解决了锁带来的执行效率问题,也解决了每个线程使用一个ThreadLocal对象带来的空间浪费问题。假设有1000个任务,线程池控制线程的数量为10,来执行这1000个任务。整个解决方法方案的构思图如下:
在这里插入图片描述
下面用代码来实现上面这个思路:

public class SimpleDataFormatDemo01 {public static void main(String[] args) {ExecutorService threadPool = Executors.newFixedThreadPool(10);for (int i = 0; i < 1000; i++) {int finalI = i;threadPool.execute(()->{System.out.println(date(finalI));});}}public static String date(int seconds){// 从 1970-01-01 00:00:00 开始计时,GMT时间// 中国是东八区,等于GMT计时 + 8Date date = new Date(seconds * 1000);SimpleDateFormat sdf = ThreadSafeSimpleDateFormat.simpleDateFormatThreadLocal.get();String format = sdf.format(date);return format;}
}class ThreadSafeSimpleDateFormat{public static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){// 通过匿名内部类的方式创建ThreadLocal对象// 并重写initValue方法@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");}};
}

这里创建ThreadLocal对象的时候,使用了匿名内部类的方式,继承ThreadLocal类,并让ThreadLocal类指向子类对象,目的是重写父类的initialValue方法。

这里除了通过匿名内部类的方式重写initialValue方法,也可以通过以下方式:

public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
ThreadLocal.withInitial(new Supplier<SimpleDateFormat>() {@Overridepublic SimpleDateFormat get() {return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");}
});

为什么可以这样创建了,我们看一下withInitial方法的源码:

 public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {return new SuppliedThreadLocal<>(supplier);}

可以看到withInitial是一个静态的工具方法,而且是泛型方法,返回的是ThreadLocal对象,需要传入一个Supplier对象,我们来看一下Supplier的源码:

@FunctionalInterface
public interface Supplier<T> {T get();
}

Supplier是一个接口,实现该接口的类需要重写get方法。这里我们重写了get方法,返回SimpleDateFormat对象:

public SimpleDateFormat get() {return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");}

然后将Supplier接口对象作为参数传递给SuppliedThreadLocal类的构造函数,并开始构造SuppliedThreadLocal对象,我们看一下SuppliedThreadLocal类的源码:

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {private final Supplier<? extends T> supplier;SuppliedThreadLocal(Supplier<? extends T> supplier) {this.supplier = Objects.requireNonNull(supplier);}@Overrideprotected T initialValue() {return supplier.get();}
}

SuppliedThreadLocal类继承了ThreadLocal类,并重写了initialValue方法,该方法的实现调用了supplier.get()方法,而supplier.get()根据前面重写的接口,会返回一个SimpleDateFromat对象,实现了前面的功能。

1.2 典型场景二:每个线程内需要保存的全局变量

比如有以下场景,通过拦截器获取到当前请求的用户信息,然后Controller层、Service层等都要用到该用户信息,有一种解决方法就是把用户信息作为参数传递,但是这样就提高了代码的复杂度,增加了方法的复杂性,每个方法都要传入一个用户信息参数。使用ThreadLocal可以避免传参。

下图是使用传参的方式,可以发现,在多层调用时,每层的方法都要加上User对象参数,这无疑提高了代码的冗余以及模块之间的耦合。
在这里插入图片描述

下图是通ThreadLocal的方式,方法中就无需加上用户参数:
在这里插入图片描述
用户信息都保存到哪里呢,后面讲解ThreadLocal原理时会详细地介绍,这里就用一幅图先展示在这里,了解一下就行,不理解没关系:
在这里插入图片描述

2 使用ThreadLocal带来的好处

ThreadLocal好处主要有以下几点:

  • 达到线程安全
  • 不需要加锁,提高并发执行效率
  • 免去传参的繁琐,降低代码耦合度

3 主要方法介绍

ThreadLocal 中有几个比较重要的方法,下面我们来一一看一下

3.1 initialValue

谈initialValue必须谈get方法,该方法实际上是一个延迟加载的方法,在调用get方法的时候才会被调用。

触发时机:当调用ThreadLocal的get方法,但是当前线程的threadlocals成员变量指向空时。此时说明当前线程并没有向ThreadLocal对象set键值对,get方法里面会调用initialValue方法并返回初始化的值,如果没有重写该方法,默认返回null。

3.2 set

set方法为当前线程设置一个新的值

3.3 get

get方法会获取当前线程设置的值,如果没有设置值,会触发initalValue方法。

3.4 remove

删除当前线程设置的值,其它线程设置的值。

4 ThreadLocal原理

要想理解ThreadLocal的原理,需要理清楚ThreadLocal、Thread、ThreadLocalMap三个类之间的关系,每个Thread对象都持有了一个ThreadLocalMap成员变量,我们来看下面这段代码:

public class ThreadLocalTest {// 定义三个静态的 ThreadLocal 变量,用于存储线程局部变量static ThreadLocal threadLocal1 = new ThreadLocal<>();static ThreadLocal threadLocal2 = new ThreadLocal<>();static ThreadLocal threadLocal3 = new ThreadLocal<>();public static void main(String[] args) {// 创建三个线程,每个线程都会执行相同的逻辑for (int i = 0; i < 3; i++) {new Thread(() -> {// 在线程局部变量 threadLocal1 中存储整数 123threadLocal1.set(123);// 尝试从 threadLocal2 中获取值,初始情况下 threadLocal2 没有设置值,因此会输出 nullSystem.out.println(threadLocal2.get());// 在线程局部变量 threadLocal2 中存储字符串 "abc"threadLocal2.set("abc");// 再次从 threadLocal2 中获取值,这次会输出 "abc"System.out.println(threadLocal2.get());// 在线程局部变量 threadLocal3 中存储一个 User 对象threadLocal3.set(new User("Jack", 18));// 从 threadLocal3 中获取并输出 User 对象,输出格式为 User{age=Jack, name=18}System.out.println(threadLocal3.get());}).start(); // 启动线程}}
}// 定义一个 User 类,包含两个属性:age(年龄)和 name(名字)
class User{String age; // 用户的年龄,类型为 Stringint name;   // 用户的名字,类型为 int(这里可能是写反了,通常名字应该是 String,年龄是 int)// 构造函数,用于初始化 User 对象的 age 和 name 属性public User(String age, int name) {this.age = age;this.name = name;}// 重写 toString 方法,方便输出 User 对象的字符串表示@Overridepublic String toString() {return "User{" +"age=" + age +", name=" + name +'}';}
}

在这段代码中,我们启动了三个线程,每个线程都用threadlocal来存储,用一幅图将上面代码中的对象的内存分布画出来:
在这里插入图片描述
每一个线程,即Thread类对象,有一个成员变量,叫threadLocals,类型为ThreadLocal类里面的静态内部类类型,ThreadLocalMap,我们可以查看Thread类的底层源码,找到这个变量的定义:

ThreadLocal.ThreadLocalMap threadLocals = null;

这个ThreadLocalMap静态内部类本质上就是一个Map,存储key-value键值对,其中key为每一个ThreadLocal对象,value为各种类型的对象。这一点可以从threadlocal对象的get和set方法源码进行验证,首先看set方法:

public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {map.set(this, value);} else {createMap(t, value);}
}

首先,调用thradlocal的set方法只需传入一个参数,value,就是要存储的值,进入该方法后第一步是获取当前线程t,然后调用getMap方法,将当前线程做参数传递进去,我们来看getMap方法是如何实现的:

ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}

getMap方法的实现很简单,直接将当前线程的threadlocals成员变量返回。
回到set方法,获取到当前线程的ThreadLocalMap成员变量后,会判断这个map是否为空,如果为空,就要创建,即调用createMap方法,该方法实现如下:

void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}

createMap方法的实现也很简单,就是new一个ThreadLocalMap对象,然后让当前线程的threadLocals成员变量指向这个对象。
那如果当前的map不为空,就会调用这个map的set方法,存储键值对,其中键传入的是this,代表当前这个threadlocal对象,而值就是我们传入的参数:

map.set(this, value);

这样每个线程都保存了一一份ThreadLocalMap对象,ThreadLocalMap是ThreadLocal类中的静态内部类,ThreadLocalMap对象本质上是map对象,存储键值对,键就是ThreadLocal对象。

set方法介绍完之后,get方法就很简单了,我们看一下get方法的源码:

public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();
}

逻辑上和set方法差不多,首先获取当前线程,然后获取ThreadLocalMap对象,如果这个map为空,就初始化map并返回一个初始值,如果这个map不为空,则以当前threadlocal对象为key,取map里面查找。

这里setInitialValue()方法返回的是什么值,我们看一下这个方法的实现:

private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {map.set(this, value);} else {createMap(t, value);}if (this instanceof TerminatingThreadLocal) {TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);}return value;
}

可以看到,这个方法同样会创建map,如果map为空,因为要把初始值放进去,这里初始值是这句代码:

T value = initialValue();

也就是进入方法后的第一句代码,我们看initialValue方法的源码:

protected T initialValue() {return null;
}

这里可以看到初始实现就是直接返回null,当然,可以通过继承ThreadLocal类,然后重写initalValue方法来实现自定义的初始化。例如这段代码:

public static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){// 通过匿名内部类的方式创建ThreadLocal对象// 并重写initValue方法@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");}
};

这里就是通过匿名内部类的方式,这个匿名内部类继承ThreadLocal类,重写了initialValue方法,在以后调用threadlocal的get方法的时候,如果该线程的threadlocals变量为空,就会调用setInitialValue方法,该方法首先会调用initialValue方法获取value,然后创建ThreadLocalMap实例对象,并让线程的threadlocals变量指向这个实例对象,最后将value存放到ThreadLoclMap中,key就是当前threadlocal对象实例。

所以通过这样的方式,就无需先set了,线程直接调用get方法,如果没有ThreadLocalMmap,就会创建,并存入键值对。

为什么ThreadLocalMap要用ThreadLocal对象作为key呢?
因为一个线程可能拥有多个ThreadLocal对象。

为什么说ThreadLocal是一个工具类呢?
因为ThreadLocal本身不存储任何信息,所有的信息都是存储在Thread对象,也就是线程里面,ThreadLocal只是提供了ThreadLocalMao静态类,set/get工具方法,所以ThreadLocal是一个工具类。

5 ThreadLocal 注意点

下面我们来看ThreadLocal使用时应该注意哪些东西。

5.1 内存泄漏

内存泄漏是指我们创建的对象,不适用后没有即使的回收,就会在分配内存给新对象时不足,表现就好像内存泄漏了一样,理论上的剩余内存和实际的剩余内存不一致。

那么ThreadLocal中哪一步做错了,会导致内存泄漏呢?我们下面来看一下,发生内存泄漏有两种可能:key发生内存泄漏和value发生内存泄漏。

5.1.1 key 发生内存泄漏

要想将这个key发生内存泄漏,需要从ThreadLocalMap底层源码看,主要是看Entry类:

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

Entry类继承WeakReference,是弱引用。Java中有四种引用,垃圾回收机制对这四种引用有不同的行为:

  1. 强引用:垃圾回收机制不会回收,就是报OOM错误也不会回收。
  2. 软引用:内存空间不足时,会回收这个对象。
  3. 弱引用:不管内存空间是否充足,只要有垃圾回收发生,就会回收该对象。
  4. 虚引用:虚引用就跟没引用一样。

Entry类这里的key使用交给父类的构造函数构造的,而父类时弱引用,相当于key是弱引用的,而value是强引用指向的,key就有了内存泄漏的可能。

ThreadLocalMap的每个Entry都是一个对key的弱引用,同时,每个Entry都包含了一个对valu的强引用。

5.1.1 value 发生内存泄漏

正常情况下,当线程终止,保存在ThreadLocalMap对象里面的value会被垃圾回收,但是,如果线程不中止(比如线程需要保持很久),那么此时就会有如下调用链:
在这里插入图片描述
Entry的key是可以回收的,但是value由于是强引用,回收不了。
比如此时某个ThreadLocal实例不使用了,此时会把指向该实例对象的变量置为null,那么此时由于Entry里面的key是对ThreadLocal实例对象是弱引用,所以可以被垃圾回收器回收,但是value指向的对象确回收不了,因为存在上述调用链。

JDK已经考虑过这个问题,所以在set、remove、rehash方法中会扫描key为null的Entry,并把对应的value也设置为null,这样value对象就可以回收。

但是如果一个ThreadLocal实例对象不被使用了,其方法也不会被调用,还是会发生内存泄漏,所以我们要用到一些规约:
在使用完ThreadLocal后,主动调用一次remove方法

5.1.3 ThreadLocal 内存泄漏分析

我们一一段代码为例,画出其堆栈内存结构图:

public class ThreadLocalLeak01 {public static void main(String[] args) {ThreadLocal<User> local = new ThreadLocal<>();Thread t = new Thread(new Runnable() {@Overridepublic void run() {local.set(new User("jack",18));}});}
}
class User{String name;int age;public User(String age, int name) {this.name = age;this.age = name;}
}

在这里插入图片描述

其中实心箭头表示强引用,虚线箭头表示弱引用

从上图可以看出,当ThreadLocal对象不存在外部强引用时,只有k指向了ThreadLocal对象,此时,k指向ThreadLocal对象是一个弱引用,如果发生垃圾回收,k会被指向null,ThreadLocal对象会被垃圾回收线程给回收掉。而User对象还存在着t -> Thread -> threadlocals -> ThreadLocalMap -> v -> User 这样一个强引用链条,所以不会被垃圾回收线程回收,从而造成内存泄漏。

为什么key使用弱引用,而不是强引用?
假设法,假设key使用强引用,回收ThreadLocal对象时,即使local指向null,但是ThreadLocalMap对象里面的k还保持着对ThreadLocal对象的强引用,所以无法回收ThreadLocal对象。

JDK 如何解决value内存泄漏问题?
在用户调用ThreadLocal的set、get、remove方法时,会检查entry的key是否为null,如果有key为null,也会把对应的value置为null,这样既可以保证被垃圾回收

5.1 空指针异常

threadlocal在没有set之前直接get,如果没有重写initialValue方法,会返回一个null。此时如果把这个null用来使用,比如调用其方法,数据类型拆箱会导致空指针异常

5.2 共享对象

在使用treadlocal set方法设置对象时,如果每次设置的都是相同的对象,那么不同的线程都执指向的同一个对象。在get后获得的是同一个对象,在并发使用该对象时,还是会有线程安全问题。
在这里插入图片描述

5.3 Spring框架中使用ThreadLocal

Spring框架中有多处使用到了ThreadLocal,我们如果要用ThreadLocal
,尽量用框架提供的,因为框架提供的ThreadLocal类会自动调用remove方法,防止value内存泄漏。

5.3.1 DateTimeContextHolder

我们以DateTimeContextHolder类为例,来看一下Spring框架中是如何使用ThreadLocal的。该类的源码如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//package org.springframework.format.datetime.standard;import java.time.format.DateTimeFormatter;
import java.util.Locale;
import org.springframework.core.NamedThreadLocal;
import org.springframework.lang.Nullable;public final class DateTimeContextHolder {private static final ThreadLocal<DateTimeContext> dateTimeContextHolder = new NamedThreadLocal("DateTimeContext");private DateTimeContextHolder() {}public static void resetDateTimeContext() {dateTimeContextHolder.remove();}public static void setDateTimeContext(@Nullable DateTimeContext dateTimeContext) {if (dateTimeContext == null) {resetDateTimeContext();} else {dateTimeContextHolder.set(dateTimeContext);}}@Nullablepublic static DateTimeContext getDateTimeContext() {return (DateTimeContext)dateTimeContextHolder.get();}public static DateTimeFormatter getFormatter(DateTimeFormatter formatter, @Nullable Locale locale) {DateTimeFormatter formatterToUse = locale != null ? formatter.withLocale(locale) : formatter;DateTimeContext context = getDateTimeContext();return context != null ? context.getFormatter(formatterToUse) : formatterToUse;}
}

这个类本身源码不是很长,类中的第一句就定义了ThreadLocal变量:

 private static final ThreadLocal<DateTimeContext> dateTimeContextHolder = new NamedThreadLocal("DateTimeContext");

这里NamedThreadLocal是自定义的类,继承ThreadLocal类,查看该类的源码:

public class NamedThreadLocal<T> extends ThreadLocal<T> {private final String name;public NamedThreadLocal(String name) {Assert.hasText(name, "Name must not be empty");this.name = name;}public String toString() {return this.name;}
}

可以看到,NamedThreadLocal的构造函数需要传入一个name,这里由于ThreadLocl类定义了一个无参构造函数,所以在NamedThreadLocal类的构造函数编译器会自动调用父类无参构造,相当于这样:

/*** Creates a thread local variable.* 这是ThreadLocla类的无参构造函数* @see #withInitial(java.util.function.Supplier)*/
public ThreadLocal() {
}
public NamedThreadLocal(String name) {super() // 编译器会自动加上Assert.hasText(name, "Name must not be empty");this.name = name;
}

NamedThreadLocal 重写了toString方法,在打印NamedThreadLocal对象时,会将name打印出来。

5.3.2 RequestContextHolder

RequestContextHolder 也使用到了ThreadLocal类:

private static final boolean jsfPresent = ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");

这里定义了两个ThreadLocal,一个存放的是请求的熟悉,一个存放是请求上下文信息。里面通过setXXX/getXXX设置获取属性。


http://www.mrgr.cn/news/67465.html

相关文章:

  • 使用CentOS宝塔面板docker搭建EasyTier内网穿透服务
  • 最新榜单!国内免费好用的OA协同软件前十名
  • 构造一个具有特定边界和向量场性质的紧致4维流形,并计算其上曲率形式的特定积分
  • ORACLE RAC用DNS服务器的配置
  • CST参数扫描设置细节
  • ChatGPT的多面手:日常办公、论文写作与深度学习的结合
  • 【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
  • HTML学习笔记十三
  • YoloV8改进策略:上采样改进:CARAFE:轻量级上采样|即插即用|附改进方法+代码
  • TOEIC 词汇专题:市场销售篇
  • (免费领源码)node.js#koa#mysql点餐系统app 84406-计算机毕设 原创
  • 详解Java操作PDF:一键生成文件,插入文字、选项、签名及公章
  • arm中内存读取延迟性能测试
  • [含文档+PPT+源码等]精品基于PHP实现的会员综合管理平台的设计与实现
  • java ssm 社团管理系统 高校社团信息平台 三个角色社长 源码 jsp
  • 对比Java和TypeScript中的服务注册和查找机制
  • 计算机体系结构之多级缓存、缓存miss及缓存hit(二)
  • 遥感数字图像处理
  • TLS(Transport Layer Security,传输层安全协议)
  • 【Node-Red】一款可以自定义的table节点