ThrealLocal是面试中的一个重点,所以掌握好这部分知识点至关重要的。

你可能会用到的链接:
ThreadLocal源码分析
Java的强、软、弱、虚四种引用类型

文章目录

  • 1、ThreadLocal
    • 1.1、什么是ThreadlLocal
    • 1.2、多线程会出现的问题
    • 1.3、加锁
    • 1.4、使用ThreadLocal的方法
    • 1.5、ThreadLocal方案的好处
  • 2、ThreadLocal内部结构
    • 2.1、结构变化
    • 2.2、常用方法
  • 3、ThreadLocalMap
    • 3.1、内部细节
    • 3.2、核心方法
  • 4、弱引用和内存泄漏
    • 4.1、概念
    • 4.2、ThrealLocal中的内存泄漏
  • 5、扩容

1、ThreadLocal

1.1、什么是ThreadlLocal

我们查看其对应的源码,根据英文的翻译可得:

ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证 各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。

我们可以得知 ThreadLocal 的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。(在多个线程的基础之上,ThreadLocal 变量可以理解为每个线程的自己的变量,不同的线程都拥有属于自己的那一份)

1.2、多线程会出现的问题

测试代码:

package pers.mobian.ThreadLocal;public class ThreadlLocalTest01 {private String content;private String getContent() {return content;}private void setContent(String content) {this.content = content;}public static void main(String[] args) {//实例化我们的对象,然后传入对应的值ThreadlLocalTest01 t1 = new ThreadlLocalTest01();for (int i = 0; i < 5; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {t1.setContent(Thread.currentThread().getName() + "");System.out.println(Thread.currentThread().getName() + "的数据为:" + t1.getContent());}});thread.start();}}
}

执行结果:

Thread-1的数据为:Thread-1
Thread-3的数据为:Thread-3
Thread-2的数据为:Thread-2
Thread-0的数据为:Thread-3
Thread-4的数据为:Thread-4

我们不难发现,我们传入对应的内容以后,取出来的值却不是对应的值。其实这一点不难理解,因为在多线程情况下,我们的线程互相的争夺资源,会出现拿到别人资源的情况。

怎么办呢?

第一反应,加锁,多线程情况下,我们总是一言不合就加锁。那么问题来了,加什么锁?

1.3、加锁

synchronized锁

synchronized (ThreadlLocalTest01.class) {t1.setContent(Thread.currentThread().getName() + "");System.out.println(Thread.currentThread().getName() + "的数据为:" + t1.getContent());
}

ReentrantLock锁

ReentrantLock lock = new ReentrantLock();
for (int i = 0; i < 5; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {lock.lock();t1.setContent(Thread.currentThread().getName() + "");System.out.println(Thread.currentThread().getName() + "的数据为:" + t1.getContent());lock.unlock();}});thread.start();
}

以上就是两种加锁的方式,确实能够解决资源的冲突问题,但是随之而来的是性能的问题。

当我们使用加锁的方式的时候,总是让线程进行一种排队的方式修改并且获取资源,这是一种十分有效但是效率却十分低的修改方式(我们不难发现,直接加锁的方式是十分简单粗暴的,但是大家最终都会为了性能做出相应的妥协,当然synchronized关键字在JDK1.6的时候也进行了优化了,提高了性能,这就涉及到另一个锁升级的问题,这里就不展开了)。于是我们的ThreadLocal就来了。

我们使用ThreadLocal以后,就相当于我不限制你的访问,我只在每个线程内部做一个标记,不同的线程使用不同的标记,当我们的线程需要访问对应的资源的时候,我们就去根据当前的线程标记,找到与之匹配的标记,继而获取相同标记下的数据。使用这种方式,我们不仅能够解决资源冲突问题,还可以提高我们的并发度。

1.4、使用ThreadLocal的方法

修改后的代码:

public class ThreadlLocalTest02 {private static String content;private String getContent() {//返回对应的ThreadLocal中的数据return local.get();}private void setContent(String content) {//我们在设置值的时候,将内容绑定到对应的ThreadLocal中local.set(content);this.content = content;}private static ThreadLocal<String> local = new ThreadLocal();public static void main(String[] args) {ThreadlLocalTest02 t2 = new ThreadlLocalTest02();for (int i = 0; i < 5; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {t2.setContent(Thread.currentThread().getName());System.out.println(Thread.currentThread().getName() + ":" + t2.getContent());}});thread.start();}}
}

测试结果:

Thread-1:Thread-1
Thread-4:Thread-4
Thread-2:Thread-2
Thread-3:Thread-3
Thread-0:Thread-0

补充:我们在实际的测试过程中,很容易出现资源没有发生冲突的现象,我们可以使用将线程sleep的方式,来扩大多线程下资源的冲突问题。

ThreadLocal与synchronized的区别

synchronized ThreadLocal
原理 同步机制采用’以时间换空间’的方式, 只提供了一份变量,让不同的线程排队访问 ThreadLocal采用’以空间换时间’的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰
侧重点 多个线程之间访问资源的同步性 多线程中让每个线程之间的数据相互隔离

1.5、ThreadLocal方案的好处

