Java面试-ThreadLocal你知道多少?

最近做的一个项目中用到了ThreadLocal,在***中获取并存储访问请求的head中存储的用户的信息,以便进行方法级别的权限验证。用了感觉一知半解的,空闲了查询了许多的资料,并参考了慕课网的《玩转Java并发工具》课程,发现ThreadLocal是面试中很容易考到的并发类,于是将ThreadLocal相关的内容整理并记录于此,方便自己和同样准备找工作的同学学习。

典型应用场景

场景1:每个线程需要一个独享的对象
  • 通常应用在线程不安全的工具类,如SimpleDateFormat,Random
  • 每个Thread内有自己的实例副本,不共享
  • 比喻:课本只有一本,一群人同时做笔记会发生冲突有线程安全问题。把课本复印成一人一本就没问题了

案例内容:

编写一个函数,计算1970年1.1 08:00:00 GMT后 seconds 秒后的时间,假设是1000个线程进行调用

  • 方案1
public class ThreadLocalNormalUsage02 { 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 ThreadLocalNormalUsage02().date(finalI); System.out.println(date); } }); } threadPool.shutdown(); } //获取1970年1.1 08:00:00 GMT后 seconds 的时间 public String date(int seconds) { //参数的单位是毫秒,从1970年1.1 08:00:00 gmt计时 Date date = new Date(1000 * seconds); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); return dateFormat.format(date); } } 

这种方案存在一个问题,每次调用都需要创建一个SimpleDateFormat 对象,消耗太大了,有没有解决的方案呢

  • 方案2:
    将SimpleDateFormat 对象抽出来作为静态变量
public static SimpleDateFormat dateFormat ; //获取1970年1.1 08:00:00 GMT后 seconds 的时间 public String date(int seconds) { //参数的单位是毫秒,从1970年1.1 08:00:00 gmt计时 Date date = new Date(1000 * seconds); return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(date); } 

新的问题又出现了,运行结果存在相同的值,发生了线程安全问题

  • 方案3:
    将使用SimpleDateFormat对象的代码锁起来
public String date(int seconds) { //参数的单位是毫秒,从1970年1.1 08:00:00 gmt计时 Date date = new Date(1000 * seconds); String s; synchronized (ThreadLocalNormalUsage03.class){ s = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(date); } return s; } 

这种情况下还存在问题,在高并发下每个线程都需要排队获取,效率低,不适用

  • 方案4:利用ThreadLocal再次升级实现,线程安全且能并行执行
//获取1970年1.1 08:00:00 GMT后 seconds 的时间 public String date(int seconds) { //参数的单位是毫秒,从1970年1.1 08:00:00 gmt计时 Date date = new Date(1000 * seconds); SimpleDateFormat dateFormat = ThreadStatrFormatter.dateFormatThreadLocal.get(); return dateFormat.format(date); } class ThreadStatrFormatter { public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); } }; } 
场景2:当前用户信息需要被线程内所有方法共享

在应用开发中,有些参数需要被线程内许多方法使用,如权限管理,很多的方法都需要验证当前线程用户的身份信息

案例内容:一个系统中,user对象需要在很多server中进行使用

  • 方案1
    将user作为参数层层传递,从service1->service2->service3以此类推。这样会导致代码冗余难以维护

  • 方案2
    定义一个全局的static 的user,想要拿的时候直接获取。
    这是一种错误的方案!!
    因为我们现在的场景是多用户的系统,每个线程对应着不同的用户,每个线程的user是不同的

  • 方案3
    定义一个UserMap,每次访问从Map中获取用户的信息,多线程访问下加锁或者使用ConcurrentHashMap,但是对性能有影响

  • 方案4
    利用ThreadLocal,不需要锁,不影响性能。
    强调的是同一个请求内不同方法间的共享

    代码演示:

/** * 避免传递参数的麻烦 * ThreadLocalan案例2 * @author Chkl * @create 2020/3/10 * @since 1.0.0 */ public class ThreadLocalNormalUsage06 { public static void main(String[] args) { new Service1().process(); } } class Service1 { public void process() { User user = new User("张三"); UserContextHolder.holder.set(user); new Service2().process(); } } class Service2 { public void process() { User user = UserContextHolder.holder.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); } } class UserContextHolder { public static ThreadLocal<User> holder = new ThreadLocal<>(); } class User { String name; public User(String name) { this.name = name; } } 

ThreadLocal的两个作用

  • 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
  • 同一线程中,在任何方法中都可以轻松获取到该对象

两种初始化方法使用场景

  • 场景1:initialValue
    如果在ThreadLocal第一次get的时候把对象给初始化时使用,对象的初始化时机受控制

  • 场景2:set
    如果需要保存到ThreadLocal的对象的生成时机不由我们随意控制,如访问***生成用户信息的情况下使用

使用ThreadLocal的好处

  • 线程安全
  • 不需要加锁,执行效率高
  • 更高效的利用内存,节省开销
  • 避免传参的繁琐操作

ThreadLocal与Thread的关系

一张图搞懂Thread,ThreadLocal,ThreadLocalMap三者的关系:

每个Thread对象都持有一个ThreadLocalMap成员变量
查看Thread的源码也可以发现确实存在这样一个变量

