手把手教你使用ThreadLocal

什么是ThreadLocal?

首先我们来看javadoc

This class provides thread-local variables. 
These variables differ from their normal counterparts in that each thread that accesses one 
(via its get or set method) has its own, 
independently initialized copy of the variable. 
ThreadLocal instances are typically private static fields in classes that wish to 
associate state with a thread (e.g., a user ID or Transaction ID).
译:这个类提供线程局部变量。
这些变量与其普通对应变量的不同之处在于,
访问一个变量的每个线程(通过其get或set方法)都有其自己的独立初始化的变量副本。
ThreadLocal实例通常是 
希望将状态与线程(例如,用户ID或事务ID)相关联的类中的私有静态字段。

通过阅读官方文档,我们知道:ThreadLocal是一个关于创建线程局部变量的类。
通常情况下,我们创建的成员变量都是线程不安全的。
因为他可能被多个线程同时修改,
此变量对于多个线程之间彼此并不独立,是共享变量。
而使用ThreadLocal创建的变量只能被当前线程访问,
其他线程无法访问和修改。也就是说:将线程公有化变成线程私有化

ThreadLocal有什么作用?

使用场景1,确保线程安全

ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。

使用场景2,避免传参

ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。

实战源码

需求:循环1000次,依次输出mm:ss格式的时间字符串。
预期:输出1000个不重复的mm:ss格式的时间字符串。

  1. 演示在单线程环境下,使用SimpleDateFormat是安全的
  2. 演示在多线程环境下,使用SimpleDateFormat是不安全的
  3. 演示使用ThreadLocal类,使SimpleDateFormat实例成为线程独享,保证线程安全

1. 单线程环境

import java.text.SimpleDateFormat;
import java.util.Date;

import lombok.extern.slf4j.Slf4j;

/**
 * <pre>
 * 程序目的:演示在单线程的环境中,使用SimpleDateFormat是ok的
 * 部分输出结果:
 * 运行总耗时:11633ms
 * </pre>
 * created at 2020/7/17 07:55
 * @author lerry
 */
@Slf4j
public class SimpleDateFormatInOneThread {
	private static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");

	public static void main(String[] args) {
		long startTime = System.currentTimeMillis();
		for (int i = 0; i < 1000; i++) {
			final int finalI = i;
			String date = new SimpleDateFormatInOneThread().date(finalI);
			log.info("\tdate is {}", date);
		}
		log.info("运行总耗时:{}ms", (System.currentTimeMillis() - startTime));
	}

	/**
	 * 按照秒数,输出一天开始的时间格式表示
	 * 如:第1秒 00:01
	 * 第2秒 00:02
	 * 第60秒 01:00
	 * @param seconds
	 * @return
	 */
	public String date(int seconds) {
		Date date = new Date(1000 * seconds);
		// 模拟方法执行耗时
		try {
			Thread.sleep(10);
		}
		catch (InterruptedException e) {
			e.printStackTrace();
		}
		return dateFormat.format(date);
	}
}

在这里插入图片描述

图:单线程环境下的输出结果
可以看到,程序从00:00输出到16:39,符合预期

2. 多线程环境

在此,使用了线程池来管理线程。

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import lombok.extern.slf4j.Slf4j;

/**
 * <pre>
 * 程序目的:演示多线程访问线程不安全的类
 * 部分输出结果:
 * 运行总耗时:2956ms
 * </pre>
 * created at 2020/7/17 07:55
 * @author lerry
 */
@Slf4j
public class SimpleDateFormatInMultiThread {

	/**
	 * 核心线程数
	 */
	public static final int CORE_POOL_SIZE = 4;

	/**
	 * 等待队列数量
	 */
	private static ArrayBlockingQueue queue = new ArrayBlockingQueue(1000 - CORE_POOL_SIZE);

	/**
	 * 线程池
	 */
	private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE, CORE_POOL_SIZE, 0, TimeUnit.SECONDS, queue);

	/**
	 * 线程共享的成员变量
	 */
	private static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");

	public static void main(String[] args) {
		long startTime = System.currentTimeMillis();
		for (int i = 0; i < 1000; i++) {
			final int finalI = i;
			threadPool.submit(() -> {
				String date = new SimpleDateFormatInMultiThread().date(finalI);
				log.info("\tdate is {}", date);
			});
		}
		threadPool.shutdown();

		for (; ; ) {
			if (threadPool.isTerminated()) {
				log.info("运行总耗时:{}ms", (System.currentTimeMillis() - startTime));
				break;
			}
		}
	}

	/**
	 * 按照秒数,输出一天开始的时间格式表示
	 * 如:第1秒 00:01
	 * 第2秒 00:02
	 * 第60秒 01:00
	 * @param seconds
	 * @return
	 */
	public String date(int seconds) {
		Date date = new Date(1000 * seconds);
		// 模拟方法执行耗时
		try {
			Thread.sleep(10);
		}
		catch (InterruptedException e) {
			e.printStackTrace();
		}
		return dateFormat.format(date);
	}
}

在这里插入图片描述

图:多线程环境下,存在重复输出

