1.使用场景
- 每个线程需要一个独享的对象(通常是工具类,例如SimpleDateFormat在并发情况下是不安全的
- 每个线程内需要保存全局变量(例如拦截器的用户信息
1.独享对象
每个Thread内有自己的实例副本,不共享
比喻:一个班级有一本教材,学生要做笔记的话会有线程安全,那么就每个学生打印一份
例子:SimpleDateFormat的进化
刚开始我们线程池中有1000个线程任务,它们都需要去调用date
方法来进行日期格式转化
此时的date
方法中会单独创建SimpleDateFormat对象,那么1000个任务就要创建1000个对象使用完毕后又要进行销毁会造成资源浪费
/**
* 毫秒 转为 yyyy-MM-dd hh:mm:ss
* @param seconds
* @return
*/
public String date(int seconds){
Date date = new Date(1000*seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return dateFormat.format(date);
}
于是我们将SimpleDateFormat变成了一个公共对象
然后再运行线程池时,就出现了问题:
有多个值相同的结果出现了!
这是因为多个线程共用一个对象造成的!而SimpleDateFormat
本身又不是线程安全的,所以就出现了这种现象!
如何解决?===>上锁!
public String date(int seconds){
Date date = new Date(1000*seconds);
String s = null;
//类锁
synchronized (ThreadLocalNormalUsage03.class){
s = dateFormat.format(date);
}
return s;
}
但是使用锁就会造成效率降低!
ThreadLocal闪亮登场!
使用ThreadLocal 每个线程会生成一份属于他们自己的format对象,这样就不会有并发问题也不会降低性能!
定义一个类:每个线程进入该方法只会生成一份!
需要重写==initialValue()==方法
class ThreadSafeFormatter{
//每个线程会生成一份
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
}
date方法通过ThreadSafeFormatter来获得对象
public String date(int seconds){
Date date = new Date(1000*seconds);
//SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
2.全局变量
例子:不同请求携带着不同的用户信息,那么就不能用static来存放信息了!
升级例子:在上个例子的基础上,使用一个UserMap
来存放用户信息,每个线程通过不同的key来get出相应的用户信息
但是!多个线程来操作map就会有线程安全问题,那么就又需要用到 synchronized / ConcurrentHashMap 但是不论用以上哪个,都会对性能造成影响!!!
最好的方法: ThreadLocal
保存当前线程的用户信息
- 强调的是用一个请求(线程)内不同方法间的共享
- 不需要重写initialValue(),但是必须手动调用set()
/**
* 演示ThreadLocal用法2:避免参数传递的麻烦
*/
public class ThreadLocalNormalUsage05 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i <3; i++) {
threadPool.execute(()->{
new Service1().process();
});
}
}
}
//方法1
class Service1{
public void process(){
User user = new User("Roderick"+Thread.currentThread().getName());
//将用户信息set到ThreadLocal
UserContextHolder.holder.set(user);
new Service2().process();
}
}
//方法2
class Service2{
public void process(){
//省略业务逻辑
User user = UserContextHolder.holder.get(); //从ThreadLocal中获取当前线程的变量
System.out.println("Service2获取到user"+user.toString());
new Service3().process();
}
}
//方法3
class Service3{
public void process(){
//省略业务逻辑
User user = UserContextHolder.holder.get();//从ThreadLocal中获取当前线程的变量
System.out.println("Service3获取到user"+user.toString());
}
}
class UserContextHolder{
//ThreadLocal
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User{
String name;
public User(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
通过ThreadLocal,即使在不同方法中也能获取到当前线程存放的值
3.总结
ThreadLocal的两个作用
- 每个线程都有自己的独立对象【线程隔离】
- 可以在任何方法中轻松获取到对象
如何正确选择使用场景(共享对象生成时机)
- 场景一:initialValue(对象初始化时机由我们控制)
在初始化ThreadLocal
时,就可以将对象初始化,例如(SimpleDateFormat) - 场景二:set(对象初始化时机不由我们控制)
ThreadLocal
里的对象并不是在创建ThreadLocal
时就设置好的!而是需要等拦截器传递(如User)后再手动的将其set
进去!以便后续使用
2.好处
- 线程安全
- 不需要加锁,提高效率
高效的利用内存、节省开销
例如:相比每个任务都新建一个SimpleDateFormat来保证线程安全,显然ThreadLocal可以节省空间
避免传参的麻烦
可以在任何方法内都获取到线程的ThreadLocal对象,无需通过参数传递
3.原理
- Thread、ThreadLocal、ThreadLocalMap三者关系
每个Thread对象中都持有一个ThreadLocalMap对象,Map中以KeyValue存储ThreadLocal
4.主要方法
1.initialValue()
初始化 (没有默认实现,需重写)
- 该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用
get()
的时候才会触发 - 当线程第一次使用
get
访问变量,才会调用initialValue()
但是!当之前使用了set
方法,那么就不会调用initialValue()
- 每个线程最多只需要调用一次
initialValue()
.但是!如果调用了remove()
那么下次get()
会再次调用
2.void set()
为线程设置值
3.get()
得到线程对应的value,如果是首次调用get(),并且没有set值,那么会调用initialize来得到值
主要逻辑:先取出当前线程的ThreadLocalMap
然后调用map.getEntry
将本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value
map.getEntry传入的是this—->当前ThreadLocal,将其作为key去找到value
4.remove()
删除对应这个线程的值
先获取当前线程的ThreadLocalMap
如何移除Map中的value(以当前ThreadLocal做key)
5.ThreadLocalMap
ThreadLocalMap类,也就是Thread.threadLocals
- ThreadLocalMap类是每个线程Thread类里的变量,里面最重要的是一个键值对数组
Entry[] table
,可以认为是一个map:- key:当前ThreadLocal
- value:实际存放的变量,如user、SimpleDateFormat
- 从源码来看,
setInitialValue
和set()
最后都是利用map.set()
来设置值到Map中 - 最后都会对应到ThreadLocalMap的一个Entry
6.注意点
1.内存泄漏
某个对象不再有用,但是占用的内存却不能被回收!
在ThreadLocalMap类中,我们可以看到他的内部类Entry的key是弱引用的,但是value是强引用!
key’是弱引用在gc的时候能够被顺利回收,但是value却是强引用,那么就有可能造成内存泄漏问题
- 正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收
- 但是!如果线程不终止【例如线程池!】,那么key对应的value就不能被回收!
那么当线程多次执行任务。虽然Map中的key是弱引用会被gc, 但是value不会!这就造成了内存泄漏;可能会导致OOM
JDK已经考虑到了这个问题,所以在set
remove
rehash
方法中会扫描key为null的Entry,并把对应的value设置为null,这样value就会被gc
但是万一我们不调用
set
remove
rehash
怎么办呢?
如何避免内存泄漏?(阿里规约)
当我们使用完ThreadLocal,就应该调用remove()
将value设为null!!
2.空指针
在ThreadLocal没有set的情况下进行get,一般会得到null
但是需要注意的是,接收的时候避免装箱拆箱,否则可能会导致空指针异常
例如ThreadLocal用long接受
3.共享对象
假设每个线程中ThreadLocal.set()
传入的本身就是多线程共享的对象,例如static
那么使用get取出来的还是这个共享对象本身,还是有可能造成并发问题!
较量避免放入这种对象
7.Spring实例
在框架由ThreadLocal相关使用的情况下,优先使用框架的,尽量不要自己维护
例如DateTimeContextHolder
类,就使用到了ThreadLocal
每次HTTP请求都对应一个线程,线程之间相互隔离,这也是ThreadLocal的典型应用场景