ThreadLocal的重要方法

  • initialValue()

    • 该方法返回当前线程对应的初始值,使用了延迟加载,当调用get()方法是才会触发
    • 当第一次使用get()方法时会调用此方法,如果调用前用set()方法设置了值就不会调用
    • 当调用remove()方法后再次调用get()方法依然会调用initialize
    • 如果不重写initialValue方法,直接调用get()会返回null
  • set() 为线程设置新的值

  • get()

    • 得到线程对应的value,如果首次调用,则会调用initialize
    • get方法是先取出当前线程的ThreadLocalMap,再通过map.getEntry(ThreadLocal)方法将本ThreadLocal的引用作为参数传入获取ThreadLocal的值
    • ThreadlocalMap这个Map是存放在Thread中而不是ThreadLocal中
  • remove() 删除线程所保持的值

    • remove()方法也是在ThreadlocalMap中进行操作,传入当前ThreadLocal对象的引用,删除m ap中的value的值

ThreadLocal注意点

  • 最后一次使用之后应该手动的调用remove()方法,防止内存溢出
  • 如果可以不使用ThreadLocal就解决问题,不要强行使用(如:任务数很少时)
  • 优先使用框架的支持,而不是自己创造
    Spring中,如果可以使用RequestContextHolder就不要用ThreadLocal

ThreadLocal 为什么会发生内存溢出?

ThreadLocal的存储实际是把当前线程作为key,存储数据当做value存储在ThreadLocalMap(内部实现为Entry)中,key使用的是弱引用,而value使用的是强引用。当任务执行结束后因为value没有回收导致数据不会被GC处理,会一直存在于线程中,积累到足够多就会发生内存溢出

如何解决内存溢出

最后一次使用之后应该手动的调用remove()方法

针对ThreadLocal 的内存溢出,也有相应的操作去解决,当调用set(),remove()方法时,会对没有使用的键值对进行处理(方便GC回收),所以操作完成后需要手动的对ThreadLocal 进行处理,调用threadLocal.remove()


本文整理自慕课网《玩转Java并发工具》


更多Java面试复习笔记和总结可访问我的面试复习专栏《Java面试复习笔记》,或者访问我另一篇博客《Java面试核心知识点汇总》查看目录和直达链接

全部评论

相关推荐

自由水:这HR已经很好了,多的是已读不回和不读了
点赞 评论 收藏
分享
避坑恶心到我了大家好,今天我想跟大家聊聊我在成都千子成智能科技有限公司(以下简称千子成)的求职经历,希望能给大家一些参考。千子成的母公司是“同创主悦”,主要经营各种产品,比如菜刀、POS机、电话卡等等。听起来是不是有点像地推销售公司?没错,就是那种类型的公司。我当时刚毕业,急需一份临时工作,所以在BOSS上看到了千子成的招聘信息。他们承诺无责底薪5000元,还包住宿,这吸引了我。面试的时候,HR也说了同样的话,感觉挺靠谱的。于是,我满怀期待地等待结果。结果出来后,我通过了面试,第二天就收到了试岗通知。试岗的内容就是地推销售,公司划定一个区域,然后你就得见人就问,问店铺、问路人,一直问到他们有意向为止。如果他们有兴趣,你就得摇同事帮忙推动,促进成交。说说一天的工作安排吧。工作时间是从早上8:30到晚上18:30。早上7点有人叫你起床,收拾后去公司,然后唱歌跳舞(销售公司都这样),7:55早课(类似宣誓),8:05同事间联系销售话术,8:15分享销售技巧,8:30经理训话。9:20左右从公司下市场,公交、地铁、自行车自费。到了市场大概10点左右,开始地推工作。中午吃饭时间大约是12:00,公司附近的路边盖饭面馆店自费AA,吃饭时间大约40分钟左右。吃完饭后继续地推工作,没有所谓的固定中午午休时间。下午6点下班后返回公司,不能直接下班,需要与同事交流话术,经理讲话洗脑。正常情况下9点下班。整个上班的一天中,早上到公司就是站着的,到晚上下班前都是站着。每天步数2万步以上。公司员工没有自己的工位,百来号人挤在一个20平方米的空间里听经理洗脑。白天就在市场上奔波,公司的投入成本几乎只有租金和工资,没有中央空调。早上2小时,晚上加班2小时,纯蒸桑拿。没有任何福利,节假日也没有3倍工资之类的。偶尔会有冲的酸梅汤和西瓜什么的。公司的晋升路径也很有意思:新人—组长—领队—主管—副经理—经理。要求是业绩和团队人数,类似传销模式,把人留下来。新人不能加微信、不能吐槽公司、不能有负面情绪、不能谈恋爱、不能说累。在公司没有任何坐的地方,不能依墙而坐。早上吃早饭在公司外面的安全通道,未到上班时间还会让你吃快些不能磨蹭。总之就是想榨干你。复试的时候,带你的师傅会给你营造一个钱多事少离家近的工作氛围,吹嘘工资有多高、还能吹自己毕业于好大学。然后让你早点来公司、无偿加班、抓住你可能不会走的心思进一步压榨你。总之,大家在找工作的时候一定要擦亮眼睛,避免踩坑!———来自网友
qq乃乃好喝到咩噗茶:不要做没有专业门槛的工作
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务