  1. 数据传递 : 保存每个线程绑定的数据,在需要的地方可以直接获取, 避免参数直接传递带来的代码耦合

  2. 线程隔离 : 各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失


2、ThreadLocal内部结构

2.1、结构变化

在JDK早期的设计:

在ThreadLocal内部维护一个map,然后将我们的线程设置为key,然后对应的参数设置为value,每一个线程去获取对应的value时,就去比照对应的key

JDK优化后的设计:

每一个Thread线程,单独维护一个ThreadLocalMap,这个对应Map的key为ThreadLocal实例本身,value为我们需要存储的值,是一个Object类型。

优化后的好处:

  • 这样设计之后每个Map存储的Entry数量就会变少,因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。
  • 当Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。

2.2、常用方法

源码部分,建议配合使用:ThreadLocal源码分析

方法 作用
ThreadLocal() 创建ThreadLocal对象
public void set( T value) 设置当前线程绑定的局部变量
public T get() 获取当前线程绑定的局部变量
public void remove() 移除当前线程绑定的局部变量
protected T initialValue() 返回当前线程局部变量的初始值

get,set和remove逻辑是比较相似的,明白一个以后,就可以一通百通了。

get方法

//返回当前线程中保存ThreadLocal的值,
//如果当前线程没有此ThreadLocal变量(第一次添加value),则会通过initialValue进行初始化
public T get() {Thread t = Thread.currentThread();//获取对应的mapThreadLocalMap map = getMap(t);//判断是获取节点还是初始化一个节点if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();
}//用于初始化值initialValue,并返回初始化后的值
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;
}//获取当前线程Thread对应维护的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}//创建当前线程Thread对应维护的ThreadLocalMap
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}

set方法

//设置当前线程对应的ThreadLocal的值(类似于初始化get下的setInitialValue方法)
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
}

remove方法

//删除当前线程中保存的ThreadLocal对应的实体entry
public void remove() {hreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);
}

initialValue方法

protected T initialValue() {return null;
}

其实这个类的源码还是比较简单的。删除方法和初始化方法就不需要多说了。

set方法为,获取对应的ThreadLocalMap 实例,如果获取到了,就修改对应的value,没有获取到就创建一个ThreadLocalMap 实例

get方法为,首先获取对应的ThreadLocalMap 实例,没获取到就调用initialValue方法进行初始化(第一次进来),获取到了对应的实例,就返回对应节点下的信息。


3、ThreadLocalMap

ThreadLocalMap是ThreadLocal的内部类,代码占了整个类的一半多,可见其重要性

该类内部对应的类图:

其内部包含了一个Entry节点,Entry节点内部又包含一个弱引用(Java的四种引用类型之一)

如果对Java的引用知识点不熟悉的小伙伴,可以去看我的另一篇博客:Java的强、软、弱、虚四种引用类型

3.1、内部细节

源码部分,建议配合使用:ThreadLocal源码分析

基本变量

private static final int INITIAL_CAPACITY = 16;private Entry[] table;private int size = 0;private int threshold; private void setThreshold(int len) { threshold = len * 2 / 3; }

Entry节点

static class Entry extends WeakReference<ThreadLocal> {Object value;Entry(ThreadLocal k, Object v) {super(k);value = v;}
}

构造方法

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//创建一个新的节点table = new Entry[INITIAL_CAPACITY];//计算对应的索引值int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//将我们的节点,放在对应的hash位置i上table[i] = new Entry(firstKey, firstValue);size = 1;//设置阈值(默认的变量是16)setThreshold(INITIAL_CAPACITY);
}

firstKey.threadLocalHashCode方法

//返回对应的hash值
private final int threadLocalHashCode = nextHashCode();private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//AtomicInteger,java在JUC包下的原子类,用于解决volatile无法满足的原子性
private static AtomicInteger nextHashCode =  new AtomicInteger();//Java开发者选择的一个解决hash冲突,计算出的一个比较好的数字
private static final int HASH_INCREMENT = 0x61c88647;

3.2、核心方法

我们既然已经知道了ThreadLocalMap内部维护的是一个map,那么我们是否可以想到就会出现hash冲突?那我们的ThreadLocalMap是如何解决这个冲突的呢?我们一起来看它的set方法

