JUC系列之ThreadLocal 的使用
JUC系列文章目录
JUC系列往期文章
文章目录
- JUC系列文章目录
- 前言
- 一、ThreadLocal与Synchronized的区别
- 二、ThreadLocal的使用及原理
- 1、ThreadLocal的两种用法
- 2、ThreadLocal源码分析
- 2、ThreadMap源码分析
- 3、ThreadLocal只能当前线程可见吗
- 三、ThreadLocal的内存泄漏
前言
ThreadLocal提供了线程的局部变量,该变量对其他线程而言是隔离的,每个线程都可以通过set()和get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。
一、ThreadLocal与Synchronized的区别
Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问,多线程同时操作共享变量并且能正确的输出结果;
ThreadLocal是为每一个线程都提供了变量的副本,把共享变量变成线程私有了,每个线程都有独立的一个变量。ThreadLocal的实例和他的值仍然是放到在堆的。ThreadLocal对象也是对象,对象就在堆。只是JVM通过一些技巧将其可见性变成了线程可见。
二、ThreadLocal的使用及原理
如下源码,每一个thread中都有一个ThreadMap对象维护着线程中的threadLocals变量,具体可以看上面的图解。每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。
ThreadLocalMap getMap(Thread t) {return t.threadLocals;}public class Thread implements Runnable {……/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null;/** InheritableThreadLocal values pertaining to this thread. This map is* maintained by the InheritableThreadLocal class.*/ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;……
那么怎样去使用ThreadLocal呢
1、ThreadLocal的两种用法
第一种用法:每个线程需要有一个独享的对象,比如工具类SimpleDateFormat。如下代码创建了10个核心线程数的线程池,并通过重写ThreadLocal中的initialValue方法给每一个线程新建SimpleDateFormat副本。
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() {@Overridepublic 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 = ThreadSafeFormatter.dateFormatThreadLocal.get();return dateFormat.format(date);}
}class ThreadSafeFormatter {public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");}};
}
第二种用法:每个线程内需要保存全局变量,可以让不同的业务使用,比如获取用户的session信息,让不同的service使用,避免参数需要一直传递导致代码的冗余。主要是强调同一个请求内,不同方法的共享。
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;}
}
2、ThreadLocal源码分析
get()方法源码如下所示,如果第一次调用get,ThreadLocalMap中的值为null,说明在这之前没有set进去数据,那么会调用setInitialValue()方法,在setInitialValue()方法中会调用initialValue()方法也就是我们在第一种用法中重写的方法,如果没有重写默认返回null。
public T get() {//1、获取当前线程Thread t = Thread.currentThread();//2、获取当前线程的ThreadLocalMapThreadLocalMap map = getMap(t);//3、如果map数据不为空,if (map != null) {//3.1、获取threalLocalMap中存储的值ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为nullreturn setInitialValue();}private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;}
如下源码,在第二种用法中如果没有初始化,进行了set()方法,set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。
public void set(T value) {//1、获取当前线程Thread t = Thread.currentThread();//2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,//则直接更新要保存的变量值,否则创建threadLocalMap,并赋值ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);else// 初始化thradLocalMap 并赋值createMap(t, value);}
2、ThreadMap源码分析
ThreadLocalMap是ThreadLocal的内部静态类,并未实现Map接口,它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value
static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}……}
为什么需要数组呢?
一个线程可以有多个TreadLocal来存放不同变量,但是他们都会放到当前线程的ThreadLocalMap里,所以肯定要数组来存。但是这里ThreadLocalMap和我们以前用的hashmap不一样,它是怎么解决哈希冲突的呢。
private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();if (k == key) {e.value = value;return;}if (k == null) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}
从上我们可以看到,ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode。在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i。如果当前位置是空的,就初始化一个Entry对象放在位置i上。如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value。如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。
在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。
private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);}private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;
// get的时候一样是根据ThreadLocal获取到table的i值,然后查找数据拿到后会对比key是否相等 if (e != null && e.get() == key)。while (e != null) {ThreadLocal<?> k = e.get();// 相等就直接返回,不相等就继续查找,找到相等位置。if (k == key)return e;if (k == null)expungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}return null;}
3、ThreadLocal只能当前线程可见吗
使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。
三、ThreadLocal的内存泄漏
ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用。
关于弱引用是这样介绍的
只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的。这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。
ThreadLocal其实是与线程绑定的一个变量,如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。
想要解决这个问题,在代码的最后使用remove()方法就好了。remove方法就是找到对应的值全部置空,这样在垃圾回收器回收的时候,会自动把他们回收掉。
那为什么要把key设置成弱引用呢
在上面ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的。这就意味着当前线程依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障,从而避免内存泄漏。
JUC系列之ThreadLocal 的使用相关推荐
- 突击并发编程JUC系列-ReentrantReadWriteLock
突击并发编程JUC系列演示代码地址: https://github.com/mtcarpenter/JavaTutorial 本章节将学习 ReentrantReadWriteLock(读写锁),Re ...
- AQS源码二探-JUC系列
Python微信订餐小程序课程视频 https://edu.csdn.net/course/detail/36074 Python实战量化交易理财系统 https://edu.csdn.net/cou ...
- JUC系列(一)什么是JUC?
多线程一直Java开发中的难点,也是面试中的常客,趁着还有时间,打算巩固一下JUC方面知识,我想机会随处可见,但始终都是留给有准备的人的,希望我们都能加油!!! 沉下去,再浮上来,我想我们会变的不一样 ...
- JUC系列——基础知识 day1-1
JUC系列--基础知识 day1-1 JUC基础知识 进程 线程 进程和线程区别 并行与并发 同步 使用场景 异步 使用情景 QuickStart(new Thread方式创建新线程) 匿名内部类方式 ...
- JUC系列之线程池的使用
JUC系列文章目录 JUC系列往期文章 文章目录 JUC系列文章目录 前言 一.线程池 二.线程池构造函数的参数 三.线程池处理任务流程 四.如何自定义合适的线程池 五.如何正确关闭线程池 五.线程池 ...
- JUC系列1-基础知识
JUC系列-基础知识 线程启动 代码示例 继承Thread类 实现Runnable接口 利用FutureTask 线程常用方法 线程通知与等待 wait方法 notify方法 生产者与消费者代码示例 ...
- JUC系列之并发队列
JUC系列文章目录 JUC系列往期文章 文章目录 JUC系列文章目录 前言 一.为什么要使用队列 二.ArrayBlockingQueue和LinkedBlockingQueue 1.LinkedBl ...
- 并发编程JUC系列及部分问题
什么是 CAS 吗? CAS(Compare And Swap)指比较并交换.CAS算法CAS(V, E, N)包含 3 个参数,V 表示要更新的变量,E 表示预期的值,N 表示新值.在且仅在 V 值 ...
- 【JUC系列】Executor框架之CompletionService
[JUC系列]Executor框架之CompletionService 文章目录 [JUC系列]Executor框架之CompletionService CompletionService Execu ...
最新文章
- Proovread安装与试用
- 思维导图学 Linux Shell攻略之小试牛刀篇
- 组件生命周期管理和通信方案
- 推送通知_谷歌宣布为安卓带来声音通知功能 可监测周围声音如婴儿哭声推送通知...
- WPF DataGrid 对行中单元格的访问
- R语言︱噪声数据处理、数据分组——分箱法(离散化、等级化)
- 深度学习TF—1.TensorFlow2基本操作
- 计算机产业现状及未来,2020工业计算机行业现状及未来前景分析
- html js中点击事件的三种方法
- 蒋鑫鸿:9,6国际黄金原油最新行情价格分析策略及今日投资操作建议
- Java编程思想 - 并发
- 烧写ARM板----MYS-6ULX
- 【企业架构设计实战】2 业务架构设计
- Excel电子表格隔行自动填充底色
- 搞笑漫画:程序员的逻辑
- Python全栈开发——面向对象进阶(一切皆对象)
- Alpha GO核心原理
- MySQL8.0.27 修改编码类型(utf8mb3)
- 台式电脑怎么找不到计算机在哪,台式电脑没有声音了怎么恢复(在家用这两个方法轻松解决)...
- 未雨绸缪 大三的你如何备战考研
热门文章
- manjora上好玩的游戏_强势推荐!steam平台最值得入正的游戏
- Python-Django毕业设计流浪猫狗救助站(程序+Lw)
- 【Python】SimpleITK 针对于 LiTS 数据集,获取最大肝脏面积的切片
- Linux下TIomap芯片MUX配置
- 鸿蒙os何时能升级,鸿蒙os升级时间 鸿蒙os系统怎么样
- 大火的AIGC是什么?能用到工作中哪些地方?
- 前端的【趁手兵器】——VSCode
- PowerDesigner顺序图(时序图)生成协作图(通信图)
- vs2017解决方案列表添加文件夹与实际目录中的文件夹对应
- Anaconda+Tensorflow_Gpu+Spyder安装记录(2022年10月14日更新)