ThreadLocal解析
目录1. 两大使用场景——ThreadLocal的用途
典型场景1:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)
进化1:使用两个线程打印日期
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 描述:两个线程打印日期
*/
public class ThreadLocalNormalUsage00 {
public static void main(String[] args) {
new Thread( ()->{
String date = new ThreadLocalNormalUsage00().date(10);
System.out.println(date);
}).start();
new Thread(() ->{
String date = new ThreadLocalNormalUsage00().date(104707);
System.out.println(date);
}).start();
}
public String date(int second){
Date date = new Date(1000*second);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return dateFormat.format(date);
}
}
打印结果:
1970-01-01 08:00:10
1970-01-02 13:05:07
进化2:使用10个线程打印日期
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 10个线程打印日期
*/
public class ThreadLocalNormalUsage01 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage01().date(finalI);
System.out.println(date);
}
}).start();
Thread.sleep(100);
}
}
/**
* 参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
* @param second 毫秒
* @return
*/
public String date(int second){
Date date = new Date(1000*second);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return dateFormat.format(date);
}
}
打印结果:
1970-01-01 08:00:00
1970-01-01 08:00:01
1970-01-01 08:00:02
1970-01-01 08:00:03
1970-01-01 08:00:04
1970-01-01 08:00:05
1970-01-01 08:00:06
1970-01-01 08:00:07
1970-01-01 08:00:08
1970-01-01 08:00:09
...
进化3:1000个线程打印日期,将进化2中的代码for循环终止值改为1000,可执行,不出错,但是执行效率非常慢。
进化4:使用线程池优化1000个线程打印日期,因为每次线程执行都需要创建SimpleDateFormat对象,将该对象提取到外层。
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 1000个打印日期的任务,用线程池来执行
*/
public class ThreadLocalNormalUsage03 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage03().date(finalI);
System.out.println(date);
}
});
}
// 线程执行完毕,停止线程
threadPool.shutdown();
}
/**
* 参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
* @param second 毫秒
* @return
*/
public String date(int second){
Date date = new Date(1000*second);
return dateFormat.format(date);
}
}
打印结果:
1970-01-01 08:00:05
1970-01-01 08:00:05
1970-01-01 08:00:05
1970-01-01 08:00:04
1970-01-01 08:00:05
1970-01-01 08:00:06
1970-01-01 08:00:03
1970-01-01 08:00:07
此时,已经出现问题,按顺序打印出来的值有重复数值,逻辑上i变量传入date,应不会有重复的值。
原因就在于多线程下,SimpleDateFormat并非是线程安全的,因为SimpleDateFormat在格式化和解析日期时间时,使用了全局的日历(Calendar)对象,这个对象可以被多个线程访问到。当多个线程同时访问SimpleDateFormat时,可能会对日历对象进行修改,导致格式化和解析的结果不一致,这就是线程不安全的问题。
进化5:使用Synchronize锁解决线程不安全场景
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 加锁来解决线程安全问题
*/
public class ThreadLocalNormalUsage04 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage04().date(finalI);
System.out.println(date);
}
});
}
// 线程执行完毕,停止线程
threadPool.shutdown();
}
/**
* 参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
* @param second 毫秒
* @return
*/
public String date(int second){
Date date = new Date(1000*second);
String s = null;
//使用类锁
synchronized (ThreadLocalNormalUsage04.class){
s = dateFormat.format(date);
}
return s;
}
}
打印结果:
1970-01-01 08:00:00
1970-01-01 08:00:02
1970-01-01 08:00:06
1970-01-01 08:00:03
1970-01-01 08:00:12
1970-01-01 08:00:11
1970-01-01 08:00:10
1970-01-01 08:00:01
......
从结果中可以看到,已经可以实现该功能。但使用synchronized会使得每一个线程排队依次执行format方法。效率大打折扣。
进化6:利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
*/
public class ThreadLocalNormalUsage05 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage05().date(finalI);
System.out.println(date);
}
});
}
// 线程执行完毕,停止线程
threadPool.shutdown();
}
/**
* 参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
* @param second 毫秒
* @return
*/
public String date(int second){
Date date = new Date(1000*second);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter{
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
// 结合Lambda表达式改写
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal.withInitial(()->
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);
}
总结:SimpleDateFormat的进化之路
- 2个线程分别用自己的SimpleDateFormat,这没有问题。
- 后来延伸出10个,那就有10个线程和10个SimpleDateFormat,虽然写法不优雅(应该复用对象),但勉强可以接受。
- 但是当需求变成了1000个,那么必然要用线程池(否则消耗内存太多)
- 所有的线程都共用同一个SimpleDateForamat对象
- 这是线程不安全的,出现了并发安全问题。
- 我们可以选择加锁,加锁后结果正常,但是效率低
- 在这里更好的解决方案是ThreadLocal
- Lambda表达式改写优化
- 线程安全
- 没有Synchronize带来的性能问题,完全可以并行执行。因为每个线程内都有自己独享的对象,所以不同的线程之间不会相互共享,也就不存在线程安全问题
典型场景2:每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦
目标:每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦
-
在此基础上可以演进,使用UserMap,但是Map是线程不安全的。
-
可以使用Synchronized,也可以使用ConcurrentHashMap,但是无论怎么用,势必会对性能有所影响。
- 使用ThreadLocal
案例:使用ThreadLocal存储用户对象信息
public class ThreadLocalNormalUsage06 {
public static void main(String[] args) {
new Service1().process();
}
}
class Service1 {
public void process() {
User user = new User("admin");
//放入对象
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service2 拿到用户名:" + user.name);
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
打印信息:
Service2 拿到用户名:admin
流程图如下
场景总结
- 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
- 在任何方法中都可以轻松获取到该对象
如何选择场景是使用initialValue还是set
2.使用ThreadLocal带来的好处
- 达到线程安全
- 不需要加锁,提高执行效率
- 更高效地利用内存、节省开销:相比于每个任务都新建一个SimpleDateFormat、显然使用ThreadLocal可以节省内存和开销。
- 免去传参的繁琐:无论是场景一的工具类,还是场景二的用户名,都可以在任务地方直接通过ThreadLocal拿到,再也不需要每次都传相同的参数。ThreadLocal使得代码耦合度更低,更优雅。
3.主要方法介绍
initialValue():初始化。
- 该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发。
- 当线程第一次使用get方法访问变量时,在调用此方法,除非线程先调用了Set方法,在这种情况下,不会为线程调用本initialValue方法。
- 通常,每个线程最多调用一次此方法,但如果已经调用了remove()后,在调用get(),则可以再次调用此方法。
- 如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。
get():
先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value
注意:这个map以及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中
set():与setInitialValue类似
4.原理、源码
每一个Thread对象都持有一个ThreadLocalMap成员变量。
- 冲突:HashMap(拉链法)
- ThreadLocalMap:使用线性探测法
5.ThreadLocal注意点
-
内存泄露
-
什么是内存泄漏:某个对象不再有用,但是占用的内存却不能被回收。
-
弱引用的特点,如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收
-
所以弱引用不会阻止GC,因此这个弱引用的好处体现
-
此处的super(k)查看其父类为WeakReference(),弱引用。
如何避免内存泄露(阿里规约)
- 调用remove方法,就会删除对应的Entry对象,可以避免内存泄露,所以使用完ThreadLocal之后,应该调用remove方法。
6. 空指针问题
使用包装类,而非基础类。如果第8行get()方法的返回对象是long,由于包装类拆箱,而包装类为null(setInitialValue初始化返回为null),导致NPE
public class ThreadLocalNPE {
ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
public void set(){
longLocal.set(Thread.currentThread().getId());
}
// 注意使用包装类,而非基础类
public Long get(){
return longLocal.get();
}
public static void main(String[] args) {
ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
System.out.println(threadLocalNPE.get());
// 主线程线程id是1
// threadLocalNPE.set();
// System.out.println(threadLocalNPE.get());
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
threadLocalNPE.set();
System.out.println(threadLocalNPE.get());
}
});
thread.start();
}
}
实际应用场景——在Spring中实践
- DateTimeContextHolder类,看到里面用到了ThreadLocal。
- RequestContextHolder中,每次HTTP请求都对应一个线程,线程之间相互隔离。如getRequestAttributes()这就是ThreadLocal的典型应用场景。