ThreadLocalMap内部的set方法

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内部的set方法对应的索引方法

//获取环形数组的下一个索引
private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);
}//获取环形数组的上一个索引
private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1);
}

ThreadLocalMap使用开发地址-线性探测法来解决哈希冲突,线性探测法的地址增量di = 1, 2, … 其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

那么此时我们就可以将我们的table看成一个环形table


4、弱引用和内存泄漏

4.1、概念

内存溢出和内存泄漏:

  • Memory Overflow:内存溢出,没有足够的内存提供申请者使用
  • Memory leak:内存泄漏是指程序中已动态分配的堆内存由于某些原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积将导致内存溢出

注意不要混淆了这两个概念

弱引用和强引用

Java中的弱引用有4种类型:强、软、弱、虚。我们这里只涉及其中的两个。想要补课的小伙伴:Java的强、软、弱、虚四种引用类型

  • 强引用(Strong Reference)即我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还活着,垃圾回收器就不会回收这个对象
  • 弱引用(Weak Reference)即垃圾回收器一旦发现了只要具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存

4.2、ThrealLocal中的内存泄漏

根据我们的类图,我们可以将我们的整个调用调用过程:

我们每启动一个线程,就会出现CurrentThread Ref --> CurrentThread --> Map --> Entey

我们每使用一次ThreadLocal就会出现ThreadLocal Ref --> ThreadLocal,key–> ThreadLocal

有人说,我们使用ThrealLocal出现的内存泄漏和弱引用有关。我们来分析一下,是这样吗?甚至想一想为什么ThrealLocal不直接使用强引用,非要使用弱引用?

使用强引用

分析

  1. 假设在业务代码中ThreadLocal使用完毕,ThreadLocal Ref被回收了
  2. 但是因为threadLocalMap的Entry强引用了threadLocal,造成ThreadLocal无法被回收
  3. 在没有手动删除Entry以及CurrentThread依然运行的前提下,始终有强引用链threadRef --> currentThread --> Entry,Entry就不会被回收( Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏
  4. 即ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的

弱引用

分析

  1. 假设在业务代码中使用完ThreadLocal,ThreadLocal Ref被回收了
  2. 由于threadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例,所以threadlocal就可以顺利被gc回收,此时Entry中的key = null
  3. 在没有手动删除Entry以及CurrentThread依然运行的前提下,也存在有强引用链threadRef --> currentThread --> value,value就不会被回收,而这块value永远不会被访问到了,导致value内存泄漏
  4. 也即ThreadLocalMap中的key使用了弱引用,也有可能内存泄漏。

结论:无论ThreadLocalMap中的key是使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系

要避免内存泄漏有两种方式:

  1. 使用完ThreadLocal,调用其remove方法删除对应的Entry
  2. 使用完ThreadLocal,当前Thread也随之运行结束

相对于第一种方式,第二种方法显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的,而是返回到线程池

也就是说,只要记得在使用完ThreadLocal后,及时的调用remove方法,无论key是弱引用还是强引用都不会有问题,那么为什么key还要使用弱引用呢?

事实上,在ThreadLocalMap中的set/getEntry方法中,会对key设置为null(也就是ThreadLocal为null)进行判断,如果为null的话,那么是会将value置为null的

这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用多了一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set、get、remove中的任一方法的时候会被消除,从而避免内存泄漏

补充我们的getEntry方法和remove方法:

private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;else//e=null时return getEntryAfterMiss(key, i, e);
}private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;//不会进入while循环while (e != null) {ThreadLocal<?> k = e.get();if (k == key)return e;if (k == null)expungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}//将节点设置为nullreturn null;
}
private void remove(ThreadLocal<?> key) {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)]) {if (e.get() == key) {e.clear();expungeStaleEntry(i);return;}}
}

5、扩容

对于扩容部分,请去源码分析中查看,这里就不再赘述。

ThreadLocal源码分析

