ThreadLocal的应用及原理

news/2025/1/12 15:56:19/

一、ThreadLocal 定义

  官方JDK的定义:此类提供线程局部变量。这些变量与其正常对应变量的不同之处在于,每个访问一个(通过其get或set方法)的线程都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,这些字段希望将状态与线程(例如,用户ID或事务ID)相关联。
例如,下面的类生成每个线程本地的唯一标识符。线程的id在第一次调用ThreadId.get()时被分配,并且在随后的调用中保持不变。

ThreadLocal是用来存放线程相关数据的一个容器,这个容器叫做ThreadLocalMap,它是ThreadLocal的一个静态内部类,同时作为Thread类的一个成员变量。ThreadLocal在使用时,先拿到当前线程的成员变量ThreadLocalMap,以当前的ThreadLocal对象作为key,变量作为value存入ThreadLocalMapThreadLocal相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal关联的实例互不干扰。然后每个线程取变量都是从线程各自的ThreadLocalMap中取值,自然是线程安全的了。因为变量只在自己线程的生命周期内起作用,所以说ThreadLocal提供线程局部变量,或者叫线程本地变量。

ThreadLocal实例通常总是以静态字段初始化如下:

static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();

 二、ThreadLocal的使用

ThreadLocal 的常用方法:

  1. public ThreadLocal():通过构造器创建对象。一般是静态的。
  2. static <S> ThreadLocal<S>withInitial(Supplier<? extends S> supplier):初始化一个 ThreadLcoal。
  3. void set(T value):设置当前线程绑定的局部变量。
  4. T get():返回此线程局部变量的当前线程副本中的值。
  5. void remove():删除当前线程绑定的局部变量。

对于多任务,Java标准库提供的线程池可以方便地执行这些任务,同时复用线程。Web应用程序就是典型的多任务应用,每个用户请求页面时,我们都会创建一个任务,类似:

public void process(User user) {checkPermission();doWork();saveStatus();sendResponse();
}

process()方法需要传递的状态就是User实例。

这种在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等。

给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,User对象就传不进去了。

Java标准库提供了一个特殊的ThreadLocal,它可以在一个线程中传递同一个对象。

ThreadLocal实例通常总是以静态字段初始化如下:

static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();

它的典型使用方式如下:

void processUser(user) {try {threadLocalUser.set(user);step1();step2();} finally {threadLocalUser.remove();}
}

通过设置一个User实例关联到ThreadLocal中,在移除之前,所有方法都可以随时获取到该User实例:

void step1() {User u = threadLocalUser.get();log();printUser();
}void log() {User u = threadLocalUser.get();println(u.name);
}void step2() {User u = threadLocalUser.get();checkUser(u.id);
}

注意到普通的方法调用一定是同一个线程执行的,所以,step1()step2()以及log()方法内,threadLocalUser.get()获取的User对象是同一个实例。

实际上,可以把ThreadLocal看成一个全局Map<Thread, Object>:每个线程获取ThreadLocal变量时,总是使用Thread自身作为key:

Object threadLocalValue = threadLocalMap.get(Thread.currentThread());

因此,ThreadLocal相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal关联的实例互不干扰。

最后,特别注意ThreadLocal一定要在finally中清除:

try {threadLocalUser.set(user);...
} finally {threadLocalUser.remove();
}

这是因为当前线程执行完相关代码后,很可能会被重新放入线程池中,如果ThreadLocal没有被清除,该线程执行其他代码时,会把上一次的状态带进去。

为了保证能释放ThreadLocal关联的实例,我们可以通过AutoCloseable接口配合try (resource) {...}结构,让编译器自动为我们关闭。例如,一个保存了当前用户名的ThreadLocal可以封装为一个UserContext对象:

public class UserContext implements AutoCloseable {static final ThreadLocal<String> ctx = new ThreadLocal<>();public UserContext(String user) {ctx.set(user);}public static String currentUser() {return ctx.get();}@Overridepublic void close() {ctx.remove();}
}

 二、ThreadLocal原理解析

ThreadLocal 的原理要从它的set(T value)get()、remove()方法的源码入手:

1、set()方法

public void set(T value) {//获取当前线程Thread t = Thread.currentThread();//获取维护当前线程变量的ThreadLocalMap数据,一种类似于HashMap的数据结构ThreadLocalMap map = getMap(t);//如果当前线程已经存在了Map,直接调用map.setif (map != null)map.set(this, value);//不存在Map,则先进行新增map,再进行setelsecreateMap(t, value);
}

set方法中出现了一个ThreadLocalMap这个数据结构,点进去看一下

static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}//代码太多不一一贴出来了
}

其中维护了一个entry结构用来用来维护节点的数据,细心地同学应该已经发现了Entry这个结构继承了WeakReference,从构造方法可以看出,ThreadLocalMap的Key是软引用维护的。这个地方很重要,至于为什么重要,后面再细说。

再继续点击一下发现ThreadLocal成员变量里面定义了这么一句话

ThreadLocal.ThreadLocalMap threadLocals = null;

这句话的出现表明了,针对于每一个线程,都是独立维护一个ThreadLocalMap,一个线程也可以拥有多个ThreadLocal变量。

