Java中ThreadLocal简要介绍

news/2024/11/17 0:35:11/

1. 简介

在这篇文章中,我们将会介绍ThreadLocal在java.lang这个包中是如何被构造的。ThreadLocal提供了在当前线程中单独存储数据,并能将特殊类型的对象保存在ThreadLocal里面的能力。

2. ThreadLocal API

ThreadLocal构造函数允许存储一个只能在特定线程中才能获取的数据。
比如我们想让一个Integer类型的数据和一个特定线程绑定在一起:

ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();

当我们想在这个线程中使用这个数据时,可以调用set方法或者get方法,简单来说,ThreadLocal存储数据就是将线程作为map中的key。正是因为这个原因,我们可以在threadLocal中调用get方法获取Integer值。

threadLocalValue.set(1);
Integer result = threadLocalValue.get();

ThreadLocal可以使用withInitial()静态方法通过传递supplier方法的lambda方式来实现初始化,写法如下:

ThreadLocalthread threadLocal = ThreadLocal.withInitial(() -> 1);

我们可以调用remove方法来移除ThreadLocal中的值

threadLocal.remove()

为了充分的了解使用ThreadLocal,我们首先需要看一下不使用ThreadLocal的例子,然后我们将会利用ThreadLocal的构造函数来重写我们的例子。

3. 在一个Map中存储用户的数据

考虑一个特定的程序,需要存储给定userId用户的上下文数据:

public class Context {private String userName;public Context(String userName) {this.userName = userName;}
}

给每个userId都赋予一个线程,通过实现Runnable接口来创建一个SharedMapWithUserContext类,run函数的实现中,通过调用UserRepository类中方法,根据参数userId,返回一个Context的对象。
接着,我们将userId作为ConcurrentHashMap中的key存储上下文信息。

public class SharedMapWithUserContext implements Runnable {public static Map<Integer, Context> userContextPerUserId = new ConcurrentHashMap<>();private Integer userId;private UserRepository userRepository = new UserRepository();@Overridepublic void run() {String userName = userRepository.getUserNameForUserId(userId);userContextPerUserId.put(userId, new Context(userName));}
// standard constructor}

我们可以为两个不同userId创建两个不同的线程,通过调用线程中的start方法,并且使用断言判断userContextPerUserId这个map中有两个条目。

SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1);
SharedMapWithUserContext secondUser = new SharedMapWithUserContext(2);
new Thread(firstUser).start();
new Thread(secondUser).start();assertEquals(SharedMapWithUserContext.userContextPerUserId(), 2);

4. 在ThreadLocal中存储用户数据

我们可以使用ThreadLocal重写我们的例子,每个线程都会有他们自身的ThreadLocal实例。
当使用ThreadLocal时,需要十分小心,因为每个ThreadLocal实例需要和一个特定的线程关联起来,在我们的例子中,我们为每一个userId设置特定的线程,这个线程是由我们自己产生的,所以我们能完全控制此线程。
在run方法中将会获取每个user的上下文,并且将上下文使用set方法存储在ThreadLocal变量中。

public class ThreadLocalWithUserContext implements Runnable {private static ThreadLocal<Context> userContext = new ThreadLocal<>();private Integer userId;private UserRepository userRepository = new UserRepository();@Overridepublic void run() {String userName = userRepository.getUserNameForUserId(userId);userContext.set(new Context(userName));System.out.println("thread context for given userId: " + userId + " is: " + userContext.get());}
// standard constructor
}

通过启动两个线程来执行根据userId来获取上下文的动作。

ThreadLocalWithUserContext firstUser = new ThreadLocalWithUserContext(1);
ThreadLocalWithUserContext secondUser = new ThreadLocalWithUserContext(2);
new Thread(firstUser).start();
new Thread(secondUser).start();

通过运行上述的代码,我们可以看到在每个线程中的ThreadLocal的标准输出:

可以得出结论,每个user都有他们自己的上下文。

5. ThreadLocals和Thread池

ThreadLocal提供了一系列很容易使用的API来限制某个线程中的某些值,目的是为了保证java线程中的安全。
我们在同时使用ThreadLocals和线程池大的过程中需要十分的小心。
为了能够更好的理解可能存在的场景,考虑一下如下的场景:
1、首先,应用从线程池中拿到一个线程。
2、然后存储一些线程限制的值到当前线程的ThreadLocal。
3、一旦当前线程的执行过程结束,应用需要将取到的线程返回到线程池。
4、过了一会,应用获取到相同的线程并且执行另外的请求。
5、如果应用没有执行cleanup方法,那么应用就会在新的请求中使用相同的ThreadLocal数据。
这种场景在高并发场景中将会造成令人意外的结果。
为了避免出现这种问题,我们需要在不使用ThreadLocal的时候,移除掉ThreadLocal中缓存的内容,实践中需要严格的代码审视,才能排查出这类问题。

