使用场景
典型场景一
每个线程需要一个独享的对象 (通常是工具类,典型需要使用的类有 SimpleDateFormat 和 Random)
1000 个打印线程的任务,都用线程池来运行
避免众多对象创建和销毁的开销,把 SimpleDateFormat 提出来作为公共变量,但这就会产生线程安全问题,解决方案可以加锁,但是太影响性能了,更好的解决方案使用 ThreadLocal,每个 Thread 内有自己的实例副本不共享,利用 ThreadLocal,给每个线程分配自己的 dateFormat 对象,保证了线程安全,高效利用内存
public class ThreadLocalTest1 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args){
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalTest1().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
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");
}
};
}
SimpleDateFormat的进化之路
- 2个线程分别用自己的 SimpleDateFormat,这没问题
- 后来延伸出10个,那就有10个线程和10个SimpleDateFormat ,这虽然写法不优雅(应该复用对象),但勉强可以接受
- 但是当需求变成了 1000 个,那么必然要用线程池(否则消耗内存太多)
- 所有的线程都共用同一个 simpleDateFormat 对象
- 这是线程不安全的,出现了并发安全问题
- 我们可以选择加锁 ,加锁后结果正常,但是效率低
- 在这里更好的解决方案是使用 ThreadLocal
典型场景二
每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦,强调的是同一个请求内(同一个线程内)不同方法间的共享,不需重写 initialValue() 方法,但是必须手动调用 set() 方法
用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、user ID等)。这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的
当多线程同时工作时,我们需要保证线程安全,可以用 synchronized 也可以用 ConcurrentHashMap ,但无论用什么都会对性能有所影响
更好的办法是使用ThreadLocal ,这样无需synchronized ,可以在不影响性能的情况下,也无需层层传递参数,就可达到保存当前线程对应的用户信息的目的
在线程生命周期内,都通过这个静态 Threadlocal 实例的 get() 方法取得自己set 过的那个对象,避免了将这个对象(例如user对象)作为参数传递的麻烦
public class ThreadLocalTest2 {
public static void main(String[] args) {
new Service1().process("");
}
}
class Service1 {
public void process(String name) {
User user = new User("邦哥");
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
ThreadSafeForMatter.dateFormatThreadLocal.get();
System.out.println("Service2拿到用户名:" + user.name);
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户名:" + user.name);
UserContextHolder.holder.remove();
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
运行结果
Service2拿到用户名:超哥
Service3拿到用户名:超哥
Process finished with exit code 0
两种使用场景殊途同归
通过源码可以看出, setInitialValue 和直接 set 最后都是利用 map.set() 方法来设置值也就是说,最后都会对应到ThreadLocalMap的一个Entry ,只不过是起点和入口不一样
作用
1.让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
2.在任何方法中都可以轻松获取到该对象
根据共享对象的生成时机不同,选择 initialValue 或 set 来保存对象
场景一中:在 ThreadLocal 第一次 get 的时候把对象给初始化出来,对象的初始化时机可以由我们控制
场景二中:如果需要保存到 ThreadLocal 里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用 ThreadLocal.set 直接放到我们 ThreadLocal 中去,以便后续使用。
原理
搞清楚Thread、ThreadLocal以及ThreadLocalMap三者之间的关系,每个Thread对象中都持有一个ThreadLocalMap成员变量
方法
initialValue( )
该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用 get 的时候,才会触发,ThreadLocal 的 get 方法中调用了 initialValue() 方法
当线程第一次使用 get 方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本 initialValue 方法
通常,每个线程最多调用一次 initialValue 此方法,但如果已经调用了 remove() 后,再调用 get(),则可以再次调用此方法
如果不重写本方法这个方法会返回null。一般使用匿名内部类的方法来重写 initialValue() 方法,以便在后续使用中可以初始化副本对象。
set()
为这个线程设置一个新值
get()
get 方法是先取出当前线程的 ThreadLocalMap,然后调用 map.getEntry 方法,把本 ThreadLocal 的引用作为参数传入,取出map 中属于本 ThreadLocal 的 value
注意,这个 map 以及 map 中的 key 和 value 都是保存在线程中的,而不是保存在 ThreadLocal 中
remove( )
删除对应这个线程的值
initialValue()
是没有默认实现的,如果我们要用 initialValue 方法,需要自己实现,通常是匿名内部类的方式
ThreadLocalMap
ThreadLocalMap 类是每个线程 Thread 类里面的变量,里面最重要的是一个键值对数组 Entry[] table,可以认为是一个 map
键:这个ThreadLocal
值:实际需要的成员变量,比如 user 或者 simpleDateFormat 对象
处理冲突: ThreadLocalMap 这里采用的是线性探测法,也就是如果发生冲突就继续找下一个空位置而不是用链表拉链
内存泄漏
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
什么是内存泄漏:某个对象不再有用,但是占用的内存却不能被回收
Key 的泄漏:ThreadLocalMap 中的 Entry 继承自 WeakReference,是弱弓|用
弱引用的特点是,如果这个对象只被弱引用关联(没有任何强引用关联) , 那么这个对象就可以被回收
所以弱弓|用不会阻止GC ,因此这个弱弓|用的机制
Value的泄漏
ThreadLocalMap 的每个 Entry 都是一个对 key 的弱引用,同时每个 Entry 都包含了一个对 value 的强引|用
正常情况下,当线程终止,保存在 ThreadLocal 里的 value 会被垃圾回收,因为没有任何强引用了
但是,如果线程不终止(比如线程需要保持很久),那么 key 对应的 value 就不能被回收,因为有以下的调用链:
因为 value 和 Thread 之间还存在这个强引用链路,所以导致 value 无法回收,就可能会出现 OOM
JDK 已经考虑到了这个问题,所以在 set, remove, rehash 方法中会扫描 key 为 null 的 Entry,并把对应的 value 设置为 null,这样 value 对象就可以被回收
但是如果一个ThreadLocal 不被使用,那么实际上 set, remove,rehash 方法也不会被调用,如果同时线程又不停止,那么调用链
就一直存在,那么就导致了 value 的内存泄漏
如何避免内存泄露 (阿里规约):调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完 ThreadLocal 之后,应该调用 remove 方法