2、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();
}
private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;
}
protected T initialValue() {return null;
}

get()方法整体上比较简单,贴上了关键逻辑逻辑代码,调用get()时,如果存在值,则将值返回,不存在值调用setInitialValue()获取值,其中初始化的值为null,也就是说如果ThreadLocal变量未被赋值,或者赋值后被remove掉了,直接调用get()方法不会报错,将会返回null值。

3、remove()方法

//ThreadLocal
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);
}
//ThreadLocalMap
private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode &amp; (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {e.clear();expungeStaleEntry(i);return;}}
}

remove方法调用时会判断当前线程中ThreadLocalMap是否存在,如果存在则调用ThreadLocalMap.remove(key);遍历链表结构移除entry节点

4、ThreadLocal 如何存多个变量

ThreadLocal 使用set方法存数据时,key 用的this对象,就是当前正在使用的 ThreadLocal 对象,说明一个 ThreadLocal 对象,在一个线程中,只能存一个线程本地变量。多个线程虽然都是用的是一个 key,但是不同的线程用的是不同的ThreadLocalMap

具体做法有两种:

1、 生成ThreadLocal 对象,每个 ThreadLocal 对象对应一个业务变量

2、给 ThreadLocal 初始化一个HashMap,这是最常规的做法。比如下面:

public class ThreadLocalTest {private static final ThreadLocal<Map<String, Object>> context =ThreadLocal.withInitial(HashMap::new);private String getUserId() {return String.valueOf(context.get().get("userId"));}private void setUserId(String userId) {context.get().put("userId", userId);}public void setUserName(String userName) {context.get().put("userName", userName);}public String getUserName() {return String.valueOf(context.get().get("userName"));}public static void main(String[] args) {ThreadLocalTest test = new ThreadLocalTest();for (int i = 1; i < 5; i++) {Thread thread = new Thread(() -> {String threadName = Thread.currentThread().getName();test.setUserId(threadName + "的userId");test.setUserName(threadName + "的userName");System.out.println("===执行业务代码===");System.out.println(threadName + "-->" + test.getUserId() + "," + test.getUserName());});thread.setName("线程" + i);thread.start();}}
}

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

相关文章

Hadoop问题拾零

hadoop的文件系统叫做hdfs&#xff0c;就是hadoop分布式分布式文件系统的中文简写。这个系统是对google的gfs的开源实现。下面来回答问题。首先是节点故障&#xff1a; google在他们那篇gfs的论文中说&#xff0c;google在使用gfs曾说过&#xff0c;google在使用gfs时遇到过各种…

nodejs内存溢出;‘node --max-old-space-size=10240’不是内部或外部命令,也不是可运行的程序;

运行报错&#xff1a; Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory 第一步 全局安装 increase-memory-limit npm install -g increase-memory-limit 第二步 在项目中执行 increase-memory-limit 尝试运行npm run dev/…

Linux网络编程之recv函数

功能 recv 函数的功能就是从套接字中接收数据。 头文件 #include <sys/types.h> #include <sys/socket.h>原型 ssize_t recv(int sockfd, void *buf, size_t len, int flags);参数 参数描述sockfdsocket 文件描述符buf接收数据缓冲区len接收数据缓冲区的大小f…

AbstractStringBuilder源码

介绍 AbstractStringBuilder这个抽象类是StringBuilder和StringBuffer的直接父类&#xff0c;而且定义了很多方法&#xff0c;因此在学习这两个类之前建议先学习 AbstractStringBuilder抽象类 该类在源码中注释是以JDK1.5开始作为前两个类的父类存在的 abstract class Abstr…

性能优化之影响分析

页面性能的影响 性能非常重要&#xff0c;而具体反映到我们的业务场景中&#xff0c;可能会有如下影响&#xff1a; 不利于用户留存 站点页面的展现速度非常影响用户体验&#xff0c;很多用户会因等待的不耐而放弃站点。研究表明&#xff0c;47 % 的消费者希望页面能够在 2s …

【深度学习】Yolov8追踪从0到1, 这要是做计数啥的,简单的一批

文章目录 前言在这里插入图片描述 ![在这里插入图片描述](https://img-blog.csdnimg.cn/4af4d64555984cd182fd9bde1433788d.png)1.任务追踪1.1 搭建环境 2.跨摄像头追踪(进阶)总结 前言 用了将近2年的yolov5了&#xff0c;之前主要做目标检测后面&#xff0c;还做了yolov5的分割…

【Linux0.11代码分析】10 之 ELF可执行程序03 - Program Headers解析

【Linux0.11代码分析】10 之 ELF可执行程序03 - Program Headers解析 一、ELF概述二、ELF的组成结构2.1 ELF header&#xff1a;解析出 section headers 含31个section节和 program headers 含13个segment段2.2 Section Headers&#xff1a;获取当前程序的31个section节区信息2…

python发送email

通过第三方SMTP发送纯文本邮件 #!/usr/bin/python -- coding: UTF-8 -- import smtplib from email.mime.text import MIMEText from email.header import Header 第三方 SMTP 服务 mail_host“smtp.XXX.com” #设置服务器 mail_user“XXXX” #用户名 mail_pass“XXXXXX” …