文章目录
ThreadLocal
当在Java的多线程环境中需要确保多个线程对同一变量的安全访问时,可以使用
ThreadLocal
类型的对象。ThreadLocal
提供了一种线程封闭的机制,它能够为每个线程创建一个独立的变量副本,使得每个线程都可以独立地访问和修改自己的变量副本,而不会影响其他线程的副本。
1.ThreadLocal的基本使用
当使用 ThreadLocal
时,我们可以使用以下实例方法:
void set(T value)
- 将当前线程的变量副本设置为指定的值
value
。这个值将与当前线程关联,并且只有当前线程可以访问。
- 将当前线程的变量副本设置为指定的值
T get()
- 返回当前线程的变量副本的值。每个线程只能访问和获取自己的变量副本,而不会影响其他线程的副本。
void remove()
- 从当前线程的
ThreadLocal
实例中删除变量副本。这样可以释放当前线程占用的资源,并避免潜在的内存泄漏问题。
- 从当前线程的
protected T initialValue()
- 在首次访问
ThreadLocal
的get()
或set()
方法时,如果当前线程的变量副本尚未创建,则会调用此方法来初始化变量的初始值。默认实现返回null
。你可以通过继承ThreadLocal
并重写该方法来提供自定义的初始值。
- 在首次访问
void setInitialValue(T value)
- 设置所有线程的初始值。该方法将为所有线程创建一个共享的初始变量副本,而不是为每个线程创建独立的副本。这样,所有线程在访问变量时将共享同一个初始值。
下面我们通过一个简单的案例来了解一下 如何使用ThreadLocal
class ThreadLocalExample {
private static final Logger logger = LoggerFactory.getLogger(ThreadLocalExample.class);
// 这里我们在外层提供了一个共有的变量 myThreadLocal 当前类下的所有方法都可以进行访问
private static ThreadLocal<Integer> myThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 下面我们创建两个线程,来对共有的ThreadLocal进行修改,观察结果
// 线程1设置变量值
Thread thread1 = new Thread(() -> {
logger.error("Thread 1 - Before: {}", myThreadLocal.get());
myThreadLocal.set(10);
logger.error("Thread 1: {}", myThreadLocal.get());
});
// 线程2设置变量值
Thread thread2 = new Thread(() -> {
logger.error("Thread 2 - Before: {}", myThreadLocal.get());
myThreadLocal.set(20);
logger.error("Thread 2: {}", myThreadLocal.get());
});
thread1.start();
thread2.start();
// 最终来查看线程1和线程2的值是否会相互影响
logger.error("Main Thread: {}", myThreadLocal.get());
}
}
从结果中可以看出,每个线程本地变量 都绑定了一个独立的值,每次操作都是在自己的值上进行的
1.2.ThreadLocal.withInitial()
如果尚未 设定值,那么在访问的时候 是返回了一个null值的
如果我们希望,在创建的时就给予ThreadLocal
一个默认的值 可以使用ThreadLocal
提供的一个默认方法
ThreadLocal.withInitial()
是ThreadLocal
类提供的一个静态方法,它允许我们使用提供的初始化函数来设置变量副本的初始值。该方法返回一个新的ThreadLocal
对象,并在首次访问ThreadLocal
的get()
或set()
方法时,调用提供的初始化函数来获取初始值。
class ThreadLocalExample {
private static final Logger logger = LoggerFactory.getLogger(ThreadLocalExample.class);
private static ThreadLocal<Integer> defaultThreadLocal = ThreadLocal.withInitial(() -> 110);
public static void main(String[] args) {
logger.error("defaultThreadLocal.get() = {}", defaultThreadLocal.get());
}
}
2.ThreadLocal的使用场景
ThreadLocal
解决线程安全问题是一个比较好的方案,它通过每个线程提供一个独立的本地值,去解决并发访问的冲突问题,在很多情况下,ThreadLocal
会比使用线程同步机制synchronized
更加简单,方便,可以让程序有更好的并发性能ThreadLocal的使用可以大致分为以下两种类型:
- 线程隔离(Thread Isolation):
ThreadLocal
可以用于实现线程隔离,即为每个线程提供独立的变量副本,使得每个线程都可以独立地访问和修改自己的副本,而不会影响其他线程。这种线程隔离的特性在以下情况下非常有用:
- 数据库连接管理:在多线程环境中,每个线程需要独立的数据库连接,可以使用
ThreadLocal
来管理每个线程的连接副本,避免线程之间的混淆和资源竞争。- 用户身份信息:在 Web 应用程序中,每个请求的用户身份信息可以存储在
ThreadLocal
中,以便在整个请求处理过程中方便访问,而不必在每个方法参数中显式传递。- 事务管理:在多线程的事务环境中,可以使用
ThreadLocal
存储和传递事务上下文,确保每个线程都能独立地管理自己的事务状态。- 跨函数传递数据(Data Passing across Functions):
ThreadLocal
也可以用于在函数调用链中跨函数传递数据,避免了在每个函数参数中显式传递数据的麻烦。这种跨函数传递数据的特性在以下情况下很有用:
- 计时器和日志跟踪:在函数调用链中,可以使用
ThreadLocal
存储计时器或日志跟踪的上下文信息,通过在每个函数中更新和传递上下文,可以方便地记录函数的执行时间或跟踪日志。- 上下文数据传递:在复杂的业务逻辑中,可能需要在多个函数中传递同一份上下文数据,例如请求参数、配置信息等。使用
ThreadLocal
可以将上下文数据存储在ThreadLocal
中,在整个函数调用链中共享和访问。
下面我会通过两个不同的案例,来介绍ThreadLocal
2.1.线程隔离
下面我们通过线程池来,模拟一下ThreadLocal中,线程隔离的场景
/**
* ThreadLocal在线程池中传递
* @throws InterruptedException
*/
@Test
@DisplayName("测试threadLocal在线程池中传递")
public void test2() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 放入threadLocal的值
threadLocal.set("看看有没有被修改!");
executorService.submit(() -> {
logger.error("获取值为:{}", threadLocal.get());
threadLocal.set("hello,my-thread-local-1");
logger.error("当前值为:{}", threadLocal.get());
});
executorService.submit(() -> {
logger.error("获取值为:{}", threadLocal.get());
threadLocal.set("hello,my-thread-local-2");
logger.error("当前值为:{}", threadLocal.get());
});
Thread.sleep(TimeUnit.SECONDS.toMillis(1));
executorService.submit(() -> {
logger.error("最终前值为:{}", threadLocal.get());
});
executorService.submit(() -> {
logger.error("最终前值为:{}", threadLocal.get());
});
// 这里两个线程,首先原生的ThreadLocal 每个线程的TreadLocal都是单独占有一份的, 所以 pool-2-thread-1,pool-2-thread-2两个线程是不能获取到 main线程的ThreadLocal
// 注意 每个线程的 ThreadLocal都是【独立的】,【主线程】中的【子线程】无法获取主线程threadLocal
}
在这个例子中,我通过Executros静态方法创建了只有两个线程的固定线程池,模拟在线程池中传递一个 ThreadLocal 变量。
然后,我们使用 threadLocal.set()
方法将值放入 ThreadLocal 变量中
接下来,向线程池中提交两个任务
在每个任务中,通过 threadLocal.get()
方法获取 ThreadLocal 变量的值,并使用 threadLocal.set()
方法修改变量的值。
需要注意的是,由于线程池中的线程是复用的,所以可能会出现线程在不同任务间切换的情况。因此,每个任务都可以独立地访问和修改自己的 ThreadLocal 变量副本,而不会影响其他任务。
最后,我们可以观察到最终的两个任务中,获取到的 ThreadLocal 变量的值仍然是各自任务设置的最新值。
需要注意的是,由于线程池的线程是有限的,如果线程池中的线程数量小于任务数量,那么部分任务可能会等待可用的线程。在等待期间,线程池中的某个线程可能会被重新分配给其他任务,从而导致 ThreadLocal 变量副本的切换。因此,在使用 ThreadLocal 和线程池结合时,需要注意线程切换可能带来的副作用。
总结:通过在线程池中使用 ThreadLocal,我们可以实现线程隔离,确保每个线程都能独立地访问和修改自己的 ThreadLocal 变量副本,而不会影响其他线程。这在需要在线程池中传递数据,并保持数据隔离的场景中非常有用。
2.2.跨函数传递ThreadLocal
当在企业开发中进行跨函数传递时,ThreadLocal 应用场景非常多,下面是一些常见场景:
- 认证和授权信息传递:在一个请求处理过程中,用户的认证和授权信息可能需要在多个函数或方法中共享。使用 ThreadLocal 可以方便地传递这些信息,如用户身份、权限等。
- 跨层数据传递:在多层架构中,比如前端控制器、服务层、持久化层等,可能需要在不同层之间传递一些公共的数据,如请求信息、上下文数据等。
- 多租户数据隔离:在多租户系统中,不同租户的数据需要进行隔离。通过 ThreadLocal 可以在不同函数或方法中传递租户标识,以确保数据隔离和正确处理。
- 全局上下文信息传递:有些全局的上下文信息,如请求ID、语言设置、时间区域等,可能需要在整个应用中共享。ThreadLocal 可以方便地将这些上下文信息传递给需要使用它们的函数或方法。
- 事务上下文传递:在使用事务管理时,事务上下文信息可能需要在多个函数或方法中传递。通过 ThreadLocal 可以将事务上下文与当前线程绑定,实现跨函数的事务传递。
- 日志追踪:在分布式系统中,日志追踪是常见的需求。通过在 ThreadLocal 中存储唯一的请求
下面我们通过一个 模拟传递Seesion的案例,来了解一下通过ThreadLocal跨函数传递数据
/**
* 模拟ThreadLocal跨函数传递数据
* 例如 我们一个方法接收从Http请求中获取的Session对象,然后在后续的方法中使用这个Session对象,这个时候就可以使用ThreadLocal
* 自定义SessionHolder
*/
class SessionHolder {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
/**
* 存放Session的ThreadLocal实例
*/
private static final ThreadLocal<Session> sessionThreadLocal = new ThreadLocal<>();
/**
* 这里我模拟从Http请求中获取Session对象
* @param session Session对象
*/
public static void setSession(Session session) {
// 这里我们将Session对象放入ThreadLocal中
// 随便对session的属性进行设置
session.setStoreDir(new File("/tmp"));
session.setTimeout(Duration.ofDays(30 * 60));
session.setPersistent(true);
sessionThreadLocal.set(session);
}
/**
* 获取Session对象
* @return Session对象
*/
public static Session getSession() {
return sessionThreadLocal.get();
}
/**
* 移除ThreadLocal中的Session对象d
*/
public static void removeSession() {
sessionThreadLocal.remove();
}
@Test
@DisplayName("模拟发送Http请求,获取Session对象")
public void test() {
Session session = new Session();
// 设置Session对象
SessionHolder.setSession(session);
// 在后续的方法中使用Session对象
Session sessionInfo = SessionHolder.getSession();
// 这里我们可以使用session1对象
logger.error("获取的Session的StoreDir属性为:{}", sessionInfo.getStoreDir());
// 这里我们使用完Session对象后,需要移除
logger.error("移除Session对象!");
SessionHolder.removeSession();
// 再次获取
Session sessionInfo2 = SessionHolder.getSession();
logger.error("获取的Session的Timeout属性为:{}", sessionInfo2);
}
}
当需要在不同方法中共享数据时,可以使用 ThreadLocal 来实现,避免重复的代码并保持逻辑清晰。
- 创建一个静态的 ThreadLocal 实例
sessionThreadLocal
,用于存放 Session 对象。 - 在需要共享 Session 对象的方法中,使用
setSession()
方法将 Session 对象存储到 ThreadLocal 中。 - 在其他方法中,使用
getSession()
方法获取存储在 ThreadLocal 中的 Session 对象。 - 在合适的时机,调用
removeSession()
方法从 ThreadLocal 中移除 Session 对象。 - 通过 ThreadLocal 实现了数据在不同方法间的传递和共享,避免了重复的代码,保持了逻辑的清晰性。
问题?上述都在单线程环境下运行的,相当于同步的一个操作,那么如果在多线程并发的环境下运行的呢?还是我们想要的结果吗??下面我们通过一个案例来观察一下。
3.多线程(异步)下的ThreadLocal
/**
* 多线程环境下的ThreadLocal
* @throws InterruptedException
*/
@Test
@DisplayName("测试threadLocal在线程池中传递")
public void test2() throws InterruptedException {
ExecutorService executorService = Executors