文章目录
ThreadLocal介绍
ThreadLocal,线程本地变量,ThreadLocal 的作用是为每个线程保存一份局部变量的引用,实现多线程之间的数据隔离,从而避免了线程不安全情况的发生。这个变量保存的值只在线程的生命周期内起作用,通过使用它减少了将执行上下文信息传递到每个方法的需要。
如果多个线程同时在一个对象/实例上执行,它们将共享这个实例变量,如果不使用ThreadLocal,就需要在每个方法上传递参数,去跨对象共享这些变量,同时还会导致线程不安全的问题。
许多框架使用 ThreadLocals 来维护与当前线程相关的一些上下文。例如,当前事务存储在 ThreadLocal 中时,您不需要通过每个方法调用将其作为参数传递,以防堆栈中的某个人需要访问它。Web 应用程序可能会将有关当前请求和会话的信息存储在 ThreadLocal 中,以便应用程序可以轻松访问它们。
ThreadLocal 原理
ThreadLocals 是一种全局变量(尽管由于它们仅限于一个线程而稍微不那么邪恶),因此在使用它们时应该小心以避免不必要的副作用和内存泄漏。
每个Thread对象,专门用一个ThreadLocalMap来存储自己的私有对象。ThreadLocalMap实际上就跟我们常用的HashMap类似,存储在那里的Key-Value形式的数据。
ThreadLocal在每次获取或设置操作时,都先通过Thread.currentThread()方法来获取当前线程,再从当前线程中获取ThreadLocalMap。而实际上,保存的值是通过ThreadLocalMap来存储的。
ThreadLocal对象可以是多线程共享,但ThreadLocalMap对象却是一个线程独享的,每个线程对象,创建一个自己专属的ThreadLocalMap,与其他Thread对象创建的ThreadLocalMap不存在一个单一的关系。
当多个Thread对象共同访问同一个ThreadLocal对象时,threadLocal只是作为ThreadLocalMap的Key存在,而不是作为变量的存储位置。threadLocal的set(方法和get()方法涉及的值是存储为ThreadLocalMap的值而ThreadLocalMap是每个线程专属的,互不相同的。这就是为什么同ThreadLocal被多线程同时访问,ThreadLocal的值却互不干扰的原理。
ThreadLocalMap
ThreadLocalMap该类的核心部分是Entry class,它扩展了WeakReference. 它确保如果当前线程退出,它将被自动垃圾收集。这就是为什么它使用ThreadLocalMap而不是简单的HashMap. 它将当前ThreadLocal及其值作为Entry类的参数传递,所以当我们想要获取值时,我们可以从 中获取它table.
- 每个线程中都有一个自己的 ThreadLocalMap 类对象,可以将线程自己的对象保持到其中, 各管各的,线程可以正确的访问到自己的对象。
- 将一个共用的 ThreadLocal 静态实例作为 key,将不同对象的引用保存到不同线程的 ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取 得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
- ThreadLocalMap其实就是线程里面的一个属性,它在Thread类中定义
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal使用场景
代替参数的显式传递
当我们在写API接口的时候,通常Controller层会接受来自前端的入参,当这个接口功能比较复杂的时候,可能我们调用的Service层内部还调用了 很多其他的很多方法,通常情况下,我们会在每个调用的方法上加上需要传递的参数。
但是如果我们将参数存入ThreadLocal中,那么就不用显式的传递参数了,而是只需要ThreadLocal中获取即可。
全局存储用户信息
在现在的系统设计中,前后端分离已基本成为常态,分离之后如何获取用户信息就成了一件麻烦事,通常在用户登录后, 用户信息会保存在Session或者Token中。这个时候,我们如果使用常规的手段去获取用户信息会很费劲,拿Session来说,我们要在接口参数中加上HttpServletRequest对象,然后调用 getSession方法,且每一个需要用户信息的接口都要加上这个参数,才能获取Session,这样实现就很麻烦了。
当请求到来时,可以将当前Session信息存储在ThreadLocal中,在请求处理过程中可以随时使用Session信息,每个请求之间的Session信息互不影响。当请求处理完成后通过remove方法将当前Session信息清除即可。
解决线程安全问题
在Spring的Web项目中,我们通常会将业务分为Controller层,Service层,Dao层, 我们都知道@Autowired注解默认使用单例模式,那么不同请求线程进来之后,由于Dao层使用单例,那么负责数据库连接的Connection也只有一个, 如果每个请求线程都去连接数据库,那么就会造成线程不安全的问题,Spring是如何解决这个问题的呢?
在Spring项目中Dao层中装配的Connection肯定是线程安全的,其解决方案就是采用ThreadLocal方法,当每个请求线程使用Connection的时候, 都会从ThreadLocal获取一次,如果为null,说明没有进行过数据库连接,连接后存入ThreadLocal中,如此一来,每一个请求线程都保存有一份 自己的Connection。于是便解决了线程安全问题
ThreadLocal源码
以下是ThreadLocal的get()、set()、remove()方法的代码
/**
* 返回当前线程的 this 副本中的值
* 线程局部变量。如果变量没有值
* 当前线程,首先初始化为返回值
* 通过调用 {@link #initialValue} 方法。
*
* @return 这个线程本地的当前线程的值
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
/**
* 设置这个线程局部变量的当前线程的副本
* 到指定值。大多数子类将不需要
* 重写此方法,仅依赖于 {@link #initialValue}
* 设置线程局部变量值的方法。
*
* @param value 要存储在当前线程的副本中的值。
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
/**
* 删除此线程本地的当前线程的值
* 多变的。如果此线程局部变量随后
* {@linkplain #get read} 被当前线程读取,其值为
* 通过调用其 {@link #initialValue} 方法重新初始化,
* 除非它的值是当前线程的 {@linkplain #set set}
* 在过渡期。这可能会导致多次调用
* 当前线程中的 <tt>initialValue</tt> 方法。
*
* @自 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocal内存溢出问题
内存溢出问题模拟
在执行main方法前,先使用“-Xmx50m”的参数来配置一下 Idea,它表示将程序运行的最大内存设置为 50m,如果程序的运行超过这个值就会出现内存溢出的问题
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadLocalOOMExample {
/**
* 定义一个 10m 大的类
*/
static class MyTask {
// 创建一个 10m 的数组(单位转换是 1M -> 1024KB -> 1024*1024B)
private byte[] bytes = new byte[10 * 1024 * 1024];
}
// 定义 ThreadLocal
private static ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>();
// 主测试代码
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(5, 5, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
// 执行 10 次调用
for (int i = 0; i < 10; i++) {
// 执行任务
executeTask(threadPoolExecutor);
Thread.sleep(1000);
}
}
/**
* 线程池执行任务
* @param threadPoolExecutor 线程池
*/
private static void executeTask(ThreadPoolExecutor threadPoolExecutor) {
// 执行任务
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("创建对象");
// 创建对象(10M)
MyTask myTask = new MyTask();
// 存储 ThreadLocal
taskThreadLocal.set(myTask);
// 将对象设置为 null,表示此对象不在使用了
myTask = null;
}
});
}
}
原因分析
由于每个线程 Thread 都拥有一个数据存储容器 ThreadLocalMap,当执行 ThreadLocal.set 方法执行时,会将要存储的值放到 ThreadLocalMap 容器中。而ThreadMap 中有一个 Entry[] 数组用来存储所有的数据,而 Entry 是一个包含 key 和 value 的键值对,其中 key 为 ThreadLocal 本身,而 value 则是要存储在 ThreadLocal 中的值。
也就是说它们之间的引用关系是这样的:Thread -> ThreadLocalMap -> Entry -> Key,Value,因此当我们使用线程池来存储对象时,因为线程池有很长的生命周期,所以线程池会一直持有 value 值,那么垃圾回收器就无法回收 value,所以就会导致内存一直被占用,从而导致内存溢出问题的发生。
解决方案
严格来讲内存溢出并不是 ThreadLocal 的问题,而是因为没有正确使用 ThreadLocal 所带来的问题。想要避免 ThreadLocal 内存溢出的问题,只需要在使用完 ThreadLocal 后调用 remove 方法即可。
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class App {
/**
* 定义一个 10m 大的类
*/
static class MyTask {
// 创建一个 10m 的数组(单位转换是 1M -> 1024KB -> 1024*1024B)
private byte[] bytes = new byte[10 * 1024 * 1024];
}
// 定义 ThreadLocal
private static ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>();
// 测试代码
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(5, 5, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
// 执行 n 次调用
for (int i = 0; i < 10; i++) {
// 执行任务
executeTask(threadPoolExecutor);
Thread.sleep(1000);
}
}
/**
* 线程池执行任务
* @param threadPoolExecutor 线程池
*/
private static void executeTask(ThreadPoolExecutor threadPoolExecutor) {
// 执行任务
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("创建对象");
try {
// 创建对象(10M)
MyTask myTask = new MyTask();
// 存储 ThreadLocal
taskThreadLocal.set(myTask);
// 其他业务代码...
} finally {
// 释放内存
taskThreadLocal.remove();
}
}
});
}
}