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 的使用相关推荐

  1. 突击并发编程JUC系列-ReentrantReadWriteLock

    突击并发编程JUC系列演示代码地址: https://github.com/mtcarpenter/JavaTutorial 本章节将学习 ReentrantReadWriteLock(读写锁),Re ...

  2. AQS源码二探-JUC系列

    Python微信订餐小程序课程视频 https://edu.csdn.net/course/detail/36074 Python实战量化交易理财系统 https://edu.csdn.net/cou ...

  3. JUC系列(一)什么是JUC?

    多线程一直Java开发中的难点,也是面试中的常客,趁着还有时间,打算巩固一下JUC方面知识,我想机会随处可见,但始终都是留给有准备的人的,希望我们都能加油!!! 沉下去,再浮上来,我想我们会变的不一样 ...

  4. JUC系列——基础知识 day1-1

    JUC系列--基础知识 day1-1 JUC基础知识 进程 线程 进程和线程区别 并行与并发 同步 使用场景 异步 使用情景 QuickStart(new Thread方式创建新线程) 匿名内部类方式 ...

  5. JUC系列之线程池的使用

    JUC系列文章目录 JUC系列往期文章 文章目录 JUC系列文章目录 前言 一.线程池 二.线程池构造函数的参数 三.线程池处理任务流程 四.如何自定义合适的线程池 五.如何正确关闭线程池 五.线程池 ...

  6. JUC系列1-基础知识

    JUC系列-基础知识 线程启动 代码示例 继承Thread类 实现Runnable接口 利用FutureTask 线程常用方法 线程通知与等待 wait方法 notify方法 生产者与消费者代码示例 ...

  7. JUC系列之并发队列

    JUC系列文章目录 JUC系列往期文章 文章目录 JUC系列文章目录 前言 一.为什么要使用队列 二.ArrayBlockingQueue和LinkedBlockingQueue 1.LinkedBl ...

  8. 并发编程JUC系列及部分问题

    什么是 CAS 吗? CAS(Compare And Swap)指比较并交换.CAS算法CAS(V, E, N)包含 3 个参数,V 表示要更新的变量,E 表示预期的值,N 表示新值.在且仅在 V 值 ...

  9. 【JUC系列】Executor框架之CompletionService

    [JUC系列]Executor框架之CompletionService 文章目录 [JUC系列]Executor框架之CompletionService CompletionService Execu ...

最新文章

  1. Proovread安装与试用
  2. 思维导图学 Linux Shell攻略之小试牛刀篇
  3. 组件生命周期管理和通信方案
  4. 推送通知_谷歌宣布为安卓带来声音通知功能 可监测周围声音如婴儿哭声推送通知...
  5. WPF DataGrid 对行中单元格的访问
  6. R语言︱噪声数据处理、数据分组——分箱法(离散化、等级化)
  7. 深度学习TF—1.TensorFlow2基本操作
  8. 计算机产业现状及未来,2020工业计算机行业现状及未来前景分析
  9. html js中点击事件的三种方法
  10. 蒋鑫鸿:9,6国际黄金原油最新行情价格分析策略及今日投资操作建议
  11. Java编程思想 - 并发
  12. 烧写ARM板----MYS-6ULX
  13. 【企业架构设计实战】2 业务架构设计
  14. Excel电子表格隔行自动填充底色
  15. 搞笑漫画:程序员的逻辑
  16. Python全栈开发——面向对象进阶(一切皆对象)
  17. Alpha GO核心原理
  18. MySQL8.0.27 修改编码类型(utf8mb3)
  19. 台式电脑怎么找不到计算机在哪,台式电脑没有声音了怎么恢复(在家用这两个方法轻松解决)...
  20. 未雨绸缪 大三的你如何备战考研

热门文章

  1. manjora上好玩的游戏_强势推荐!steam平台最值得入正的游戏
  2. Python-Django毕业设计流浪猫狗救助站(程序+Lw)
  3. 【Python】SimpleITK 针对于 LiTS 数据集,获取最大肝脏面积的切片
  4. Linux下TIomap芯片MUX配置
  5. 鸿蒙os何时能升级,鸿蒙os升级时间 鸿蒙os系统怎么样
  6. 大火的AIGC是什么?能用到工作中哪些地方?
  7. 前端的【趁手兵器】——VSCode
  8. PowerDesigner顺序图(时序图)生成协作图(通信图)
  9. vs2017解决方案列表添加文件夹与实际目录中的文件夹对应
  10. Anaconda+Tensorflow_Gpu+Spyder安装记录(2022年10月14日更新)