Java ThreadLocal是什么

文章目录

  • 引子:SimpleDateFormat类
  • ThreadLocal是什么
  • ThreadLocal 的另一个用途
  • **总结**ThreadLocal的两大用途
  • ThreadLocal 的源代码
  • ThreadLocalMap
  • ThreadLocalMap 的问题
  • ThreadLocal的key为什么设置成弱引用?value为什么不是弱引用?
  • Thread、ThreadLocal、ThreadLocalMap 的关系
  • ThreadLocal带来的性能提升
  • ThreadLocalRandom类
  • 参考

引子:SimpleDateFormat类

为什么SimpleDateFormat 是线程不安全的?

  • 这主要是因为,它内部使用了一个全局的 Calendar 变量,来存储 date 信息

解决方式:

  1. 在sdf.parse()前后加锁,这也是我们一般的处理思路。
  2. 手动改造代码,给每个线程都new一个SimpleDateFormat
  3. 使用JDK的ThreadLocal类(和2的思想相似)

2代码如下

public class ThreadSafeSDFUsingMap {private Map<Long, SimpleDateFormat> sdfMap = new ConcurrentHashMap();//key 是线程 ID,value 是 SimpleDateFormatpublic String formatIt(Date date) {Thread currentThread = Thread.currentThread();long threadId = currentThread.getId();SimpleDateFormat sdf = sdfMap.get(threadId);if (null == sdf) {sdf = new SimpleDateFormat("yyyyMMdd HHmm");sdfMap.put(threadId, sdf);}return sdf.format(date);}
}

3代码如下

static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();public static class ParseDate implements Runnable {int i = 0;public ParseDate(int i) {this.i = i;}public void run() {try {if (tl.get() == null) {tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));}Date t = tl.get().parse("2015-03-29 19:29:" + i % 60);System.out.println(i + ":" + t);} catch (ParseException e) {e.printStackTrace();}}
}

从这里也可以看到,为每一个线程人手分配一个对象的工作并不是由ThreadLocal来完成的,而是需要在应用层面保证的。如果在应用上为每一个线程分配了相同的对象实例,那么ThreadLocal也不能保证线程安全。这点也需要大家注意。

ThreadLocal是什么

  • 用于创建线程的本地变量。可以使用get()\set()方法去获取他们的默认值或者在线程内部改变他们的值
  • 简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了

ThreadLocal 的另一个用途

  • ThreadLocal 还有另一个用途,那就是保存线程上下文信息。这一点在很多框架乃至JDK类加载中都有用到。
  • 比如Spring的事务管理,方法A里头调用了方法B,方法B如果失败了,需要执行connection.rollback()来回滚事务。那么方法B怎么知道connection是哪个?最简单的就是方法A在调用方法B时,把connection对象传进去
  • 显然,这样很挫,需要修改方法的定义
  • 不过你现在知道ThreadLocal了,只需把connection塞入threadLocal,methodB和methodA在一个线程中执行,那么自然,methodB可以获取到和methodA相同的connection。
  • Spring 采用 Threadlocal 的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,具体可以参考Spring的TransactionSynchronizationManager类

总结ThreadLocal的两大用途

  1. 实现线程安全;
  2. 保存线程上下文信息;

当然ThreadLocal肯定还有更多的用途,只要我们弄懂了它的原理,就知道如何灵活使用。

ThreadLocal 的源代码

设置到ThreadLocal中的数据,写入了threadLocals这个Map。其中,key为ThreadLocal当前对象,value就是我们需要的值。而threadLocals本身就保存了当前自己所在线程的所有“局部变量”,也就是一个ThreadLocal变量的集合

在set时,首先获得当前线程对象,然后通过getMap()拿到线程的ThreadLocalMap,并将值设入ThreadLocalMap中

get()方法先取得当前线程的ThreadLocalMap对象。然后,通过将自己作为key取得内部的实际数据

public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
}public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null)return (T)e.value;}return setInitialValue();
}ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}

ThreadLocalMap

  • ThreadLocalMap 是 ThreadLocal 的内部类,没有实现 Map 接口,用独立的方式实现了 Map 的功能,其内部的 Entry 也独立实现。
  • 在 ThreadLocalMap 中,也是用 Entry 来保存 K-V 结构数据的。但是 Entry 中 key 只能是 ThreadLocal 对象 ,这点被 Entry 的构造方法已经限定死了。
  • 和 HashMap 的最大的不同在于,ThreadLocalMap 结构非常简单,没有 next 引用,也就是说 ThreadLocalMap 中解决 Hash 冲突的方式并非链表的方式,而是采用线性探测的方式:根据初始 key 的 hashcode 值确定元素在数组中的位置,如果发现这个位置上已经有其他 key 值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
  • 显然 ThreadLocalMap 采用线性探测的方式解决 Hash 冲突的效率很低,如果有大量不同的 ThreadLocal 对象放入 map 中时发生冲突,或者发生二次冲突,则效率很低。
  • 所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到 map 中的 Key 都是相同的 ThreadLocal,如果一个线程要保存多个变量,就需要创建多个 ThreadLocal,多个 ThreadLocal 放入 Map 中时会极大的增加 Hash 冲突的可能。

