ThreadLocal能解决线程安全问题?胡扯!本文教你正确的使用姿势【享学Java】

article/2025/9/14 8:23:33

跟对领导很重要:愿意教你的,并且放手让你做的领导要珍惜。

目录

    • 前言
    • 正文
      • ThreadLocal是什么?
      • ThreadLocal怎么用?
        • 局限性
        • InheritableThreadLocal向子线程传递数据
        • 开源框架使用示例
      • ThreadLocal不能解决共享变量的线程安全问题
      • ThreadLocal使用的正确姿势
    • 总结
  • 关注A哥

前言

ThreadLocal:线程 + 本地 -> 线程本地变量(所以说我觉得它取名叫ThreadLocalVariable获取还更能让人易懂些),它的出镜率可不低。虽然写业务代码一般用不着,但它是开源工具的常客:用于在线程生命周期内传递数据。

有的人说,每看一遍ThreadLocal都会有新的感受,这其实是比较诡异的现象,因为我认为“真理”是不应该经常变的(或者说是不可能变化的)。我自己百度了一波,关于ThreadLocal的文章满天飞,有讲使用的亦有讲原理的,鱼龙混杂。其中有一派文章主旨讲述:使用ThreadLocal解决多线程程序的并发问题,使用该工具写出简洁、优美的多线程程序

这类水文不在少数,大有占据主流的意思,它对初学者的误导性非常大,从而造成了每看一遍都会有新感受的错觉。本文为社区贡献一份微薄之力,在这里教你完全正确的使用ThreadLocal的姿势,避免你以后再犯迷糊。


正文

本文的内容并不讲述ThreadLocal/InheritableThreadLocal的源码、原理,一方面确实不难,另一方面关于它的源码、原理讲解的相关文章确实不在少数。


ThreadLocal是什么?

我们从字面上的意思来理解ThreadLocal,Thread:线程;Local:本地的,局部的。也就是说,ThreadLocal是线程本地的变量,只要是本线程内都可以使用,线程结束了,那么相应的线程本地变量也就跟随着线程消失了。

ThreadLocalInheritableThreadLocal均是JDK1.2新增的API,在JDK5后支持到了泛型。它表示线程局部变量:为当前线程绑定一个变量,这样在线程的声明周期内的任何地方均可取出。

说明:InheritableThreadLocal继承自ThreadLocal,在其基础上扩展了能力:不仅可在本线程内获取绑定的变量,在子线程内亦可获取到。(请注意:必须是子线程,若你是线程池就可能不行喽,因为线程池里的线程是实现初始化好的,并不一定是你的子线程~)

它仅有如下三个public方法:

public void set(T value) { ... }
public T get() { ... }
public void remove() { ... }

分别代表:

  • 设置值:把value和当前线程绑定
  • 获取值:获取和当前线程绑定的变量值
  • 删除值:移除绑定关系

说明:虽然每个绑定关系都是使用的WeakReference,但是还是建议你显示的做好remove()移除动作,否则容易造成内存泄漏。当然关于ThreadLocal内存泄漏并不是本文的内容,有兴趣可以自行去了解。

另外对于解释ThreadLocal是什么,建议可参考下它的Javadoc:

 * This class provides thread-local variables.  These variables differ from* their normal counterparts in that each thread that accesses one (via its* {@code get} or {@code set} method) has its own, independently initialized* copy of the variable.  {@code ThreadLocal} instances are typically private* static fields in classes that wish to associate state with a thread (e.g.,* a user ID or Transaction ID).

大致意思是:

