揭秘ThreadLocal(转)
转载自:掘金大闲人柴毛毛博客。
ThreadLocal是开发中最常用的技术之一,也是面试重要的考点。本文将由浅入深,介绍ThreadLocal的使用方式、实现原理、内存泄漏问题以及使用场景。
ThreadLocal作用
在并发编程中时常有这样一种需求:每条线程都需要存取一个同名变量,但每条线程中该变量的值均不相同。
如果是你,该如何实现上述功能?常规的思路如下: 使用一个线程共享的Map<Thread,Object>
,Map中的key为线程对象,value即为需要存储的值。那么,我们只需要通过map.get(Thread.currentThread())
即可获取本线程中该变量的值。
这种方式确实可以实现我们的需求,但它有何缺点呢?——答案就是:需要同步,效率低!
由于这个map对象需要被所有线程共享,因此需要加锁来保证线程安全性。当然我们可以使用java.util.concurrent.*
包下的ConcurrentHashMap
提高并发效率,但这种方法只能降低锁的粒度,不能从根本上避免同步锁。而JDK提供的ThreadLocal
就能很好地解决这一问题。下面来看看ThreadLocal是如何高效地实现这一需求的。
如何使用ThreadLocal
在介绍ThreadLocal原理之前,首先简单介绍一下它的使用方法。
public class Main{
private ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); public void start() {
for (int i=0; i<10; i++) {
new Thread(new Runnable(){
@override
public void run(){
threadLocal.set(i);
threadLocal.get();
threadLocal.remove();
}
}).start();
}
}
}
- 首先我们需要创建一个线程共享的ThreadLocal对象,该对象用于存储Integer类型的值;
- 然后在每条线程中可以通过如下方法操作ThreadLocal:
set(obj)
:向当前线程中存储数据get()
:获取当前线程中的数据remove()
:删除当前线程中的数据
ThreadLocal的使用方法非常简单,关键在于它背后的实现原理。回到上面的问题:ThreadLocal究竟是如何避免同步锁,从而保证读写的高效?
ThreadLocal实现原理
ThreadLocal的内部结构如下图所示:
ThreadLocal
并不维护ThreadLocalMap
,并不是一个存储数据的容器,它只是相当于一个工具包,提供了操作该容器的方法,如get、set、remove等。而ThreadLocal
内部类ThreadLocalMap
才是存储数据的容器,并且该容器由Thread
维护。
每一个Thread
对象均含有一个ThreadLocalMap
类型的成员变量threadLocals
,它存储本线程中所有ThreadLocal对象及其对应的值。
ThreadLocalMap
由一个个Entry
对象构成,Entry
的代码如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry
继承自WeakReference<ThreadLocal<?>>
,一个Entry
由ThreadLocal
对象和Object
构成。由此可见,Entry
的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该key就会被垃圾收集器回收。
那么,ThreadLocal是如何工作的呢?下面来看set和get方法。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(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) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
} ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。
get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。
由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。
为何要使用弱引用?
对弱引用不了解的同学可以参考笔者的另一篇文章:http://blog.csdn.net/u010425776/article/details/50760053。
Java设计之初的一大宗旨就是——弱化指针。 Java设计者希望通过合理的设计简化编程,让程序员无需处理复杂的指针操作。然而指针是客观存在的,在目前的Java开发中也不可避免涉及到“指针操作”。如:
Object a = new Object();
上述代码创建了一个强引用a,只要强引用存在,垃圾收集器是不会回收该对象的。如果该对象非常庞大,那么为了节约内存空间,在该对象使用完成后,我们需要手动拆除该强引用,如下面代码所示:
a = null;
此时,指向该对象的强引用消除了,垃圾收集器便可以回收该对象。但在这个过程中,仍然需要程序员处理指针。为了弱化指针这一概念,弱引用便出现了,如下代码创建了一个Person类型的弱引用:
WeakReference<Person> wr = new WeakReference<Person>(new Person());
此时程序员不用再关注指针,只要没有强引用指向Person对象,垃圾收集器每次运行都会自动将该对象释放。
那么,ThreadLocalMap中的key使用弱引用的原因也是如此。当一条线程中的ThreadLocal对象使用完毕,没有强引用指向它的时候,垃圾收集器就会自动回收这个Key,从而达到节约内存的目的。
那么,问题又来了——这会导致内存泄漏问题!
ThreadLocal的内存泄漏问题
在ThreadLocalMap中,只有key是弱引用,value仍然是一个强引用。当某一条线程中的ThreadLocal使用完毕,没有强引用指向它的时候,这个key指向的对象就会被垃圾收集器回收,从而这个key就变成了null;然而,此时value和value指向的对象之间仍然是强引用关系,只要这种关系不解除,value指向的对象永远不会被垃圾收集器回收,从而导致内存泄漏!
不过不用担心,ThreadLocal提供了这个问题的解决方案。
每次操作set、get、remove操作时,ThreadLocal都会将key为null的Entry删除,从而避免内存泄漏。
那么问题又来了,如果一个线程运行周期较长,而且将一个大对象放入LocalThreadMap后便不再调用set、get、remove方法,此时该仍然可能会导致内存泄漏。
这个问题确实存在,没办法通过ThreadLocal解决,而是需要程序员在完成ThreadLocal的使用后要养成手动调用remove的习惯,从而避免内存泄漏。
ThreadLocal的使用场景
Web系统Session的存储就是ThreadLocal一个典型的应用场景。
Web容器采用线程隔离的多线程模型,也就是每一个请求都会对应一条线程,线程之间相互隔离,没有共享数据。这样能够简化编程模型,程序员可以用单线程的思维开发这种多线程应用。
当请求到来时,可以将当前Session信息存储在ThreadLocal中,在请求处理过程中可以随时使用Session信息,每个请求之间的Session信息互不影响。当请求处理完成后通过remove方法将当前Session信息清除即可。
作者:大闲人柴毛毛
链接:https://juejin.im/post/5aa74967f265da23a334e373
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
揭秘ThreadLocal(转)的更多相关文章
- 深度揭秘Netty中的FastThreadLocal为什么比ThreadLocal效率更高?
阅读这篇文章之前,建议先阅读和这篇文章关联的内容. 1. 详细剖析分布式微服务架构下网络通信的底层实现原理(图解) 2. (年薪60W的技巧)工作了5年,你真的理解Netty以及为什么要用吗?(深度干 ...
- Spark Streaming揭秘 Day7 再探Job Scheduler
Spark Streaming揭秘 Day7 再探Job Scheduler 今天,我们对Job Scheduler再进一步深入一下,对一些更加细节的源码进行分析. Job Scheduler启动 在 ...
- Spark Streaming揭秘 Day5 初步贯通源码
Spark Streaming揭秘 Day5 初步贯通源码 引子 今天,让我们从Spark Streaming最重要的三个环节出发,让我们通过走读,逐步贯通源码,还记得Day1提到的三个谜团么,让我们 ...
- 基础篇:JAVA引用类型和ThreadLocal
前言 平时并发编程,除了维护修改共享变量的场景,有时我们也需要为每一个线程设置一个私有的变量,进行线程隔离,java提供的ThreadLocal可以帮助我们实现,而讲到ThreadLocal则不得不讲 ...
- ThreadLocal简单理解
在java开源项目的代码中看到一个类里ThreadLocal的属性: private static ThreadLocal<Boolean> clientMode = new Thread ...
- Syscan360会议胸牌破解揭秘
Syscan360会议胸牌破解揭秘 背景 有幸参加今年11月份的上海Syscan360安全会议,会议期间有一个亮点就是360的独角兽团队设计了一款电子badge(胸牌)供参加人员进行破解尝试,类似于美 ...
- Android线程管理之ThreadLocal理解及应用场景
前言: 最近在学习总结Android的动画效果,当学到Android属性动画的时候大致看了下源代码,里面的AnimationHandler存取使用了ThreadLocal,激起了我很大的好奇心以及兴趣 ...
- Threadlocal使用Case
Threadlocal能够为每个线程分配一份单独的副本,使的线程与线程之间能够独立的访问各自副本.Threadlocal 内部维护一个Map,key为线程的名字,value为对应操作的副本. /** ...
- CI Weekly #9 | 揭秘阿里 Docker 化实践之路
2017年悄然而至,对 flow.ci 你有什么新的期待呢?新的一年,flow.ci会越来越强大好用,希望继续得到你的支持与反馈.最近,我们做了如下的「功能优化」与「问题修复」,看看有没有你想要的: ...
随机推荐
- BSGS算法+逆元 POJ 2417 Discrete Logging
POJ 2417 Discrete Logging Time Limit: 5000MS Memory Limit: 65536K Total Submissions: 4860 Accept ...
- CDOJ 842 天下归晋 树状数组
天下归晋 Time Limit: 1 Sec Memory Limit: 256 MB 题目连接 http://acm.uestc.edu.cn/#/problem/show/842 Descrip ...
- Unity ScriptObject创建Asset文件
创建ScriptObject可以创建带序列化的资源,只保存数据不用绑定在游戏对象上.创建出来的本子资源可以通过资源加载到游戏里使用.这里介绍一下使用Resources加载. 创建好的asset文件也可 ...
- iOS Masonry的使用需要注意的地方
自动布局最重要的是约束:UI元素间关系的数学表达式.约束包括尺寸.由优先级和阈值管理的相对位置.它们是添加剂,可能导致约束冲突 .约束不足造成布局无法确定 .这两种情况都会产生异常. 使用前:Auto ...
- Sublime的中文GBK显示乱码的解决方法
import urllib2,os,hashlib; h = '7183a2d3e96f11eeadd761d777e62404' + 'e330c659d4bb41d3bdf022e94cab3cd ...
- Go:《Go语言 云动力》
背景 中秋快速的读了一遍<Go语言 云动力>,对Go有一下几点感觉: 在静态类型和动态类型之间取得了非常好的平衡,隐式接口实现会被后续的语言借鉴(希望C#能借鉴一下). 缺乏异常处理机制, ...
- 第一章 Actionscript学习基本知识笔记及flashdevelop软件的安装问题
OOP:封装.继承.多态. Pubilc :完全公开. Internal:包内类成员可以互相访问. Private:仅当前类可以访问. Protected:当前类和当前类的子类可以访问. 被关键词fi ...
- mysql concat函数进行模糊查询
concat() 函数,是用来连接字符串. 精确查询: select * from user where name=”zhangsan” 模糊查询: select * from user where ...
- 2)Linux程序设计入门--进程介绍
)Linux程序设计入门--进程介绍 Linux下进程的创建 前言: 这篇文章是用来介绍在Linux下和进程相关的各个概念.我们将会学到: 进程的概念 进程的身份 进程的创建 守护进程的创建 .进程的 ...
- Android NDK开发----- JNI多线程
一.概述 JNI编程和Linux上的C/C++编程还是挺相似的,每次java调用JNI中的函数时都会传入有关JVM的一些参数(如JNIEnv,jobject),每次JNI回调java中的方法时都要通过 ...