8.Java并发编程—ThreadLocal,TreadLocalMap,InheritableThreadLocal,TransmittableThreadLocal使用指南

ThreadLocal

当在Java的多线程环境中需要确保多个线程对同一变量的安全访问时,可以使用ThreadLocal类型的对象。ThreadLocal提供了一种线程封闭的机制,它能够为每个线程创建一个独立的变量副本,使得每个线程都可以独立地访问和修改自己的变量副本,而不会影响其他线程的副本。

1.ThreadLocal的基本使用

当使用 ThreadLocal 时,我们可以使用以下实例方法:

  1. void set(T value)
    • 将当前线程的变量副本设置为指定的值 value。这个值将与当前线程关联,并且只有当前线程可以访问。
  2. T get()
    • 返回当前线程的变量副本的值。每个线程只能访问和获取自己的变量副本,而不会影响其他线程的副本。
  3. void remove()
    • 从当前线程的 ThreadLocal 实例中删除变量副本。这样可以释放当前线程占用的资源,并避免潜在的内存泄漏问题。
  4. protected T initialValue()
    • 在首次访问 ThreadLocalget()set() 方法时,如果当前线程的变量副本尚未创建,则会调用此方法来初始化变量的初始值。默认实现返回 null。你可以通过继承 ThreadLocal 并重写该方法来提供自定义的初始值。
  5. 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());
        
	}
}

image-20240326200125246

从结果中可以看出,每个线程本地变量 都绑定了一个独立的值,每次操作都是在自己的值上进行的

1.2.ThreadLocal.withInitial()

如果尚未 设定值,那么在访问的时候 是返回了一个null值的
如果我们希望,在创建的时就给予ThreadLocal一个默认的值 可以使用 ThreadLocal提供的一个默认方法

ThreadLocal.withInitial()ThreadLocal 类提供的一个静态方法,它允许我们使用提供的初始化函数来设置变量副本的初始值。该方法返回一个新的 ThreadLocal 对象,并在首次访问 ThreadLocalget()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());

	}
}

image-20240326203015626

2.ThreadLocal的使用场景

ThreadLocal解决线程安全问题是一个比较好的方案,它通过每个线程提供一个独立的本地值,去解决并发访问的冲突问题,在很多情况下,ThreadLocal会比使用线程同步机制synchronized更加简单,方便,可以让程序有更好的并发性能

ThreadLocal的使用可以大致分为以下两种类型:

  1. 线程隔离(Thread Isolation):
    ThreadLocal 可以用于实现线程隔离,即为每个线程提供独立的变量副本,使得每个线程都可以独立地访问和修改自己的副本,而不会影响其他线程。这种线程隔离的特性在以下情况下非常有用:
    • 数据库连接管理:在多线程环境中,每个线程需要独立的数据库连接,可以使用 ThreadLocal 来管理每个线程的连接副本,避免线程之间的混淆和资源竞争。
    • 用户身份信息:在 Web 应用程序中,每个请求的用户身份信息可以存储在ThreadLocal中,以便在整个请求处理过程中方便访问,而不必在每个方法参数中显式传递。
    • 事务管理:在多线程的事务环境中,可以使用ThreadLocal存储和传递事务上下文,确保每个线程都能独立地管理自己的事务状态。
  2. 跨函数传递数据(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



}

image-20240326204337171

在这个例子中,我通过Executros静态方法创建了只有两个线程的固定线程池,模拟在线程池中传递一个 ThreadLocal 变量。

然后,我们使用 threadLocal.set() 方法将值放入 ThreadLocal 变量中

接下来,向线程池中提交两个任务

在每个任务中,通过 threadLocal.get() 方法获取 ThreadLocal 变量的值,并使用 threadLocal.set() 方法修改变量的值。

需要注意的是,由于线程池中的线程是复用的,所以可能会出现线程在不同任务间切换的情况。因此,每个任务都可以独立地访问和修改自己的 ThreadLocal 变量副本,而不会影响其他任务。

最后,我们可以观察到最终的两个任务中,获取到的 ThreadLocal 变量的值仍然是各自任务设置的最新值。

需要注意的是,由于线程池的线程是有限的,如果线程池中的线程数量小于任务数量,那么部分任务可能会等待可用的线程。在等待期间,线程池中的某个线程可能会被重新分配给其他任务,从而导致 ThreadLocal 变量副本的切换。因此,在使用 ThreadLocal 和线程池结合时,需要注意线程切换可能带来的副作用。

总结:通过在线程池中使用 ThreadLocal,我们可以实现线程隔离,确保每个线程都能独立地访问和修改自己的 ThreadLocal 变量副本,而不会影响其他线程。这在需要在线程池中传递数据,并保持数据隔离的场景中非常有用。

2.2.跨函数传递ThreadLocal

当在企业开发中进行跨函数传递时,ThreadLocal 应用场景非常多,下面是一些常见场景:

  1. 认证和授权信息传递:在一个请求处理过程中,用户的认证和授权信息可能需要在多个函数或方法中共享。使用 ThreadLocal 可以方便地传递这些信息,如用户身份、权限等。
  2. 跨层数据传递:在多层架构中,比如前端控制器、服务层、持久化层等,可能需要在不同层之间传递一些公共的数据,如请求信息、上下文数据等。
  3. 多租户数据隔离:在多租户系统中,不同租户的数据需要进行隔离。通过 ThreadLocal 可以在不同函数或方法中传递租户标识,以确保数据隔离和正确处理。
  4. 全局上下文信息传递:有些全局的上下文信息,如请求ID、语言设置、时间区域等,可能需要在整个应用中共享。ThreadLocal 可以方便地将这些上下文信息传递给需要使用它们的函数或方法。
  5. 事务上下文传递:在使用事务管理时,事务上下文信息可能需要在多个函数或方法中传递。通过 ThreadLocal 可以将事务上下文与当前线程绑定,实现跨函数的事务传递。
  6. 日志追踪:在分布式系统中,日志追踪是常见的需求。通过在 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 实现了数据在不同方法间的传递和共享,避免了重复的代码,保持了逻辑的清晰性。

image-20240326211857285

问题?上述都在单线程环境下运行的,相当于同步的一个操作,那么如果在多线程并发的环境下运行的呢?还是我们想要的结果吗??下面我们通过一个案例来观察一下。

3.多线程(异步)下的ThreadLocal

	/**
	 * 多线程环境下的ThreadLocal
	 * @throws InterruptedException
	 */
	@Test
	@DisplayName("测试threadLocal在线程池中传递")
	public void test2() throws InterruptedException {
   

		ExecutorService executorService = Executors
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值