目录
1.ThreadLocal 介绍
ThreadLocal 从字面的意思来理解是线程本地变量的意思,也就是说它是线程中的私有变量,每个线程只能使用自己的变量。
ThreadLocal 原本设计是为了解决并发时,线程共享变量的问题,但由于过度设计,如弱引用和哈希碰撞,从而导致它的理解难度大和使用成本高等问题。当然,如果稍有不慎还是导致脏数据、内存溢出、共享变量更新等问题,但即便如此,ThreadLocal 依旧有适合自己的使用场景,以及无可取代的价值,比如本文要介绍了这两种使用场景,除了 ThreadLocal 之外,还真没有合适的替代方案。
2.ThreadLocal 的API
官方说明文档: https://docs.oracle.com/javase/8/docs/api/
2.1 ThreadLocal 核心方法:
- set 方法:用于设置线程独立变量副本。没有 set 操作的 ThreadLocal 容易引起脏数据。
- get 方法:用于获取线程独立变量副本。没有 get 操作的 ThreadLocal 对象没有意义。
- remove 方法:用于移除线程独立变量副本。没有 remove 操作容易引起内存泄漏。
public class ThreadLocalExample {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();//创建 ThreadLocal 对象
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " 存入值:" + threadName);
threadLocal.set(threadName); // 向 ThreadLocal 中设置值
print(threadName);
}
};
new Thread(runnable, "MyThread-1").start();
new Thread(runnable, "MyThread-2").start();
}
/**
* 打印线程中的 ThreadLocal 值
*/
private static void print(String threadName) {
try {
String result = threadLocal.get(); // 得到 ThreadLocal 中取出值
System.out.println(threadName + " 取出值:" + result);
} finally {
threadLocal.remove(); // 移除 ThreadLocal 中的值(防止内存溢出)
}
}
}
从上述结果可以看出,每个线程只会读取到属于自己的 ThreadLocal 值。
2.2 ThreadLocal 初始化:initialValue()
public class ThreadLocalExample {
//创建 ThreadLocal 对象
private static ThreadLocal<String> threadLocal = new ThreadLocal() {
@Override
protected String initialValue() {
System.out.println("执行 initialValue() 方法");
return "默认值";
}
};
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
print(threadName);
}
};
new Thread(runnable, "MyThread-1").start();
new Thread(runnable, "MyThread-2").start();
}
/**
* 打印线程中的 ThreadLocal 值
*/
private static void print(String threadName) {
try {
String result = threadLocal.get(); // 得到 ThreadLocal 中取出值
System.out.println(threadName + " 取出值:" + result);
} finally {
threadLocal.remove(); // 移除 ThreadLocal 中的值(防止内存溢出)
}
}
}
执行结果:
注意: 当先使用了 threadLocal.set() 方法之后,initialValue() 方法就不会被执行了,如下代码所示:
public class ThreadLocalExample {
//创建 ThreadLocal 对象
private static ThreadLocal<String> threadLocal = new ThreadLocal() {
@Override
protected String initialValue() { // 注意返回值类型要与泛型一致
System.out.println("执行 initialValue() 方法");
return "默认值";
}
};
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " 存入值:" + threadName);
threadLocal.set(threadName); // 向 ThreadLocal 中设置值
print(threadName);
}
};
new Thread(runnable, "MyThread-1").start();
new Thread(runnable, "MyThread-2").start();
}
/**
* 打印线程中的 ThreadLocal 值
*/
private static void print(String threadName) {
try {
String result = threadLocal.get(); // 得到 ThreadLocal 中取出值
System.out.println(threadName + " 取出值:" + result);
} finally {
threadLocal.remove(); // 移除 ThreadLocal 中的值(防止内存溢出)
}
}
}
为什么 先执行 set() 之后,初始化代码就不执行了?
要理解这个问题,需要从 ThreadLocal.get() 方法的源码中得到答案,因为初始化方法 initialValue() 在 ThreadLocal 创建时并不会立即执行,而是在调用了 get() 方法只会才会执行,测试代码如下:
从上述源码可以看出,当 ThreadLocal 中有值时会直接返回值 e.value,只有 Threadlocal 中没有任何值时才会执行初始化方法 initialValue()。
2.3 ThreadLocal 初始化:withInitial()
import java.util.function.Supplier;
public class ThreadLocalExample {
// 完整写法
// private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(new Supplier<String>() {
// @Override
// public String get() {
// System.out.println("执行 withInitial() 方法");
// return "默认值";
// }
// });
// lambda表达式简写
private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "默认值");
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
// System.out.println(threadName + " 存入值:" + threadName);
// threadLocal.set(threadName); // 向 ThreadLocal 中设置值
print(threadName);
}
};
new Thread(runnable, "MyThread-1").start();
new Thread(runnable, "MyThread-2").start();
}
/**
* 打印线程中的 ThreadLocal 值
*/
private static void print(String threadName) {
try {
String result = threadLocal.get(); // 得到 ThreadLocal 中取出值
System.out.println(threadName + " 取出值:" + result);
} finally {
threadLocal.remove(); // 移除 ThreadLocal 中的值(防止内存溢出)
}
}
}
注意: 同理,当先使用了 threadLocal.set() 方法之后,withInitial() 方法就不会被执行了。
3 使用场景
3.1 场景一:ThreadLocal 实现时间格式化
public class SimpleDateFormatTest {
// 创建 ThreadLocal 并设置默认值
private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
public static void main(String[] args) {
// 创建线程池执行任务
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
// 执行任务
for (int i = 0; i < 1000; i++) {
int finalI = i;
// 执行任务
threadPool.execute(() -> {
Date date = new Date(finalI * 1000); // 得到时间对象
formatAndPrint(date); // 执行时间格式化
});
}
threadPool.shutdown(); // 线程池执行完任务之后关闭
}
/**
* 格式化并打印时间
*/
private static void formatAndPrint(Date date) {
String result = dateFormatThreadLocal.get().format(date); // 执行格式化
System.out.println("时间:" + result); // 打印最终结果
}
}
使用 ThreadLocal 也可以解决线程并发问题,并且避免了代码加锁排队执行的问题。
SimpleDateFormat的非线程安全问题见另一篇博文 https://blog.youkuaiyun.com/QiuHaoqian/article/details/116594422
3.2 场景二:跨类传递数据
可以使用 ThreadLocal 来实现线程中跨类、跨方法的数据传递。
比如登录用户的 User 对象信息,需要在不同的子系统中多次使用,如果使用传统的方式,需要使用方法传参和返回值的方式来传递 User 对象,然而这样就无形中造成了类和类之间,甚至是系统和系统之间的相互耦合了,所以此时我们可以使用 ThreadLocal 来实现 User 对象的传递。
演示代码如下:
先在主线程中构造并初始化一个 User 对象,并将此 User 对象存储在 ThreadLocal 中,存储完成之后,就可以在同一个线程的其他类中,如仓储类或订单类中直接获取并使用 User 对象了。
主线程中的业务代码:
public class ThreadLocalByUser {
public static void main(String[] args) {
// 初始化用户信息
User user = new User("小明");
// 将 User 对象存储在 ThreadLocal 中
UserStorage.setUser(user);
// 调用订单系统
OrderSystem orderSystem = new OrderSystem();
// 添加订单(方法内获取用户信息)
orderSystem.add();
// 调用仓储系统
RepertorySystem repertory = new RepertorySystem();
// 减库存(方法内获取用户信息)
repertory.decrement();
}
}
User 实体类:
/**
* 用户实体类
*/
class User {
public User(String name) {
this.name = name;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
ThreadLocal 操作类:
/**
* 用户信息存储类
*/
class UserStorage {
public static ThreadLocal<User> USER = new ThreadLocal(); // 存储用户信息
public static void setUser(User user) {
USER.set(user);
}
}
订单类:
/**
* 订单类
*/
class OrderSystem {
/**
* 订单添加方法
*/
public void add() {
// 从ThreadLocal得到用户信息
User user = UserStorage.USER.get();
// 业务处理代码(忽略)...
System.out.println(String.format("订单系统收到用户:%s 的请求。", user.getName()));
}
}
仓储类:
/**
* 仓储类
*/
class RepertorySystem {
/**
* 减库存方法
*/
public void decrement() {
// 从ThreadLocal得到用户信息
User user = UserStorage.USER.get();
// 业务处理代码(忽略)...
System.out.println(String.format("仓储系统收到用户:%s 的请求。", user.getName()));
}
}
执行结果:
从上述结果可以看出,当在主线程中先初始化了 User 对象之后,订单类和仓储类无需进行任何的参数传递也可以正常获得 User 对象了,从而实现了一个线程中,跨类和跨方法的数据传递。
4 总结
使用 ThreadLocal 可以创建线程私有变量,所以不会导致线程安全问题,同时使用 ThreadLocal 还可以避免因为引入锁而造成线程排队执行所带来的性能消耗;再者使用 ThreadLocal 还可以实现一个线程内跨类、跨方法的数据传递。