一、ThreadLocal两大使用场景
- 每个线程需要一个独享的对象
- 每个线程内需要保存全局变量
1) 每个线程需要一个独享的对象
- 通常是工具类(线程不安全),典型需要使用的类比如SimpleDateFormat和Random
- ThreadLocal定义为静态变量
- 通过重写initialValue()方法在本地线程第一次获取对象时进行创建。
- 本地线程通过threadLocal.get()获取该对象。
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @auth Hahadasheng
* @since 2020/10/27
*/
public class ThreadLocalExclusiveObj {
private static final ThreadLocal<SimpleDateFormat> dateFormatLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"));
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
final int index = i;
executor.execute(() -> {
Date date = new Date(1000 * index);
String format = dateFormatLocal.get().format(date);
System.out.println(format);
});
}
executor.shutdown();
}
}
- 拓展:时间格式化
注意,h和H代表的含义是不一样的
y = 年(yy或yyyy)
M = 月(MM)
d = 月中的天(dd)
h = 小时(0-12)(hh)
H = 小时(0-23)(HH)
m = 时分(mm)
s = 秒(ss)
S = 毫秒(SSS)
z = 时区文本(例如,太平洋标准时间…)
Z = 时区,时间偏移量(例如-0800)
以下是一些模式示例,其中包含每个模式如何格式化或期望解析日期的示例:
yyyy-MM-dd(2009-12-31)
dd-MM-YYYY(31-12-2009)
yyyy-MM-dd HH:mm:ss(2009-12-31 23:59:59)
HH:mm:ss.SSS(23:59.59.999)
yyyy-MM-dd HH:mm:ss.SSS(2009-12-31 23:59:59.999)
yyyy-MM-dd HH:mm:ss.SSS Z(2009-12-31 23:59:59.999 +0100)
2) 每个线程内需要保存全局变量
- 比如在拦截器中获取用户的信息,可以让不同方法直接使用,避免参数传递的麻烦。
- 在本地线程生命周期内,通过set/get方法设置获取线程独占变量,避免参数到处传递。
- 强调的是同一个请求内(同一个线程)不同方法间的共享。
- 不需要要重写initialValue()方法
可以利用共享的Map:使用static的ConcurrentHashMap,把当前线程的ID作为key,把user作为value来保存,这样可以做到线程间的隔离,但是依然有性能影响。使用ThreadLocak就没有性能影响,内部没有使用synchronized等同步机制,也无需层层传递参数。
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @auth Hahadasheng
* @since 2020/10/29
*/
public class ThreadLocalShareInThread {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 20; i++) {
final int index = i;
pool.execute(() -> {
User user = new User();
user.setId(String.format("NO.%s", index + 1));
user.setName(String.format("HHDS-%s", index + 1));
user.setGender(index & 1);
user.setAge(index + 10);
UserHolder.holder.set(user);
otherMethod();
});
}
pool.shutdown();
}
public static void otherMethod() {
System.out.println(UserHolder.holder.get());
UserHolder.holder.remove();
}
}
@Getter
@Setter
class User {
private String id;
private String name;
private int gender;
private int age;
@Override
public String toString() {
return "{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", gender=" + gender +
", age=" + age +
'}';
}
}
class UserHolder {
public static final ThreadLocal<User> holder = new ThreadLocal<>();
}
3) 总结
- 让某个需要用到的对象在线程间隔离,每个线程都有自己独立的对象
- 在任何方法中都能轻松获取该对象。
- initialValue使用场景:
- 在ThreadLocal第一次get的时候吧对象给初始化,对象的初始化时机可以由我们控制
- set:
- 如果需要保存到ThreadLocal里面的对象的生成时机不由我们随意控制,比如拦截器生成的用户信息,用ThreadLocal.set直接放进去即可
4) ThreadLocal带来的好处
- 达到线程安全
- 不需要加锁,提高执行效率
- 更高效地利用内存,节省开销(例如每个线程持有一个SimpleDateFormat)。
- 免去传参的繁琐
二、ThreadLocal原理
1) Thread与ThreadLocal以及ThreadLocalMap之间的关系
- 每个Thread实例都会有一个独立的ThreadLocalMap对象
- ThreadLocalMap中的Entry的key为ThreadLocal对应的引用(弱引用),value则是线程独享的对象
Thread与TL和TLM之间的关系.png
2) 重要方法
1> T initialValue()
:该方法会返回当前线程对应的“初始值”,延迟加载的方法,只有在调用get的时候才会触发。
- 当线程第一次使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种相框下,不会为线程调用本initialValue方法。
get内部实现是检查对象是否为null,如果为null则执行initialValue()方法<重写该方法后执行重写的方法>,否则直接返回对象。
3.如果调用了remove()后,再调用get(),则可以再次调用initialValue()方法。
- initialValue()方法默认实现是直接返回一个null,如果需要独享对象,一般使用匿名内部类的方式重写该方法。
ThreadLocal.withInitial(() -> {... return ...})
2> void set(T t)
- 为这个线程设置一个新值
3> T get()
- 得到线程对应的value。如果是首次调用get()<之前没有调用void set(T t)>,则会调用initialValue来得到这个值。
4> void remoe()
- 删除线程对应的值。
3) 源码分析
- get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value
- 注意,这个map以及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中
- initialValue方法:默认返回null,可以自定义实现
- remove方法,只删除ThreadLocalMap对应本ThreadLocal引用的Entry
4) ThreadLocalMap类
-
在Thread中以threadLocals作为成员变量
-
ThreadLocalMap类是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个Map键值对
- 键:这个ThreadLocal
- 值:实际需要的成员变量
-
ThreadLocalMap类使用上类似HashMap,但是在实现上略有不同,
- 并没有实现Map接口
-
解决冲突
- HashMap解决Hash冲突的思路是链表+红黑树
- ThreadLocalMap采用的是线性探测法:如果发生冲突,就继续找下一个空位置,而不是用拉链或者红黑树
-
可以当做为一个Map去理解
5) 两种使用场景殊途同归
- setInitialValue和直接set最后都是利用map.set()方法来设置值。最后都会对应到ThreadLocalMap的一个Entry,只不过起点和入口不一样。
三、ThreadLocal注意点
1) 内存泄露
弱引用:如果一个对象只被弱引用关联(没有任务强引用),那这个对象可以被GC垃圾回收
- 内存泄露:某个对象不再有用,但是占用的内存却不能被回收。
- ThreadLocalMap中Entry的key的是弱引用,不会导致泄露问题。
- 每个Entry都包含一个对value的强引用。
- 正常情况下,当线程终止,保存在ThreadLocalMap里的key, value会被垃圾回收,没有任何强引用。
- 如果线程不终止(比如线程需要保持很久),key对应的value就不能被回收,存在如下调用链
- Thread->ThreadLocalMap->Entry(key为null)->Value
- value无法回收,就可能出现OOM
- JDK已经考虑到这个问题,所以在set,remove,rehash方法中会扫描key为null的Entry并发对应的value设置为null,这样value对象就可以被回收了。
- 如果一个ThreadLocal不被使用,那么实际上set,remove,rehash方法也不会被调用,如果同时
线程又不停止,那么调用链就一直存在,就导致了value的内存泄露
2) 避免内存泄露(阿里规约)
- 使用完ThreadLocal之后主动调用remove方法,删除Entry对象,避免内存泄露。
3) ThreadLocal空指针异常
- 在进行get之前,必须先set,否则可能会报空指针异常?
- 可能是写的代码缺陷:包装类拆箱导致
/**
* @auth Hahadasheng
* @since 2020/10/30
*/
public class ThreadLocalNPE {
private static final ThreadLocal<Long> localId = new ThreadLocal<>();
/**
* 这里在没有调用initializeValue以及set的前提下直接调用get方法,
* 似乎直接返回null,但是却报java.lang.NullPointerException
* 是因为ThreadLocal定义的泛型为包装类的Long,在方法返回时拆箱
* 发现是null,所以报空指针
*/
public static long get() {
return localId.get();
}
/**
* 而这个方法则不会报错
*/
public static Long get2() {
return localId.get();
}
public static void main(String[] args) {
System.out.println(get2());
System.out.println(get());
}
}
4) 共享对象
- 如果在每个线程中ThreadLocal.set进去的本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get取得的还是这个共享对象本身,还是有并发问题。
如果可以不需要使用ThreadLocal,则不要进行强行使用。
5) 优先使用框架的支持,而不是自己创造
- 例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄露。
- Spring中DateTimeContextHolder类,使用了ThreadLocal
6)关于弱引用被GC清理是否可用的疑惑解答
-
引用的关系是:Thread -> ThreadLocalMap -> Entity -> 弱引用ThreadLocal 和 数据,所以:
-
虽然是弱引用,但是只要其他地方还有普通引用,就不会被清理,会一直存在(1.一般在使用的时候都是定义为静态类属性常量
... static final ThreadLocal<?> ...
,为强引用,只要此类不被虚拟机卸载,则GC不会回收该对象,相关弱引用也不会被清理;2.线程执行产生的栈帧中局部变量表中可能也会存在该强引用)。
提示:GC Roots:虚拟机栈(栈帧中的本地变量表)中引用的对象;方法区中类静态属性引用的对象;方法区中常量引用的对象;本地方法栈中JNI(即一般说的native方法)中引用的对象
- 如果不是弱引用,而且用户已经不再持有这个ThreadLocal的引用并且没有调用remove方法,那么只要线程还在,ThreadLocal和数据就会一直被引用无法回收,就是内存泄漏了,所以这里用弱引用一定程度上是帮助忘记调用remove方法的用户做清理工作…
网友评论