来吃透threadlocal!
1:需要独享对象的非线程安全的对象
常用于例如SimpleDateFormatter这样非线程安全的工具类上,比如需要1000次用到这个工具类,想要不频繁的创建导致的开销,以及高效的避免线程安全问题,就可以用
/**
* @Author:Joseph
* @bolg:https://li-huancheng.gitee.io/
* @Package:threadLocal
* @Project:bing-fa-demo
* @name:ThreadLocalDateTest
* @Date:2023-07-21 15:36
* @Filename:ThreadLocalDateTest
*/
public class ThreadLocalDateTest {
public static ExecutorService theadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int finalI = i;
theadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalDateTest().date(finalI);
System.out.println(date);
}
});
}
}
public String date(int seconds){
//从1970:1。1.0.0 00:00:00 GMT计sh'j
// 因为Data是毫秒单位,标识秒,得乘1000
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);
}
}
class ThreadSafeFormatter{
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd:hh:mm: ss");
}
};
}
例如这个场景,如果不用ThreadLocal,避免多次创建对象开销,那么只能通过synchronized加锁来保证线程安全,但是对应的就是重量级锁的效率问题,那么通过ThreadLocal就能很轻松的完成这个需求,且效率是很快的
2:线程内需要保存全局变量来避免传参麻烦
对于用户信息的传递,比如在interceptor层中,想要传递用户信息 ,首先直接用static是不行的,因为对于不同的线程,都有自己不同的信息,那么可以用一个线程安全的map,比如currentHashMap.让不同的线程存入,用到的时候再去取,即使currentHashMap效率算高了,但是他也是通过AQS cas这些操作来保障的,肯定是影响性能的,那么就可以用ThreadLocal,拦截解密token之后,放到theadLocal,用的时候取就可以了!。
Slf4j
public class LoginInterceptor implements HandlerInterceptor {
public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String accessToken = request.getHeader("token");
if(accessToken == null ){
accessToken = request.getParameter("token");
}
if(StringUtils.isNotBlank(accessToken)){
//不为空
Claims claims = JWTUtil.checkJWT(accessToken);
if(claims == null){
//未登录
CommonUtil.sendJsonMessage(response, JsonData.buildResult(BizCodeEnum.ACCOUNT_UNLOGIN));
return false;
}
long userId= Long.valueOf(claims.get("id").toString());
String headImg = (String)claims.get("head_img");
String name = (String)claims.get("name");
String mail = (String) claims.get("mail");
LoginUser loginUser = LoginUser.builder()
.headImg(headImg)
.name(name)
.id(userId)
.mail(mail).build();
//通过attribute传递用户信息
//request.setAttribute("loginUser",loginUser);
//通过threadlocal传递用户登录信息
threadLocal.set(loginUser);
return true;
}
CommonUtil.sendJsonMessage(response,JsonData.buildResult(BizCodeEnum.ACCOUNT_UNLOGIN));
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
threadLocal.remove();
}
}
这里可以看到使用了,很简单,拦截到set进去就行了,但是注意afterCompletion中的remove(),埋个伏笔,
需要使用的时候
拿出来就好了
这里总结一下ThreadLocal的作用
第一个就是让对象在线程间隔离,每个线程都可以持有自己独立的对象。简单的说,就是同个对象,在不同线程之间是不一样的,并没有重复的创建与销毁,只是创建了线程的数量而已,比如上面1000次,10个线程,那么仅仅创建了10个,对应的就是场景一,对于非线程安全的对象,要多次使用,就可以用theadLocal
第二个就是对于不同的线程(用户),都可以轻松的获取对象,来达到避免传参麻烦
有没有发现,场景一,只用工具类的时候,用的initialValue而场景二用的set方法,原因在于,场景一创建的时间是我们可控的,代码运行到这里,进行初始化就可以创建好,就用InitialValue,set的话是不可控的,加载完也是空的,拦截器拦截到,再set进去,所以就会用set,这就是threadLocal使用的一些注意事项。
1,线程安全
2.不用加锁,效率高
3更好的利用内存,节省开销,比如1000次那个场景,10个线程,对应只消耗10次对象的创建,不同的区分发生在内存的执行上,内存本身就要消耗。
4,不用层层传参
使用会了,现在搞源码!!
我们知道,这个threadLocal的创建是以线程为单位的,每个线程持有一个对象,但是为啥要有threadLocalMap呢?
这是因为一个程序中,不一定只有一个threadLocal,我们再使用的时候,有可能用了一个拦截器的set,也用了一个工具类,就是使用中的两种情况,这就需要两个threadlocal
这是Thread中的,thread持有thredLoalMap,并命名为threadLocals对象
第一个就是initalValue了,这个方法提供初始化,但是是延迟加载的,在get的时候才会触发
现在我们看一下这个方法,进入ThreadLocal类中
可以看到这个是return null,的 我们重写,才会有值
延迟加载,看一下get方法
这里的value就是我们重写的initialValue,不重写是null
当场景2,就是用set的时候,这个initialValue是不会取执行的
retrun的是我们set近ThreadLocalMap中的值
initaialValue方法还需要我们注意的是,只会在第一次调用get会触发,后面就不会触发了,会和set一样,进入if(map!=null)的逻辑,值得注意的是,当我们remove之后,threadLoalMap又空了,就可以触发了,
set方法
这个就是为线程设置一个新的值,
这个就比较简单了,拿到当前线程,再取找thradLocalMap找不到就去创建,找到就set进去
值得注意的是:threadLocal类中只是一些操作的方法,具体的保存,都是再threadLocalMap中的,也就是说,都是保存再线程中的,threadLocalMap中,key是threadLocal,value是set进去的值!!
get
就是拿到线程对应的value,threadLocalMap是以key-value为键值对的集合,这里指的就是对应threadLocal中的value
代码是这样的
就是从拿到threadLocalMap中存储我这个threadLocal的value
这里的map是thread类中的threadLocalmap,map中通过threadLocal作为key,放进去找到对应的value
remove方法就是移除value
这个就很简单了
都是一个套路。就是通过当前线程,拿到threadLocalMap,然后讲threadLocal作为key去remove数据
注意这个remove的key是当前的threadLocaL,并不是所有的threadLocal的value
这里有必要着重分析一下这个类,是整个threadLocal实现的核心
刚才我讲的,多多少少提到了
threadLocalMap指的就是thread类中的threadLocals,类型是ThreadLocal.thredLocalMap,也就是说,这个threadLoaclMap定义是在threadLocal中的,但是threadLocalMap和threadLocal使用是被thread类持有的,thread中持有threadLocalMap,threadLocalMap的key是threadLocal,
这个关系比较混乱,我们可以把它理解为一个hashMap但是略有区别
它发生hash冲突不是用拉链法,而是用线性探测法,冲突了,就在table中遍历往下找
通过源码分析
我们可以理解,initialValue和set都是通过map.set放value的,只不过initialVlaue起点是get方法,map为空,调用initial方法的罢了
很多人都不容为什么拦截器的最后,要remove一下,Jvm垃圾回收器不是可以回收吗?
这里先理清一下内存泄漏和内存溢出的区别
内存泄漏指的是:对象不使用了,应该被回收,但是没被回收
内存溢出指的是:就是申请的内存不够用,造成outofMemory
再讲一下:四大引用
强软弱虚
强引用,Reference
这个是很普遍的,
比如String s=''ss'',在内存不足的时候,jvm宁愿抛出内存溢出oom,也不会回收这个对象,只有在这个jvm进程结束才会回收
软引用:Software
这里就是再降低一些要求,内存资源够的时候,触发GC也不会回收,但是内存不够了,就会回收,指的就是有用,但不是非要不可的地步
弱引用:WeakReference
用完就没用了的东西,用过了,gc的时候一定会被回收
虚引用就用的很少了,目的是让系统知道这个对象被回收
引用类型 | 被垃圾回收时刻 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | JVM停止运行时终止 |
软引用 | 在内存不足时 | 对象简单,缓存,文件缓存,图片缓存 | 内存不足时终止 |
弱引用 | 在gc垃圾回收时 | 对象简单,缓存,文件缓存,图片缓存 | gc运行后终止 |
虚引用 | 任何时候都可能被垃圾回收器回收 | 基本不写,虚拟机使用, 用来跟踪对象被垃圾回收器回收的活动 | 未知 |
我们看一下threadLocalMap
注意这里k的赋值,并不是直接赋值,而是super了一个key,也就是虚引用,这个再发生垃圾回收是可以回收的,
但是这个value是强引用
我们知道,线程执行完就会消失,那么
Thread->ThreadLocalMap->Entry(key=null)->value
这个调用链,发生gc之后,key消失了,但是value还在,当线程执行完毕后,value会随着Threa生命的终止,也会丢失调用链路,可达性分析算法,大家可以了解,这样也是可以回收的
但是当我们用线程池的时候,一个线程是重复利用的,比如拦截器这里,我这个线程处理完一个用户,再处理下一个,那么这里的threadLocal和value是会很多的,value强引用,是不会被回收的,因为线程池服用线程,进程不会结束,
jdk考虑到了这个问题
remove\resize\set方法,会扫描为null的,并设置为null
比如这里,k为null,就会让value=null,注释也写了,help the gc,
但是当一个threadLocal不再被使用,那么这些方法也无法调用,
所有就需要我们再threadLocal结束前,要去提前remove
现在知道这个伏笔了吧!
内存泄漏是threadLocal的一个坑,同样还有
可能听过这样一句话,threadLocal用的时候要set,不然get的时候会报空指针异常 这个问题,简直不要太离谱。我们知道,initialValue方法,即使不调用,也是会有一个默认的null,的,顶多get到一个null,那么为什么有人会遇到get下,报空指针异常问题呢?这下,你就得听我好好唠唠了,不然真开发中遇到,排错很麻烦的
**
* @Author:Joseph
* @bolg:https://li-huancheng.gitee.io/
* @Package:threadLocal
* @Project:bing-fa-demo
* @name:ThreadLoaclNullPointer
* @Date:2023-07-23 11:59
* @Filename:ThreadLoaclNullPointer
*/
public class ThreadLoaclNullPointer {
ThreadLocal<Long> longThreadLocal= new ThreadLocal<Long>();
public void set(){
longThreadLocal.set(Thread.currentThread().getId());
}
public long get(){
return longThreadLocal.get();
}
public static void main(String[] args) {
ThreadLoaclNullPointer threadLoaclNullPointer = new ThreadLoaclNullPointer();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(threadLoaclNullPointer.get());
}
});
thread.start();
}
}
没错,报错了!
你能发现那里的错误吗,先别看答案,自己copy代码debug一下再看答案,
答案就是
自动拆箱,出现了空指针异常,拆箱的时候,null是无法拆箱的
//修改为Long
public Long get(){
return longThreadLocal.get();
}
这样就正常了,这是我们需要注意的地方
threadLocal使用,还有注意点
static对象,当我们set的时候,还是共享的,因为static本身类变量就是共享的,我们想用直接类.static对象就可以了,不需要放到threadLocal中
另外没必要用theadLocal就不要用了,比如在场景一中,只需要用两三次的工具类,直接new三次对象就行了
spring中用到threadLocal也是很多的,XXXContextHolder
比如RequestContextHolder
进入
只是对threadLocal做了层小小的包装,取名字而已
比如这个方法,就是把请求参数返回,也是从threadLocal中去取
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有