3. 使用ThreadLocal线程独享SimpleDateFormat

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import lombok.extern.slf4j.Slf4j;

/**
 * <pre>
 * 程序目的:演示多线程访问线程不安全的类,逐步引入 ThreadLocal 类
 * 可以让每个线程都拥有一个自己的 simpleDateFormat 对象,来保证线程安全
 * 输出结果:
 * 运行总耗时:2985ms
 * </pre>
 * created at 2020/7/17 07:55
 * @author lerry
 */
@Slf4j
public class ThreadLocalDemoWithThreadLocal {

	/**
	 * 核心线程数
	 */
	public static final int CORE_POOL_SIZE = 4;

	/**
	 * 等待队列数量
	 */
	private static ArrayBlockingQueue queue = new ArrayBlockingQueue(1000 - CORE_POOL_SIZE);

	/**
	 * 线程池
	 */
	private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE, CORE_POOL_SIZE, 0, TimeUnit.SECONDS, queue);

	public static void main(String[] args) throws InterruptedException {
		long startTime = System.currentTimeMillis();
		for (int i = 0; i < 1000; i++) {
			final int finalI = i;
			threadPool.submit(() -> {
				String date = new ThreadLocalDemoWithThreadLocal().date(finalI);
				log.info("\tdate is {}", date);
			});
		}
		threadPool.shutdown();

		for (; ; ) {
			if (threadPool.isTerminated()) {
				log.info("运行总耗时:{}ms", (System.currentTimeMillis() - startTime));
				break;
			}
		}
	}

	/**
	 * 按照秒数,输出一天开始的时间格式表示
	 * 如:第1秒 00:01
	 * 第2秒 00:02
	 * 第60秒 01:00
	 * @param seconds
	 * @return
	 */
	public String date(int seconds) {
		Date date = new Date(1000 * seconds);
		SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
		// 模拟方法执行耗时
		try {
			Thread.sleep(10);
		}
		catch (InterruptedException e) {
			e.printStackTrace();
		}
		return dateFormat.format(date);
	}
}

/**
 * <pre>
 * 我们使用了 ThreadLocal 帮每个线程去生成它自己的 simpleDateFormat 对象,
 * 对于每个线程而言,这个对象是独享的。
 * 但与此同时,这个对象就不会创造过多,一共只有 4 个,因为在线程池管理下,线程只有 4 个。
 * </pre>
 * created at 2020/7/17 08:11
 * @author lerry
 */
@Slf4j
class ThreadSafeFormatter {
	public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> {
		SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
		log.info("simpleDateFormat对象的hashCode:{}", System.identityHashCode(simpleDateFormat));
		return simpleDateFormat;
	});
}

在这里插入图片描述

图:使用了ThreadLocal的输出结果

接下来,演示:

使用场景2,避免传参

/**
 * <pre>
 * 程序目的:演示ThreadLocal类的用法
 * 每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),
 * 可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。
 * 例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),
 * 这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。
 * 在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,
 * 避免了将这个对象(如 user 对象)作为参数传递的麻烦。
 * </pre>
 * created at 2020/7/18 12:28
 * @author lerry
 */
@Slf4j
public class ThreadLocalDemo07 {

	public static void main(String[] args) {
		new Service1().service1();
	}
}

/**
 * 需要执行的服务类
 * created at 2020-07-18 12:28
 * @author lerry
 */
@Slf4j
class Service1 {

	public void service1() {
		String name = "浩克";
		User user = new User(name);
		log.info("service1设置用户名:{}", name);
		UserContextHolder.holder.set(user);
		new Service2().service2();
	}
}

@Slf4j
class Service2 {

	public void service2() {
		User user = UserContextHolder.holder.get();
		log.info("Service2拿到用户名:{}", user.name);
		new Service3().service3();
	}
}

@Slf4j
class Service3 {

	public void service3() {
		User user = UserContextHolder.holder.get();
		log.info("Service3拿到用户名:{}", user.name);
		UserContextHolder.holder.remove();
	}
}

/**
 * 持有一个ThreadLocal成员变量,保存了User用户信息
 * created at 2020-07-18 12:29
 * @author lerry
 */
class UserContextHolder {
	public static ThreadLocal<User> holder = new ThreadLocal<>();
}

/**
 * 保存用户数据
 * created at 2020-07-18 12:27
 * @author lerry
 */
class User {

	String name;

	public User(String name) {
		this.name = name;
	}
}

在这里插入图片描述

图:使用ThreadLocal避免传参的输出结果

注意事项
  1. 如果各Service,异步执行,那么ThreadLocal传参的方式就不适用了。
  2. 使用完毕,需要手动ThreadLocalremove()方法,来避免内存泄露。 详情可参照:ThreadLocal内存泄漏问题 - 掘金

本文目录结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HTv5ThNl-1595376196416)(evernotecid://A0276EB5-0616-4074-A4BE-0A1331D6E773/appyinxiangcom/1711267/ENResource/p8716)]

图:本文目录结构

参考资料

ThreadLocal内存泄漏问题 - 掘金

环境说明

  • java -version
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)

  • OS:macOS High Sierra 10.13.4
  • 日志:logback
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值