ThreadLocalMap 的问题

  • 由于 ThreadLocalMap 的 key 是引用,而 Value 是强引用。
  • 这就导致了一个问题,ThreadLocal 在没有外部对象强引用时,发生 GC 时弱引用 Key 会被回收,而 Value 不会回收,如果创建 ThreadLocal 的线程一直持续运行,那么这个 Entry 对象中的 value 就有可能一直得不到回收,发生内存泄露

如何避免泄漏

  • 既然 Value 是强引用,那么我们要做的事,就是在调用 ThreadLocal 的 get()、set() 方法时完成后再调用 remove 方法,将 Entry 节点和 Map 的引用关系移除,这样整个 Entry 对象在 GC Roots 分析后就变成不可达了,下次 GC 的时候就可以被回收。
  • 如果使用 ThreadLocal 的 set 方法之后,没有显示的调用 remove 方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完 ThreadLocal 之后,记得调用 remove 方法

ThreadLocal的key为什么设置成弱引用?value为什么不是弱引用?

假如key对ThreadLocal对象的弱引用,改为强引用。

即使ThreadLocal变量生命周期完了,设置成null了,但由于Entry中的key对ThreadLocal还是强引用。

就会存在这样的强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。

假设该线程一直存在,那么,ThreadLocal对象和ThreadLocalMap都将不会被GC回收,于是产生了内存泄露问题。

但如果是弱引用,当ThreadLocal变量=null之后(强引用没了),只剩下key的弱引用,gc就会自动回收ThreadLocal对象

所以这里要理解的是:如果强引用和弱引用同时引用一个对象,那么这个对象不会被GC回收


Entry的value一般只被Entry引用,有可能没被业务系统中的其他地方引用。如果将value改成了弱引用,被GC贸然回收了(数据突然没了),可能会导致业务系统出现异常。

参考:threadlocal value为什么不是弱引用?

Thread、ThreadLocal、ThreadLocalMap 的关系

在这里插入图片描述

ThreadLocal带来的性能提升

多线程下产生随机数的性能对比(多线程共用同一个Random类、使用ThreadLocal实现每个线程各自持有一个Random类)

注意Random类是线程安全的

package com.space.java.juc.threadlocal;import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;public class RandomDemo {public static final int GEN_COUNT = 10000000;//生成随机数的数量public static final int THREAD_COUNT = 4;//线程数量static ExecutorService exe = Executors.newFixedThreadPool(THREAD_COUNT);public static Random rnd = new Random(123);public static ThreadLocal<Random> tRnd = new ThreadLocal<Random>() {// 当ThreadLocal get为null时,会调用此方法初始化@Overrideprotected Random initialValue() {return new Random(123);}};public static class RndTask implements Callable<Long> {private int mode = 0;public RndTask(int mode) {this.mode = mode;}public Random getRandom() {if (mode == 0) {return rnd;} else if (mode == 1) {return tRnd.get();} else {return null;}}@Overridepublic Long call() {long b = System.currentTimeMillis();for (long i = 0; i < GEN_COUNT; i++) {getRandom().nextInt();}long e = System.currentTimeMillis();System.out.println(Thread.currentThread().getName() + " spend " + (e - b) + "ms");return e - b;}}public static void main(String[] args) throws InterruptedException, ExecutionException {Future<Long>[] futs = new Future[THREAD_COUNT];for (int i = 0; i < THREAD_COUNT; i++) {futs[i] = exe.submit(new RndTask(0));}long totaltime = 0;for (int i = 0; i < THREAD_COUNT; i++) {totaltime += futs[i].get();}System.out.println("多线程访问同一个Random实例:" + totaltime + "ms");// ThreadLocal的情况for (int i = 0; i < THREAD_COUNT; i++) {futs[i] = exe.submit(new RndTask(1));}totaltime = 0;for (int i = 0; i < THREAD_COUNT; i++) {totaltime += futs[i].get();}System.out.println("使用ThreadLocal包装Random实例:" + totaltime + "ms");exe.shutdown();}
}

ThreadLocalRandom类

Random是线程安全的,但由于内部使用cas机制,导致它不是高并发的

而ThreadLocalRandom是一个性能强悍的高并发随机数生成器,ThreadLocalRandom继承自Random

和ThreadLocal的原理类似,Thread类内部有一些变量专门用于让随机数生成器只访问本地线程数据,从而避免竞争

基本使用方法:

public static void main(String[] args) {for (int i = 0; i < 10; i++) {new Player().start();}
}private static class Player extends Thread {@Overridepublic void run() {System.out.println(getName() + ": " + ThreadLocalRandom.current().nextInt(100));}
}

用于生成随机订单编号

private static final SimpleDateFormat dateFormatOne=new SimpleDateFormat("yyyyMMddHHmmssSS");private static final ThreadLocalRandom random=ThreadLocalRandom.current();/*** 生成订单编号-方式一* @return*/
public static String generateOrderCode(){//时间戳+N为随机数流水号return dateFormatOne.format(DateTime.now().toDate()) + generateNumber(4);
}//N为随机数流水号
public static String generateNumber(final int num){StringBuffer sb=new StringBuffer();for (int i=1;i<=num;i++){sb.append(random.nextInt(9));}return sb.toString();
}

