今天我们要学习一种在JAVA线程中至关重要的类——ThreadLocal。
ThreadLocal是一个强大的JAVA类,它能实现线程局部变量的功能。通过ThreadLocal,每一个线程都可以拥有自己的一份变量副本,互相之间不会影响操作,真正做到数据隔离。尤其在多线程环境下,避免了线程间混乱的数据共享、避免出现竞争条件,使得编程更加优雅。
ThreadLocal通常用来解决线程安全性问题,比如在Web应用中,你可以把数据库连接、用户身份信息等放在ThreadLocal里,确保每个线程都能独自访问这些信息,互不干涉。除此之外,ThreadLocal还常用于实现线程封闭(Thread Confinement)的模式,将数据完全限制在单个线程内,极大地简化了并发编程的难度。
我个人工作经历里,确实用得比较少,可能因为我的工作与之关系不大。但认识这个类对于你的工作肯定大有裨益。我们都知道,当JAVA方法需要传参数时,如两个方法间进行交互,传出值是另一个方法的输入值,随着方法越来越多,我们就容易遇到问题。此时如果利用ThreadLocal,每个线程可以独立保存一个值,非常方便。
下面让我们不再空谈理论,直接开始实际操作吧。让我们先学习一下如何使用这个类。
这里我们不再特意新建一个项目来做这件事情,可以借鉴。我之前关于servlet的文章:Java技术中的经典之作:Servlet的实践运用,我们就在这个项目上做一下二次创作吧。
构建它
首先我们需要构造这样一个类
package com.masiyi.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author masiyi
* @Date 2023/12/18
* @PackageName:com.masiyi.servlet
* @ClassName: MyTheadLocal
* @Description: TODO
* @Version 1.0
*/
@WebServlet(urlPatterns = "/myThreadLocal")
public class MyThreadLocal extends HttpServlet {
ThreadLocal threadLocal = new ThreadLocal<String>();
/**
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
threadLocal.set("你好");
method1();
}
private void method1() {
System.out.println(threadLocal.get());
method2();
}
private void method2() {
// threadLocal.remove();
System.out.println(threadLocal.get());
}
/**
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println(threadLocal.get());
super.doPost(req, resp);
}
}
启动后,我们用到接口调用工具,比如apifox,然后我们去调用这个接口。
学习它
跑到这个方法里面以后,我们可以看到我们是用成员属性方法创建了一个成员变量,这个时候我们给这个成员变量设置一个值。这里需要了解的是,每个HTTP请求就是一条单独的线程。在这个线程中,无论我们怎么调方法,它都会在同一个线程中执行,除非我们新建了一个异步线程来完成其他操作。所以,我们可以看到我们现在所在线程的名字是"http-nio-8080-exec-1"@2,977。
上面提到过,这个成员变量只有当前线程可以访问。也就是说,在这个线程中设置了一个值后,其他线程无法获取这个变量,只能由当前线程访问。就像我们现在正在使用的method1方法,我们直接从get方法中获取它设定的值,结果显示的是"你好"。
但是,现在如果我们调用这个servlet的post方法,我们将发现它与我们刚刚的线程不同。
那么,自然地,它直接获取的值就是null。
现在我们再次调用get方法,这时我们会发现它的线程已经改变了,这就是我说的道理。每个请求都会有一个独立的线程。因此,当我们直接调用它的get方法时,就得到了null。
接下来,我们再调用method1方法。会发现我们在doget方法中已经设定的值,这次可以顺利获取了。
最后,在这个方法中我们将其remove掉。remove意味着所有的值都会被清空。所以,我们看到的值就是null。
通过这个类,我们就可以实现了,方法的输入和输出参数不用明确标明它们的具体值,我们可以通过这个类来传递值,而且这个类的成员变量是每个线程专用的,每个线程都是独立的。
利用它
ThreadLocal在工作中可以用来解决线程安全和上下文传递的问题。举个例子,假设我们有一个需要在多个方法中传递用户身份信息的场景,可以使用ThreadLocal来存储用户身份信息,以便在整个线程中访问。
public class UserContext {
private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
public static void setUser(User user) {
userThreadLocal.set(user);
}
public static User getUser() {
return userThreadLocal.get();
}
public static void clear() {
userThreadLocal.remove();
}
}
public class UserService {
public void processUserRequest() {
User user = // 从请求中获取用户信息
UserContext.setUser(user);
// 执行业务逻辑
// 在其他方法中可以通过UserContext.getUser()获取用户信息
UserContext.clear();
}
}
在上面的例子中,UserContext类使用ThreadLocal存储用户信息,而UserService类中的processUserRequest方法可以在整个线程中访问和传递用户信息,而不需要显式地将用户信息作为参数传递给每个方法。这样可以简化代码,避免在方法间传递上下文信息的复杂性。
查看它
让我们仔细看看这三个方法,set gat remove
,它们各有什么特点,为什么能做到如此独特的操作?
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);
}
}
这是ThreadLocal类中的set方法,用于把特定的值塞进当前线程的ThreadLocalMap里。首先,它拿到了当前线程的引用,然后尝试从这个线程的ThreadLocalMap里捞出对应的map。如果映射存在,就把值藏进去;如果映射不存在,那就造一个新的出来,然后再把值藏进去。总之就是要保证每个线程都有自己专享的ThreadLocalMap,顺利实现了线程局部变量的存储和获取,避免了多线程环境下的数据共享和争抢问题。
具体来说,这段代码里最关键的操作就是通过ThreadLocalMap来实现线程局部变量的存储和访问,同时保证每个线程都能自由处理自己的变量副本。
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();
}
这是ThreadLocal类里的get方法,用于从当前线程的ThreadLocalMap里把保存的值取出来。首先,它拿到当前线程的引用,然后尝试从当前线程的ThreadLocalMap里把对应的map找到。如果映射存在,就尝试从map里把值找出来,如果找到了就直接返回;如果找不到,就调用setInitialValue方法来初始化一个值并回传。
这个方法的作用就是确保每个线程都能取到自己专享的那份ThreadLocalMap里保存的值。如果当前线程的ThreadLocalMap里有对应的值,那就直接取回来;如果没有,那就可以通过setInitialValue方法初始化一个值,然后继续取回来。
remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m!= null) {
m.remove(this);
}
}
remove方法主要用于从当前线程的ThreadLocalMap中把跟当前ThreadLocal对象相关联的值给清理掉。首先,它拿到当前线程的引用,然后尝试从当前线程的ThreadLocalMap里找到对应的map。如果map存在,就调用map的remove方法把跟ThreadLocal对象相关联的值给清理掉。
这个方法主要是为了保证在不再需要用ThreadLocal对象保存的值时,能将其从当前线程的ThreadLocalMap中清理掉,避免内存泄露或者无谓的资源占用。
如果你去深入研究源码,就会发现这里面主要涉及三个类,ThreadLocalMap、Entry和ThreadLocal。它们的关系大致如下:
- ThreadLocalMap是ThreadLocal类的一个子类,主要用来存储线程局部变量的值。每个线程都有自己专属的ThreadLocalMap实例,以此来存储该线程所需的局部变量值。
- Entry是ThreadLocalMap里面的一个内置类,主要用来表示ThreadLocalMap里的项。每个Entry实例都包括一个ThreadLocal对象和相应的值,用于描绘线程局部变量的键值对。
- ThreadLocal是一个线程局部变量,它提供了get、set和remove等方法,用于在当前线程里存取局部变量的值。每个ThreadLocal实例都跟当前线程的ThreadLocalMap里面的一个Entry相关联,以此来实现线程局部变量的存储和访问。
于是,ThreadLocalMap主要用来存储线程局部变量的值,Entry是用来描绘存储在ThreadLocalMap里面的键值对,而ThreadLocal就是线程局部变量的一种抽象表示,它跟ThreadLocalMap里面的Entry相绑定,使得线程局部变量的存储和访问得以实现。