5.1 扩展ThreadPoolExecutor

可以通过扩展ThreadPoolExecutor类并且提供beforeExecute和afterExecute方法的实现,来解决上述的问题。线程池将在使用获得的线程运行任何方法前执行beforeExecute方法,在执行完我们的逻辑后,就会调用afterExecute方法。
因此,我们将会扩展ThreadPoolExecutor类,并且在afterExecute方法中清除ThreadLocal数据:

public class ThreadLocalAwareThreadPool extends ThreadPoolExecutor {@overrideprotected void afterExecute(Runnable r, Throwable t) {// Call remove on each ThreadLocal}
}

如果我们将请求提交到ExecutorService这个实现中,使用ThreadLocal和线程池不会给我们的应用程序带来任何安全隐患。

6. 结论

通过测试ThreadLocal构建函数,我们发现了ThreadLocal的特性,即一个线程绑定了一个ThreadLocal,ThreadLocal和线程池的组合使用存在安全隐患,并介绍了如何规避这种安全隐患。


http://www.ppmy.cn/news/796593.html

相关文章

分享一款超级好用的下载软件,IDM--不限速,破解版,IDM破解版下载安装

本片文章分为四个部分&#xff1a; 第一部分&#xff0c;下载安装IDM 第二部分&#xff0c;IDM破解 第三部分&#xff0c;浏览器插件的安装 第四部分&#xff0c;百度网盘使用IDM下载 下载IDM 首先大家可以在浏览器搜索IDM 网址&#xff1a;https://www.internetdownloadma…

解决 Package: vips-full-8.9.2-2.el7.remi.x86_64 (remi-safe) Requires: libheif.so.1()(64bit)

错误显示 Error: Package: vips-full-8.9.2-2.el7.remi.x86_64 (remi-safe) Requires: libheif.so.1()(64bit) You could try using --skip-broken to work around the problem You could try running: rpm -Va --nofiles --nodigest运行下面进行解决 wget https://download1…

Java 网络编程:(二)InetAddress 类_InetSocketAddress 类_URL 类

InetAddress 类_InetSocketAddress 类_URL 类 1.InetAddress 类2.InetAddress 类常用的方法3.InetSocketAddress 类4.InetSocketAddress 类常用的方法5.URL 类6.URL 类常用方法 1.InetAddress 类 封装计算机的 IP 地址,不包含端口号 2.InetAddress 类常用的方法 序号方法描述…

GD32F303RCT6PWM及SPWM配置

文章目录 前言一、原理介绍二、代码 1.GD32F303RCT6的PWM配置2.GD32F303RCT6的SPWM配置总结 前言 最近在使用GD32F303的芯片替换STM32F303&#xff0c;自己也在网上查阅了资料但关于具体实现的描述还是较少&#xff0c;希望下面的东西可以帮到大家。 一、原理介绍 在这里不做…

使用GD32USBFS复合设备数据发送时出现错误问题

问题现象 解决方法排查过程 最近使用GD32F4定义了一个USB复合设备&#xff0c;有三个接口Interface&#xff0c;第一个接口用于HID键盘&#xff0c;后两个用于自定义信息发送。设备插入电脑显示设备枚举成功&#xff0c;三个接口都在&#xff0c;在使用的时候发现前设备两个接口…

oxe通信服务器组件是什么,ALE OminiPCX OXE

1.1 OmniPCX Enterprise Stand-alone System● 用户线 (包括所有类型用户): 15,000 ● 中继线 (包括公网、专网): 10,000 ● 媒体网关/远端模块 (包括IP媒体网关和Crystal媒体网关): 240 ● 远端存活模块 (PCS): 240 ● 支持85个时区 ● VoIP应用: 支持H.323/SIP标准 ● 压缩算…

GD32利用CubeMX构建代码的测试

前言 近期搞到一块GD32F103c8t6的开发板&#xff0c;号称是和STM32F103C8T6 Pin To Pin兼容的&#xff0c;查了一些资料&#xff0c;很多老哥也搞过类似的测试&#xff0c;多半结果是不兼容&#xff0c;具体哪里不兼容&#xff0c;哪些共功能不兼容&#xff0c;老哥们基本不说…

linux php gd库安装,Linux系统gd库安装步骤说明

gd库是PHP处理图形的扩展库&#xff0c;它提供了一系列用来处理图片的API(应用程序编程接口)&#xff0c;使用gd库可以处理图片或者生成图片。在网站上&#xff0c;gd库通常用来生成缩略图&#xff0c;或者对图片加水印&#xff0c;或者生成汉字验证码&#xff0c;或者对网站数…