该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量
(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程
(例如,用户 ID 或事务 ID)相关联。

更准确的说,一般我们使用ThreadLocal是作为private static final字段来使用的~


ThreadLocal怎么用?

知道了ThreadLocal是什么后,怎么用其实就非常简单了。看如下这个简单示例:

本例模拟使用Person对象和当前线程绑定:

@Setter
@ToString
private static class Person {private Integer age = 18;
}

书写测试代码:

private static final ThreadLocal<Person> THREAD_LOCAL = new ThreadLocal<>();@Test
public void fun1() {// 方法入口处,设置一个变量和当前线程绑定setData(new Person());// 调用其它方法,其它方法内部也能获取到刚放进去的变量getAndPrintData();System.out.println("======== Finish =========");
}private void setData(Person person){System.out.println("set数据,线程名:" + Thread.currentThread().getName());THREAD_LOCAL.set(person);
}
private void getAndPrintData() {// 拿到当前线程绑定的一个变量,然后做逻辑(本处只打印)Person person = THREAD_LOCAL.get();System.out.println("get数据,线程名:" + Thread.currentThread().getName() + ",数据为:" + person);
}

运行程序打印输出:

set数据,线程名:main
get数据,线程名:main,数据为:Test2.Person(age=18)
======== Finish =========

这便是ThreadLocal的典型应用场景:方法调用间传参,并不一定必须得从方法入参处传入进来,还可以通过ThreadLocal来传递,进而在该线程生命周期内任何地方均可获取到,非常的方便有木有

小细节:set和get数据时的线程是同一个线程:均未main线程


局限性

上例是ThreadLocal的典型应用场景,大部分情况下均能正常work。但是,在当下互联网环境下,经常会用到了异步方式来提高程序运行效率,比如如上方法调用getAndPrintData()因比较耗时所以我希望异步去进行,改造如下:

@Test
public void fun1() throws InterruptedException {// 方法入口处,设置一个变量和当前线程绑定setData(new Person());// getAndPrintData();// 异步获取数据Thread subThread = new Thread(() -> getAndPrintData());subThread.start();subThread.join();// 非异步方式获:在主线程里获取getAndPrintData();System.out.println("======== Finish =========");
}

运行程序,打印输出:

set数据,线程名:main
get数据,线程名:Thread-0,数据为:null
get数据,线程名:main,数据为:Test2.Person(age=18)
======== Finish =========

线程名为Thread-0的子线程里并没有获取到数据,只因为它并不是当前线程,貌似合情合理,这便是ThreadLocal的局限性。

那既然这是一个常见需求,除了把变量作为方法入参传进去,有没有什么更为便捷的方案呢?有的,JDK扩展了ThreadLocal提供了一个子类:InheritableThreadLocal,它能够向子线程传递数据。


InheritableThreadLocal向子线程传递数据

它继承自ThreadLocal,所以它能力更强:通过它set进去的数据,不仅本线程内任意地方可以获取,子线程(包括子线程的子线程…)内的任意地方也都可以获取到值。

重说三:必须是子线程,必须是子线程,必须是子线程(当然子线程的子线程…也算作这个范畴)

因此对于上例,只需做个微小的变化:

// 使用InheritableThreadLocal作为实现
private static final ThreadLocal<Person> THREAD_LOCAL = new InheritableThreadLocal<>();

再次运行测试程序,打印:

set数据,线程名:main
get数据,线程名:Thread-0,数据为:Test2.Person(age=18)
get数据,线程名:main,数据为:Test2.Person(age=18)
======== Finish =========

完美。

强调说明:其实这还不完美。还有非父子线程、垮线程池之间的数据产地它解决不了,对于这种场景较为复杂,源生JDK并没有“特效类”,一般需要借助阿里巴巴的开源库:TTL(transmittable-thread-local)来搞定,这个后面文章还会继续补充。


开源框架使用示例

优秀的开源框架中有非常多的对ThreadLocal的使用示例,这里以Spring的为例:

RequestContextHolder

public abstract class RequestContextHolder  {...private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal<>("Request attributes");private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal<>("Request context");...@Nullablepublic static RequestAttributes getRequestAttributes() {RequestAttributes attributes = requestAttributesHolder.get();if (attributes == null) {attributes = inheritableRequestAttributesHolder.get();}return attributes;}...
}

TransactionSynchronizationManager

public abstract class TransactionSynchronizationManager {...private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal<>("Transaction synchronizations");private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal<>("Current transaction name");private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal<>("Current transaction read-only status");private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal<>("Current transaction isolation level");private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal<>("Actual transaction active");...
}

如果你们公司做过全链路追踪、全链路压测,那么ThreadLocal将是其中使用最为频繁的基础组件之一。


ThreadLocal不能解决共享变量的线程安全问题

标题即是结论,请务必烂熟于胸,使用时请勿滥用。网上太多的文章说:ThreadLocal使得每个线程均持有这个变量的副本,所以对多线程是安全的。言外之意便是:

  • 只要这个变量是共享变量,把它用ThreadLocal包起来便可
  • 别的线程修改其线程绑定的变量,并不影响其它线程里的变量值

以上结果,如果你的ThreadLocal绑定的是Immutable不可变变量,如字符串等,那结论尚能成立,但若绑定的是引用类型的变量,结论可就大错特错喽,如下示例:

private static final ThreadLocal<Person> THREAD_LOCAL = new InheritableThreadLocal<>();@Test
public void fun2() throws InterruptedException {setData(new Person());Thread subThread1 = new Thread(() -> {Person data = getAndPrintData();if (data != null)data.setAge(100);getAndPrintData(); // 再打印一次});subThread1.start();subThread1.join();Thread subThread2 = new Thread(() -> getAndPrintData());subThread2.start();subThread2.join();// 主线程获取线程绑定内容getAndPrintData();System.out.println("======== Finish =========");
}private void setData(Person person) {System.out.println("set数据,线程名:" + Thread.currentThread().getName());THREAD_LOCAL.set(person);
}private Person getAndPrintData() {// 拿到当前线程绑定的一个变量,然后做逻辑(本处只打印)Person person = THREAD_LOCAL.get();System.out.println("get数据,线程名:" + Thread.currentThread().getName() + ",数据为:" + person);return person;
}

对本实例模拟的场景做如下文字解释:

  1. 主线程设置一个共享变量Person(age=18),希望子线程得以共享
  2. 创建两个子线程subThread1/subThread2用于模拟多个线程,共享访问Person这个共享变量
  3. 线程subThread1在其执行过程中,把共享变量Person的age值改为了100
  4. 线程subThread2以及主线程此时也去获取共享变量Person,情况如何呢?

运行测试程序,打印如下:

set数据,线程名:main
get数据,线程名:Thread-0,数据为:Test2.Person(age=18)
get数据,线程名:Thread-0,数据为:Test2.Person(age=100)
get数据,线程名:Thread-1,数据为:Test2.Person(age=100)
get数据,线程名:main,数据为:Test2.Person(age=100)
======== Finish =========

看到这个结果,你或许会傻眼。不是拷贝了一个副本吗,为何最终值也变了呢?可以明确的告诉你,这不是ThreadLocal有错,而是你没有理解它。

结论:线程subThread1把共享变量Person的值改过之后,其它线程再去获取得到的均是改变后的值,因此此处使用ThreadLocal并没有达到决绝共享变量线程安全问题的效果。

这是最为典型的一种错误认知,希望通过此例能帮你纠正你以前对ThreadLocal的理解和看法(有错则改之嘛~)。
为何会出现此现象,是因为这里面所谓的变量副本都是“引用传递”来着,可以用如下程序证明:

@Test
public void fun3() throws InterruptedException {setData(new Person());new Thread(() -> System.identityHashCode(THREAD_LOCAL.get())).start();new Thread(() -> System.identityHashCode(THREAD_LOCAL.get())).start();TimeUnit.SECONDS.sleep(2);System.out.println(System.identityHashCode(THREAD_LOCAL.get()));System.out.println("======== Finish =========");
}

运行程序,控制台输出:

set数据,线程名:main
434455603
434455603
434455603
======== Finish =========

可以看到,不管是主线程还是子线程,绑定的变量的HashCode一模一样。这样就更能解释了:为何一处修改,其它均被修改了吧,因为指向是同一位置。

因此:ThreadLocal包装根本就不能解决共享变量的多线程安全问题


ThreadLocal使用的正确姿势

说了这么多,那使用它的正确姿势是什么呢?正确姿势用文字无法表达,请以如下使用示例为参照。

众所周知,SimpleDateFomat是线程不安全的,所以若我们这样定义一个全局模版:

public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

在多线程访问的情况下,那必然会有多线程安全问题。

而通过如上表述,这么做也依旧是不靠谱的,依旧解决不了多线程安全问题。

public static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new InheritableThreadLocal<>();
static {DATE_FORMAT_THREAD_LOCAL.set(new SimpleDateFormat("yyyy-MM-dd"));
}

其实关于它的使用,阿里巴巴已经在它的规范手册里给出了使用示范:

public static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new InheritableThreadLocal<DateFormat>() {@Overrideprotected DateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd");}
};

这么处理后再使用DateFormat这个实例,就是绝对安全的。理由是每次调用set方法进行和线程绑定的时候,都是new一个新的SimpleDateFormat实例,而并非全局共享一个,不存在了数据共享那必然就线程安全喽。

当然你可能会说,这和自己在线程里面每次你自己new一个出来用有什么区别呢?答案是:效果上没任何区别,但是这样方便。比如:可以保持线程里面只有唯一一个SimpleDateFormat对象,你要手动new的话,一不小心就new多个了,消耗内存不说还不好管理。

可能你还会说,那只new一个实例,然后哪个方法要用就通过参数传给它就行。答案还是一样的:不嫌麻烦的话,这样做也是能达到效果的。

然而,对于这种全局通用的变量,使用ThreadLocal管理和维护一份即可,大大的降低了维护成本和他人的使用成本。so,只要你使用它的姿势正确了,它能让你事半功倍,特别是如果你是写中间件的小伙伴的话,跟它打交道会更为频繁。


总结

本文总体上算是一篇纠错文章,希望更多人能够看到,多多转发,为社区献上微薄之力。

ThreadLocal并不是为了解决线程安全问题,而是提供了一种将变量绑定到当前线程的机制,类似于隔离的效果。ThreadLocal跟线程安全基本不搭边:线程安全or不安全取决于绑上去的实例是怎样的:

  • 每个线程独享一份new出来的实例 -> 线程安全
  • 多个线程共享一份“引用类型”实例 -> 线程不安全

ThreadLocal最大的用处就是用来把实例变量共享成全局变量,在程序的任何方法中都可以访问到该实例变量而已。网上很多人说ThreadLocal是解决了线程安全问题,大都是望文生义,二者完全非同类问题,读者需要有自己的思考呀。


关注A哥

AuthorA哥(YourBatman)
个人站点www.yourbatman.cn
E-mailyourbatman@qq.com
微 信fsx641385712
活跃平台
公众号BAT的乌托邦(ID:BAT-utopia)
知识星球BAT的乌托邦
每日文章推荐每日文章推荐

BAT的乌托邦

往期精选

  • [享学Jackson] 一、初识Jackson – 世界上最好的JSON库
  • [享学Jackson] 二、jackson-core之流式API与JsonFactory、JsonGenerator、JsonParser
  • [享学Jackson] 三、jackson-databind之ObjectMapper与数据绑定、树模型
  • [享学Jackson] 四、控制Jackson行为的特征们之JsonFactory.Feature、JsonGenerator.Feature、JsonParser.Feature
  • [享学Jackson] 五、控制Jackson行为的特征们之JsonWriteFeature、JsonReadFeature
  • [享学Jackson] 六、控制Jackson行为的特征们之MapperFeature、SerializationFeature、DeserializationFeature
  • [享学Jackson] 七、Jackson使用bit位运算来开启/禁用Feature的原理解析
  • [享学Jackson] 八、jackson-databind数据绑定基础配置之BaseSettings、MapperConfig、MapperConfigBase
  • [享学Jackson] 九、jackson-databind数据绑定序列化/反序列化配置之SerializationConfig、DeserializationConfig
  • [享学Jackson] 十、jackson-databind序列化之ObjectMapper序列化原理、序列化器匹配原理
  • [享学Jackson] 十一、jackson-databind之JsonSerializer序列化器全解析
  • [享学Jackson] 十二、jackson-databind反序列化之ObjectMapper反序列化原理、JsonDeserializer反序列化器全解析
  • [享学Jackson] 十三、jackson-annotation注解模块全解析及Jackson注解大全
  • [享学Jackson] 十四、深入理解Jackson的Module模块化设计及原理分析
  • [享学Jackson] 十五、第三方模块Module的深度实践:JavaTimeModule、JSR310Module、ParameterNamesModule、Jdk8Module
  • [享学Jackson] 十六、Jackson在Spring MVC中的使用之Date、JSR310时间类型的处理
  • [享学Jackson] 十七、spring-web整合Jackson源码解析之Jackson2ObjectMapperBuilder
  • [享学Jackson] 十八、Spring容器深度整合Jackson的桥梁之SpringHandlerInstantiator
  • [享学Jackson] 十九、Spring下使用ObjectMapper的正确姿势 — Jackson2ObjectMapperFactoryBean
  • [享学Jackson] 二十、Spring MVC下的Jackson — MappingJackson2HttpMessageConverter
  • [享学Jackson] 二十一、Spring Boot下的Jackson — JacksonAutoConfiguration自动配置
  • [享学Jackson] 二十二、Jackson与Fastjson的恩怨情仇(完结篇)

http://chatgpt.dhexx.cn/article/xduHSUiM.shtml

相关文章

Java线程安全详细总结

以下是我的PPT文档&#xff0c;不知道怎么复制到博客&#xff0c;只能一个一个插入图片发上来了。感觉总结的不错&#xff0c;分享一下。 文档地址&#xff1a;http://download.csdn.net/detail/csujiangyu/9526641

分布式系统详解--基础知识(线程)

分布式系统详解--基础知识&#xff08;线程&#xff09; 一、导读 前面跟大家讲了一下 分布式系统详解--基础知识&#xff08;概论&#xff09; &#xff0c;可以稍微了解一下大体上分布式是怎么一回事了。这片篇文章主要是讲述一下线程的问题分别介绍一下&#xff0c;什么线…

分布式项目线程安全问题(电商扣减库存的安全问题1)

电商减库存存在的安全问题 Override public void deductStock(Map<Long, Integer> skuMap) {for (Map.Entry<Long, Integer> entry : skuMap.entrySet()) {Long skuId entry.getKey();Integer num entry.getValue();// 查询skuSku sku getById(skuId);// 判断…

分布式项目中 如何保证线程安全问题?-------ZooKeeper

前沿&#xff1a; 上篇文章我们聊到了在解决分布式项目中线程安全问题&#xff0c;提到解决方案还有其他的&#xff0c;那么在此提出 基于 zookeeper 解决分布式项目中的线程安全问题 也是目前市面上比较流行的。做为一个高级开发工程师也是必须要学习的。 ZooKeeper是什么东…

分布式线程安全(redis、zookeeper、数据库)

https://blog.csdn.net/u010963948/article/details/79006572 Q:一个业务服务器&#xff0c;一个数据库&#xff0c;操作&#xff1a;查询用户当前余额&#xff0c;扣除当前余额的3%作为手续费 synchronized lock db lock Q&#xff1a;两个业务服务器&#xff0c;一个数据库&…

分布式集群中如何保证线程安全?

目录 分布式集群中的线程安全问题 解决方法 串行化 分布式锁 Redis如何实现呢&#xff1f; 问题&#xff1a;setnx刚好获取到锁&#xff0c;业务逻辑出现异常&#xff0c;导致锁无法释放 问题&#xff1a;可能会释放其他服务器的锁。 问题&#xff1a;删除操作缺乏原子…

java outlook 发送邮件_基于java使用JavaMail发送邮件

一、邮件的相关概念 邮件协议。主要包括&#xff1a; SMTP协议&#xff1a;Simple Mail Transfer Protocol&#xff0c;即简单邮件传输协议&#xff0c;用于发送电子邮件 POP3协议&#xff1a;Post Office Protocol 3&#xff0c;即邮局协议的第三个版本&#xff0c;用于接收邮…

java 发邮件(有正文,有图片,有附件)

一 需求: 1 java实现邮件发送 2 发送内容: ① 正文: 图片说明和图片 ② 附件一: 图片作为附件发送 ③ 附件二: Excel表格 二 思路: 1首先创建一个 Java 工程&#xff0c;把下载好的 javax.mail.jar 作为类库加入工程 2邮件创建步骤: 配置连接邮件服务器的参数( 邮件服务器SM…

java接收邮件_Java实现邮件收发

一. 准备工作 1. 传输协议 SMTP协议-->发送邮件: 我们通常把处理用户smtp请求(邮件发送请求)的服务器称之为SMTP服务器(邮件发送服务器) POP3协议-->接收邮件: 我们通常把处理用户pop3请求(邮件接收请求)的服务器称之为POP3服务器(邮件接收服务器) 2. 邮件收发原理 闪电…

java发送邮件工具类

1. 普通java实现邮件发送 1.1 创建maven项目&#xff0c;配置pom.xml文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance&qu…

java发送邮件带附件

一、 开启SMTP服务 1.基本都在邮箱设置里&#xff0c;开启后会获得神秘代码&#xff0c;后面有用。 2.记得添加依赖&#xff0c;或者自己添加jar包。 <dependency><groupId>javax.mail</groupId><artifactId>mail</artifactId><version>…

java 邮件模板

邮件发送代码可参照 java 发送邮件 1.情形 邮件发送代码可参照上述&#xff0c;本例只说明如果读取模板文件。公司定义模板较为复杂的情况&#xff0c;可采用此类发送方式 2. 模板 2.1 resource 建立模板 2.2 ftl 模板如下 <p>您好&#xff0c;${name}&#xff0c;您…

使用JAVA实现邮件发送功能

一、准备工作 小编今天以 QQ邮箱 进行演示操作。 想要使用代码操作邮箱发送邮件&#xff0c;需要在邮箱设置中申请开通 POP3/SMTP 服务。 接下来跟着小编的图文一步一步的操作开通吧&#xff01; 1.1 登录网页QQ邮箱&#xff0c;点击页面顶部设置按钮。 1.2 点击后会打开邮箱…

java发送qq邮件

1.登录qq邮箱 1&#xff09;点击设置 2&#xff09;点击账户 3&#xff09;开启第一个服务&#xff0c;我已经开过了 4&#xff09;开启验证&#xff08;让你发送指定内容到某个号码&#xff09;&#xff0c;完成后点击我已发送&#xff0c;就会出现授权码&#xff0c;授权码很…

java实现邮件发送

一.第一步:导入两个jar包。 activation.jar 和 mail.jar, 一定要添加到构建路径(不然找不到包) 两个用于Java发送邮件的jar包-Java文档类资源-CSDN下载 二、创建邮箱工具类:Mail.java import java.util.*; import java.io.*; import javax.mail.*; import javax.m…

Java(81):Java发邮件简单示例

Java Email jar包下载地址&#xff1a;JavaMail API https://www.oracle.com/java/technologies/javamail.html JavaMail 右侧下载&#xff0c;选择jar包下载 API文档参考&#xff1a;JavaMail API documentation https://javaee.github.io/javamail/docs/api/ 或直接引用…

java发送qq邮件_「java发邮件」Java 通过SMTP实现发送QQ邮件 - seo实验室

java发邮件 在Eclipse中创建项目&#xff0c;并把javax.amil.jar和commons-email-1.5,jar复制到项目中 链接&#xff1a;https://pan.baidu.com/s/1sQjA1GEpKi6IJJRGHKxjeA 密码&#xff1a;4ene 添加步骤&#xff1a; 1.首先在项目下创建一个文件夹&#xff0c;保存我们的jar包…

Java发邮件配置-hutool+腾讯企业邮箱

1、技术选型 1.1、hutool工具 1.2、javax.mail 1.3、腾讯企业邮箱2、环境准备 2.1、pom <!--javax.mail--><dependency><groupId>javax.mail</groupId><artifactId>mail</artifactId><version>1.4.7</version></dependen…

Java(83)Java发邮件简单工具类

1、Maven引用 <!-- https://mvnrepository.com/artifact/javax.mail/javax.mail-api --><dependency><groupId>javax.mail</groupId><artifactId>javax.mail-api</artifactId><version>1.6.2</version></dependency><…

java发邮件 动态切换当前发送人

最近项目需要实现一个发送邮件功能&#xff0c;踩了一些坑&#xff0c;最终实现了。 在此写一下心得 开始做的时候一塌糊涂&#xff0c;觉得挺难的&#xff0c;但是做完之后发现其实简单的一批&#xff0c;接下来我就来写一下实现流程。 1、准备好拿来发送邮件的账号&#xf…