参考

  • 《实战Java高并发程序设计》4.3节
  • 拼多多面试官没想到ThreadLocal我用得这么溜,人直接傻掉
  • threadlocal value为什么不是弱引用?
  • 高并发情况下你还在用Random生成随机数?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/62274.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring MVC【一篇搞定】

Spring MVC 文章目录 Spring MVC一、什么是 Spring MVC二、介绍MVC2.1、Spring MVC 和 MVC 之间的关系 三、创建 Spring MVC四、掌握 Spring MVC 的核心 ☆☆☆☆4.1、Spring 热部署4.2、实现用户与程序的连接 ☆4.2.1、RequestMapping4.2.2、GetMapping/PostMapping 4.3、获取…

Mask RCNN网络结构以及整体流程的详细解读

文章目录 1、概述2、Backbone3、RPN网络3.1、anchor的生成3.2、anchor的标注/分配3.3、分类预测和bbox回归3.4、NMS生成最终的anchor 4、ROI Head4.1、ROI Align4.2、cls head和bbox head4.3、mask head 1、概述 Mask RCNN是在Faster RCNN的基础上增加了mask head用于实例分割…

国外问卷调查项目赚美刀,回答一个问题赚10美刀

科思创业汇 大家好&#xff0c;这里是科思创业汇&#xff0c;一个轻资产创业孵化平台。赚钱的方式有很多种&#xff0c;我希望在科思创业汇能够给你带来最快乐的那一种&#xff01; 不要胡说八道&#xff0c;只谈干货 专注于网络创业&#xff0c;赚钱实战项目&#xff0c;信…

CentOS 7 构建 LVS-DR 群集 nginx负载均衡

1、基于 CentOS 7 构建 LVS-DR 群集。 DS&#xff08;Director Server&#xff09;&#xff1a;DIP 192.168.231.132 & VIP 192.168.231.200 [root132 ~]# nmcli c show NAME UUID TYPE DEVICE ens33 c89f4a1a-d61b-4f24-a260…

gin的占位符:和通配符*

1、用法 在 Gin 路由中&#xff0c;可以使用一个通配符&#xff08;*&#xff09;或一个占位符&#xff08;:&#xff09;来捕获 URL 的一部分。 r.GET("/royal/:id", func(c *gin.Context) {id : c.Param("id")//fmt.Println("into :id")c.Str…

【HarmonyOS】Java如何引用外部jar包

【关键字】 Java、引用jar包​ 【写在前面】 使用API6和API7开发HarmonyOS应用时&#xff0c;因为应用中只能引用SDK中开放的功能接口&#xff0c;但是部分jdk自带的接口功能在SDK中并未封装&#xff0c;要想在工程中使用jdk开放的接口功能&#xff0c;需要将jdk中的jar包通过…

C语言参悟-数据类型

C语言的数据类型 一、概述二、基础数据类型1. 整数1. 计算2. 索引 2. 浮点数3. 字符4. 字符串5. 指针 三、特殊数据类型1. 枚举2. 共用体2. struct结构体 四、数据类型修饰符1. const2. unsigned、signed 一、概述 编程语言为抽象这个物理世界提供了依据&#xff0c;其中对于描…

桂林小程序https证书

现在很多APP都相继推出了小程序&#xff0c;比如微信小程序、百度小程序等&#xff0c;这些小程序的功能也越来越复杂&#xff0c;不可避免的和网站一样会传输数据&#xff0c;因此小程序想要上线就要保证信息传输的安全性&#xff0c;也就是说各种类型的小程序也需要部署https…

个推消息推送专项运营提升方案,基于AIGC实现推送文案智能生成

个推消息推送专项运营提升方案自今年3月份发布以来&#xff0c;已应用于游戏社交、影音资讯、电商购物等多个行业。现个推消息推送专项运营提升方案又实现了推送策略的智能化和推送流程的自动化&#xff0c;助力APP进一步提升消息推送的效率和效果。 丰富推送策略组合&#xf…

【Spring Boot】Thymeleaf模板引擎 — Thymeleaf页面布局

Thymeleaf页面布局 熟悉Thymeleaf的语法和表达式后&#xff0c;后面开发起来会更加得心应手。接下来好好研究一下Thymeleaf如何实现完整的Web系统页面布局。 1.引入代码片段 在模板中经常希望包含来自其他模板页面的内容&#xff0c;如页脚、页眉、菜单等。为了做到这一点&a…

华熙生物肌活:2023年版Bio-MESO肌活油性皮肤科学护肤指南

关于报告的所有内容&#xff0c;公众【营销人星球】获取下载查看 核心观点 以悦己和尝鲜为消费动机的他们&#xff0c;已迅速崛起成为护肤行业的焦点人群。而在新生代护肤议题中&#xff0c;“油性皮肤护理”已经成为一个至关重要的子集。今天&#xff0c;中国新生代人口数量…