ThrealLocal原理讲解相关推荐

  1. php 伪静态 page-18.html,PHP 伪静态实现技术原理讲解

    PHP 伪静态实现技术原理讲解 发布于 2015-01-18 23:52:58 | 129 次阅读 | 评论: 0 | 来源: 网友投递 PHP开源脚本语言PHP(外文名: Hypertext Pre ...

  2. nginx反向代理原理讲解

    一 .概述                  反向代理(Reverse Proxy)方式是指以代理服务器来接受Internet上的连接请求,然后将请求转发给内部网络上的服务器:并将从服务器上得到的结果 ...

  3. 微信小游戏开发教程-2D游戏原理讲解

    微信小游戏开发教程-2D游戏原理讲解 原理 为了更加形象的描述,这里先上一张图: 背景 a. 首先,我们看到背景好像是一张无限长的图片在向下移动.实际则不然,这是一张顶部和底部刚好重叠的图片.这是一种 ...

  4. 解密汽车全景行车安全系统的前世和今生——第二讲:原理讲解

    解密汽车全景行车安全系统的前世和今生--第二讲:原理讲解 来源:深圳市汽车电子行业协会 作者:姜卫忠 发布时间:2013-3-7  浏览(4648)次 解密汽车全景行车安全系统的前世和今生 第二讲:全 ...

  5. pureMVC简单示例及其原理讲解四(Controller层)

    本节将讲述pureMVC示例中的Controller层. Controller层有以下文件组成: AddUserCommand.as DeleteUserCommand.as ModelPrepCom ...

  6. IoT物联网嵌入式设备中30种常见传感器模块简介及原理讲解

    IoT物联网嵌入式设备中30种常见传感器模块简介及原理讲解 0.前言 一.光学传感器模块: 1. 光敏传感器模块: 2. 红外避障模块 3. 循迹传感器模块 4. U型光电传感器模块 5. 红外接收模 ...

  7. 艺点动画-跟随原理讲解

    艺点动画-跟随原理讲解 如果要想顺便解决就业问题的话,可以去试试看成都艺点动画,这家教学质量, 蛮高的毕业后是可以直接在公司里面上班的 方法/步骤 1.什么是动画里的跟随? 动画的跟随指的是:物体在运 ...

  8. 酷狗音乐的爬取,基于python,从无到有完整教程-上:搭建环境及爬取原理讲解

    酷狗音乐的爬取,基于python,从无到有完整教程,使用微软新edge作为虚拟浏览器 搭建环境及爬取原理讲解 ⬇⬇⬇ 编码环境及工具准备: 编码工具:PyCharm 所需的库: import requ ...

  9. AMCL算法原理讲解

    ROS进阶教程(二)AMCL算法原理讲解 AMCL算法理解 蒙特卡洛定位算法 蒙特卡洛定位算法自适应变种 里程计运动模型 测距仪模型 波束模型 似然域模型 AMCL算法理解 AMCL(adaptive ...

  10. Unity半透明特效原理讲解(为什么半透明设置渲染顺序和深度写入这么重要)

    Unity半透明特效原理讲解(为什么半透明设置渲染顺序和深度写入这么重要 写在前面 实验场景 实验1:红(不透明)+蓝(不透明)+默认渲染顺序(先渲染蓝Cube) 实验2:红(不透明+优先渲染)+蓝( ...

最新文章

  1. 【电子基础】模拟电路问答
  2. 使用clang将C/C++代码编译成LLVM的中间代码(LLVM ir bitcode),并反汇编LLVM bitcode
  3. np.random.choice()用法
  4. 移动互联网用户的心理需求【转载】
  5. Groovy 使用完全解析
  6. 前端学习(2197):__WEBPACK_IMPORTED_MODULE_1_vuex__.a.store is not a constructor
  7. 对10亿个数据去重java_20 亿个数字在 4G 内存中如何去重排序:快来试一试 BitMap...
  8. 兆观毫米波监护仪亮相CMEF 开创养老监护新时代
  9. koa如何实现Oauth2(一)
  10. xml dtd 约束建立xml文档
  11. IPV6 Socket编程
  12. NVIDIA-CUDA编程初探
  13. excel查找空值快捷键_『EXCEL定位条件快捷键』excel定位空值填充
  14. 安徽师大附中%你赛day4T1 金字塔 解题报告
  15. 数位板跟数位屏有什么区别,哪个好些?
  16. TMB:肿瘤突变负荷简介
  17. Tomcat 安装与配置
  18. digitalpersona 开发(系统托盘,监听指纹扫描)
  19. eclipse怎么调字体
  20. Python:利用cv2模块识别手势

热门文章

  1. [2018.07.10 T2]不回文
  2. typescript的类型转化
  3. 黑马vue实战项目-(六)商品列表组件的开发
  4. pycharm设置python环境_pycharm怎么配置python环境
  5. python爬虫系统知识_网络爬虫基础知识(Python实现)
  6. python控制台编写_Python:为控制台prin编写unittest
  7. python io多路复用_Python之路--协程/IO多路复用
  8. python中的引用怎么理解_浅谈动态类型领域中 Python 的变量、对象以及引用
  9. mysql命令语句连接数据库_MySQL_MySQL常用基本SQL语句总结,1. 常见命令连接本地数据库 - phpStudy...
  10. 2016河北省职称计算机考试试题及答案,2016年最新河北省职称计算机考试试题及答案..doc...