京东一面:说出ThreadLocal的使用场景及使用方式
扫码关注“后端架构师”,选择“星标”公众号
重磅干货,第一时间送达!
责编:架构君 | 来源:jblog.csdn.net/Mind_programmonkey/article/details/118220731
责编:架构君 | 来源:jblog.csdn.net/Mind_programmonkey/article/details/118220731
上一篇好文:如何优雅处理重复请求/并发请求?
上一篇好文:如何优雅处理重复请求/并发请求?
大家好,我是架构师。
两大使用场景-ThreadLocal的用途
典型场景1: 每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)
典型场景2: 每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。
典型场景1:每个线程需要一个独享的对象
每个Thread内有自己的实例副本,不共享;
举例:SimpleDateFormat。(当多个线程共用这样一个SimpleDateFormat,但是这个类是不安全的)
2个线程分别用自己的SimpleDateFormat,这没问题; 后来延伸出10个,那就有10个线程和10个SimpleDateFormat,这虽然写法不优雅,但勉强可以接受 但是当需求变成了1000,那么必然要用线程池,消耗内存太多; 但是每一个SimpleDateFormat我们都需要创建一遍,那么太耗费new对象了,改成static共用的,所有线程都共用一个simpleDateFormat对象,但这是线程不安全的,容易出现时间一致的情况,在调用的时候,可加锁来解决,但还是不优雅; 用ThreadLocal来解决该问题,给每个线程分配一个simpledateformat,可这个threadlocal是安全的;
package threadlocal;
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();
}
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
// SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.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");
}
};
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
典型场景2:当前用户信息需要被线程内所有方法共享
一个比较繁琐的解决方案是把user作为参数层层传递,从service-1()传到service-2(),以此类推,但是这样做会导致代码冗余且不易维护。 进阶点就是userMap来保存,但是当多线程同时工作时,需要保证线程安全,需要用synchronized,或者concurrenthashmap,但无论用什么,都会对性能有所影响
每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦
用ThreadLocal保存一些业务内存(用户权限信息,从用户系统获取到的用户名、userId等) 这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的 在线程生命周期内,都通过这个静态ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象作为参数传递的麻烦 搜索公众号Java架构师技术后台回复“Spring”,获取一份惊喜礼包。
package threadlocal;
/**
* 描述: 演示ThreadLocal用法2:避免传递参数的麻烦
*/
public class ThreadLocalNormalUsage06 {
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;
}
}
注意点:
强调的是同一个请求内(同一个线程内)不同方法见的共享; 不需重写initialValue()方法,但是必须手动调用set()方法
ThreadLocal方法使用总结
场景一:initialValue
在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机可以由我们控制。
场景二:set
如果需要保存到ThreadLocal里面的对象的生成时机不由我们随意控制。例如拦截器生成的用户信息,用ThreadLocal.set
直接放到ThreadLocal当中。
ThreadLocal原理
理清Thread,ThreadLocalMap以及ThreadLocal
主要方法介绍
T initialValue()
: 初始化void set(T t)
: 为这个线程设置一新值T get()
: 得到这个线程对应的value。如果是首次调用get()。则会调用initialize来得到这个值void remove()
: 删除这个线程得到的值
ThreadLocalMap发生冲突之后,会用线性探测法。
ThreadLocal使用问题内存泄露
什么是内存泄露
某个对象不再有用,但是占用的内存却不能被回收。
Value的泄露
在ThreadLocalMap中的每个Entry都是一个对key的弱引用,同时,每个Entry都包含了一个对value的强引用。 正常情况 ,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了。 但是,如果线程不终止(比如线程池需要保持很久),那么key对应的value就不能被回收。Thread->ThreadLocalMap->Entry(key为Null)->Value 因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能出现OOM;JDK已经考虑到这个问题,所以在set,remove,rehash方法中会扫描key为null,会把value也设置为null,这样value对象就可以被回收了。 但是如果一个ThreadLocal不被使用,那么实际上set,remove,rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄露。
如何避免内存泄露呢?
调用remove方法,就会删除对应的Entry对象,可以避免内存泄露,所以使用完ThreadLocal之后,应该调用remove方法。
两大使用场景-ThreadLocal的用途
典型场景1: 每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)
典型场景2: 每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。
典型场景1:每个线程需要一个独享的对象
每个Thread内有自己的实例副本,不共享;
举例:SimpleDateFormat。(当多个线程共用这样一个SimpleDateFormat,但是这个类是不安全的)
2个线程分别用自己的SimpleDateFormat,这没问题; 后来延伸出10个,那就有10个线程和10个SimpleDateFormat,这虽然写法不优雅,但勉强可以接受 但是当需求变成了1000,那么必然要用线程池,消耗内存太多; 但是每一个SimpleDateFormat我们都需要创建一遍,那么太耗费new对象了,改成static共用的,所有线程都共用一个simpleDateFormat对象,但这是线程不安全的,容易出现时间一致的情况,在调用的时候,可加锁来解决,但还是不优雅; 用ThreadLocal来解决该问题,给每个线程分配一个simpledateformat,可这个threadlocal是安全的;
package threadlocal;
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();
}
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
// SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.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");
}
};
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
典型场景2:当前用户信息需要被线程内所有方法共享
一个比较繁琐的解决方案是把user作为参数层层传递,从service-1()传到service-2(),以此类推,但是这样做会导致代码冗余且不易维护。 进阶点就是userMap来保存,但是当多线程同时工作时,需要保证线程安全,需要用synchronized,或者concurrenthashmap,但无论用什么,都会对性能有所影响
每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦
用ThreadLocal保存一些业务内存(用户权限信息,从用户系统获取到的用户名、userId等) 这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的 在线程生命周期内,都通过这个静态ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象作为参数传递的麻烦 搜索公众号Java架构师技术后台回复“Spring”,获取一份惊喜礼包。
package threadlocal;
/**
* 描述: 演示ThreadLocal用法2:避免传递参数的麻烦
*/
public class ThreadLocalNormalUsage06 {
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;
}
}
注意点:
强调的是同一个请求内(同一个线程内)不同方法见的共享; 不需重写initialValue()方法,但是必须手动调用set()方法
ThreadLocal方法使用总结
场景一:initialValue
在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机可以由我们控制。
场景二:set
如果需要保存到ThreadLocal里面的对象的生成时机不由我们随意控制。例如拦截器生成的用户信息,用ThreadLocal.set
直接放到ThreadLocal当中。
ThreadLocal原理
理清Thread,ThreadLocalMap以及ThreadLocal
主要方法介绍
T initialValue()
: 初始化void set(T t)
: 为这个线程设置一新值T get()
: 得到这个线程对应的value。如果是首次调用get()。则会调用initialize来得到这个值void remove()
: 删除这个线程得到的值
ThreadLocalMap发生冲突之后,会用线性探测法。
ThreadLocal使用问题内存泄露
什么是内存泄露
某个对象不再有用,但是占用的内存却不能被回收。
Value的泄露
在ThreadLocalMap中的每个Entry都是一个对key的弱引用,同时,每个Entry都包含了一个对value的强引用。 正常情况 ,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了。 但是,如果线程不终止(比如线程池需要保持很久),那么key对应的value就不能被回收。Thread->ThreadLocalMap->Entry(key为Null)->Value 因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能出现OOM;JDK已经考虑到这个问题,所以在set,remove,rehash方法中会扫描key为null,会把value也设置为null,这样value对象就可以被回收了。 但是如果一个ThreadLocal不被使用,那么实际上set,remove,rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄露。
如何避免内存泄露呢?
调用remove方法,就会删除对应的Entry对象,可以避免内存泄露,所以使用完ThreadLocal之后,应该调用remove方法。
PS:如果觉得我的分享不错,欢迎大家随手点赞、转发、在看。
版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!
PS:如果觉得我的分享不错,欢迎大家随手点赞、转发、在看。
版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!
END
最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。在这里,我为大家准备了一份2021年最新最全BAT等大厂Java面试经验总结。
别找了,想获取史上最全的Java大厂面试题学习资料
扫下方二维码回复「面试」就好了
历史好文:
还在用分页?太Low !试试 MyBatis 流式查询,真心强大!
扫码关注“后端架构师”,选择“星标”公众号
别找了,想获取史上最全的Java大厂面试题学习资料
扫下方二维码回复「面试」就好了
历史好文:
还在用分页?太Low !试试 MyBatis 流式查询,真心强大!
扫码关注“后端架构师”,选择“星标”公众号