ThreadLocal源码解析,内存泄露以及传递性
我想ThreadLocal这东西,大家或多或少都了解过一点,我在接触ThreadLocal的时候,觉得这东西很神奇,在网上看了很多博客,也看了一些书,总觉得有一个坎跨不过去,所以对ThreadLocal一直是一知半解的,好在这东西在实际开发中毕竟用的不多,所以也就得过且过了。当然我说的“用的不多”,只是对于普通的上层业务开发而言,其实在很多框架中,都用到了ThreadLocal,甚至有的还对ThreadLocal做了进一步的改进。但是ThreadLocal也算是并发编程的基础,所以还真的有必要,也必须要好好研究下的。今天我们就来好好看看ThreadLocal。
ThreadLocal简单应用
我们知道在多线程下,操作一个共享变量,很容易会发生矛盾,要解决这问题,最好的办法当然是每个线程都拥有自己的变量,其他的线程无法访问,所谓“没有共享,就没有伤害”。那么如何做到呢?ThreadLocal就这样华丽丽的登场了。
我们先来看看简单的应用:
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set("Hello");
System.out.println("当前线程是:" + Thread.currentThread().getName());
System.out.println("在当前线程中获取:" + threadLocal.get());
new Thread(() -> System.out.println("现在线程是"+Thread.currentThread().getName()+"尝试获取:" + threadLocal.get())).start();
}
运行结果:
当前线程是:main
在当前线程中获取:Hello
现在线程是Thread-0尝试获取:null
运行结果很好理解,在主线程中往threadLocal 塞了一个值,只有在同一个线程下,才可以获得值,在其他线程就无法获取值了。
尝试自己写一个ThreadLocal
在我们探究ThreadLocal之前,先让我们思考一个问题,如果叫你来实现ThreadLocal,你会怎么做?
ThreadLocal的目标就在于让每个线程都有只属于自己的变量。最直接的办法就是新建一个泛型类,在类中定义一个map,key是Long类型的,用来保存线程的id,value是T类型的,用来保存具体的数据。
set的时候,就获取当前线程的id,把这个作为key,往map里面塞数据;
get的时候,还是获取当前线程的id,把这个作为key,然后从map中取出数据。
就像下面这个样子:
public class ThreadLocalTest {
public static void main(String[] args) {
CodeBearThreadLocal threadLocal = new CodeBearThreadLocal();
threadLocal.set("Hello");
System.out.println("当前线程是:" + Thread.currentThread().getName());
System.out.println("在当前线程中获取:" + threadLocal.get());
new Thread(() -> System.out.println("现在线程是" + Thread.currentThread().getName() + "尝试获取:" + threadLocal.get())).start();
}
}
class CodeBearThreadLocal<T> {
private ConcurrentHashMap<Long , T> hashMap = new ConcurrentHashMap<>();
void set(T value) {
hashMap.put(Thread.currentThread().getId(),value);
}
T get() {
return hashMap.get(Thread.currentThread().getId());
}
}
运行结果:
当前线程是:main
在当前线程中获取:Hello
现在线程是Thread-0尝试获取:null
可以看到运行结果和“正版的ThreadLocal”是一模一样的。
探究ThreadLocal
我们自己也写了一个ThreadLocal,看上去一点问题也没有,仅仅几行代码就把功能实现了,给自己鼓个掌。那正版的ThreadLocal是怎么实现的呢?核心应该和我们写的差不多吧。遗憾的是,正版的ThreadLocal和我们写的可以说完全不一样。
我们现在看看正版的ThreadLocal是怎么做的。
set
public void set(T value) {
Thread t = Thread.currentThread();//获取当前的线程
ThreadLocalMap map = getMap(t);//获取ThreadLocalMap
if (map != null)//如果map不为null,调用set方法塞入值
map.set(this, value);
else
createMap(t, value);//新建map
}
- 获取当前的线程赋值给t;
- 调用getMap方法,传入t,也就是传入当前线程,获取ThreadLocalMap,赋值给map;
- 如果map不为null,调用set方法塞入值;
- 如果map为null,则调用createMap方法。
让我们来看看getMap方法:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
getMap方法比较简单,直接返回了传进来的线程对象的threadLocals,说明threadLocals定义在Thread类里面,是ThreadLocalMap 类型的,让我们看看threadLocals的定义:
public class Thread implements Runnable{
ThreadLocal.ThreadLocalMap threadLocals = null;
}
看到这个定义,大家一定有点晕,我们是跟着ThreadLocal的set方法进来的,怎么到了这里又回到ThreadLocal了,大家别着急,我们再来看看ThreadLocalMap是什么鬼?
ThreadLocalMap是ThreadLocal的静态内部类,我们的数据就是保存在ThreadLocalMap里面,更详细的说我们的数据就保存在ThreadLocal类中的ThreadLocalMap静态内部类中的Entry[]里面。
让我们把关系理一理,确实有点混乱,Thread类里面定义了ThreadLocal.ThreadLocalMap字段,ThreadLocalMap是TheadLocal的内部静态类,其中的Entry[]是用来保存数据的。这就意味着,每一个Thread实例中的ThreadLocalMap都是独一无二的,又不相互干扰。等等,这不就揭开了ThreadLocal的神秘面纱了吗?原来ThreadLocal是这么做到让每个线程都有自己的变量的。
如果你还不清楚的话,没关系,我们再来说的详细点。在我们实现的ThreadLocal中,是利用map实现数据存储的,key就是线程Id,你可以理解为key就是Thread的实例,value就是我们需要保存的数据,当我们调用get方法的时候,就是利用线程Id,你可以理解为利用Thread的实例去map中取出数据,这样我们取出的数据就肯定是这个线程持有的。比如这个线程是A,你传入了B线程的线程Id,也就是传入了B线程的Thread的实例就肯定无法取出线程A所持有的数据,这点应该毫无疑问把。但是,在正版的ThreadLocal中,数据是直接存在Thread实例中的,这样每个线程的数据就被天然的隔离了。
现在我们解决了一个问题,ThreadLocal是如何实现线程数据隔离的,但是还有一个问题,也就是我初学ThreadLocal看了很多博客,仍然百思不得其解的问题,既然数据是保存在ThreadLocalMap中的Entry[]的,那么就代表可以保存多个数据,不然用一个普通的成员变量不就OK了吗,为什么要用数组呢?但是ThreadLocal提供的set方法没有重载啊,如果先set一个“hello”,又set一个“bye”,那么“bye”肯定会把“hello”给覆盖掉啊,又不像HashMap一样,有key和value的概念。这个问题真的困扰我很久,后面终于知道了原因了,我们可以new多个ThreadLocal呀,就像这样:
public static void main(String[] args) {
ThreadLocal threadLocal1 = new ThreadLocal();
threadLocal1.set("Hello");
ThreadLocal threadLocal2 = new ThreadLocal();
threadLocal2.set("Bye");
}
这样一来,会发生什么情况呢?再次放出set的代码,以免大家要往上翻很久:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
threadLocal1,threadLocal2都调用了set方法,尽管threadLocal1和threadLocal2是不同的实例,但是它们在同一个线程啊,所以getMap获取的ThreadLocalMap是同一个,这样就变成了在同一个ThreadLocalMap保存了多个数据。
具体是怎么保存数据的,这个代码就比较复杂了,包括的细节太多了,我看的也不是很懂,只知道一个大概,我们先来看看Entry的定义把:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
Entry又是ThreadLocalMap的静态内部类,里面只有一个字段value,也就是说和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();
}
- 把table赋值给局部变量tab,这个table就是保存数据的字段,类型是Entry[];
- 获取tab的长度赋值给len;
- 求出下标i;
- 一个for循环,先根据第三步求出的下标,从tab里获取指定下标的值e,如果e==null,就不会进入这个for循环,也就是如果当前的位置是空的,就直接进入第五步;如果当前的位置已经有数据了,判断这个位置的ThreadLocal和我们即将要插入进去的是不是同一个,如果是的话,用新值替换掉;如果不是的话,则寻找下一个空位;
- 把创建出来的Entry实例放入tab。
其中的细节有点多,看的有点迷糊,但是最关键的应该还算是看懂了。
get
public T get() {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//传入当前线程,获取当前线程的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//传入ThreadLocal实例,获取Entry
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;//返回值
}
}
return setInitialValue();
}
- 获取当前线程;
- 获取当前线程的ThreadLocalMap;
- 传入ThreadLocal实例,获取Enrty;
- 返回值。
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
return getEntryAfterMiss(key, i, e);
}
- 求出下标i;
- 根据下标i,从table中取出值,赋值给e;
- 如果e不为空,并且e持有的ThreadLocal实例和传进去的ThreadLocal实例是同一个,直接返回;
- 如果e为空,或者e持有的ThreadLocal实例和传进去的ThreadLocal实例不是同一个,则继续往下找。
小总结
set方法和get方法都分析完毕了,我们来做一个小总结。我们在外面所使用的ThreadLocal更像是一个工具类,本身不保存任何数据,而真正的数据是保存在Thread实例中的,这样就天然的完成了线程数据的隔离。最后送上一张图,来帮助大家更好的理解ThreadLocal:
内存泄露
我们再来看看Entry的定义:
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;
}
}
Entry继承了WeakReference,关于WeakReference是什么东西,不是本文的重点,大家可以自行查阅。WeakReference包裹了ThreadLocal,我们再来看Entry的构造方法,调用了super(k),传入了我们传进来的ThreadLocal实例,也就是ThreadLocal被保存到了WeakReference对象中。这就导致了一个问题,当ThreadLocal没有强依赖,ThreadLocal会在下一次发生GC时被回收,key是被回收了,但是value却没有被回收呀,所以就出现了Entry[]存在key为NULL,但是value不为NULL的项的情况,要想回收的话,可以让创建ThreadLocal的线程的生命周期结束。但是在实际的开发中,线程有极大可能是和程序同生共死的,只要程序不停止,线程就一直在蹦跶。所以我们在使用完ThreadLocal方法后,最好要手动调用remove方法,就像这样:
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal();
try {
threadLocal.set("Hello");
threadLocal.get();
} finally {
threadLocal.remove();
}
}
别忘了,最好把remove方法放在finally中哦。
InheritableThreadLocal
我们还是来看博客一开头的例子:
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set("Hello");
System.out.println("当前线程是:" + Thread.currentThread().getName());
System.out.println("在当前线程中获取:" + threadLocal.get());
new Thread(() -> System.out.println("现在线程是" + Thread.currentThread().getName() + "尝试获取:" + threadLocal.get())).start();
}
运行结果:
当前线程是:main
在当前线程中获取:Hello
现在线程是Thread-0尝试获取:null
代码后面new出来Thread是由主线程创建的,所以可以说这个线程是主线程的子线程,在主线程往ThreadLocal set的值,在子线程中获取不到,这很好理解,因为他们并不是同一个线程,但是我希望子线程能继承主线程的ThreadLocal中的数据。InheritableThreadLocal出现了,完全可以满足这样的需求:
public static void main(String[] args) {
ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("Hello");
System.out.println("当前线程是:" + Thread.currentThread().getName());
System.out.println("在当前线程中获取:" + threadLocal.get());
new Thread(() -> System.out.println("现在线程是" + Thread.currentThread().getName() + "尝试获取:" + threadLocal.get())).start();
}
运行结果:
当前线程是:main
在当前线程中获取:Hello
现在线程是Thread-0尝试获取:Hello
这样就让子线程继承了主线程的ThreadLocal的数据,说的更准确些,是子线程继承了父线程的ThreadLocal的数据。
那到底是如何做到的呢?还是看代码把。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
InheritableThreadLocal继承了ThreadLocal,并且重写了三个方法,当我们首次调用InheritableThreadLocal的set的时候,会调用InheritableThreadLocal的createMap方法,这就创建了ThreadLocalMap的实例,并且赋值给inheritableThreadLocals,这个inheritableThreadLocals定义在哪里呢?和ThreadLocal的threadLocals一样,也是定义在Thread类中。当我们再次调用set方法的时候,会调用InheritableThreadLocal的getMap方法,返回的也是inheritableThreadLocals,也就是把原先的threadLocals给替换掉了。
当我们创建一个线程,会调用Thread的构造方法:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
init方法比较长,我只复制出和我们要探究的问题相关的代码:
Thread parent = currentThread();
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
- 获取当前线程,此时当前线程是父线程。
- 如果父线程的inheritableThreadLocals不为空,就跑到if中去。当然这里肯定是不为空的,我们上面已经说了,调用InheritableThreadLocal中的set方法,直接操作的是inheritableThreadLocals,if中做了什么,就是传入了父线程的inheritableThreadLocals,创建了新的ThreadLocalMap,赋值给Thead实例的inheritableThreadLocals,这样子线程就拥有了父线程的ThreadLocalMap,也就完成了ThreadLocal的继承与传递。
这篇博客到这里就结束了,东西还是挺多的,但是都是挺重要的,特别是ThreadLocal的原因和产生内存泄露的原因和避免的方法。
ThreadLocal源码解析,内存泄露以及传递性的更多相关文章
- Java 8 ThreadLocal 源码解析
Java 中的 ThreadLocal是线程内的局部变量, 它为每个线程保存变量的一个副本.ThreadLocal 对象可以在多个线程中共享, 但每个线程只能读写其中自己的副本. 目录: 代码示例 源 ...
- ThreadLocal源码解析-Java8
目录 一.ThreadLocal介绍 1.1 ThreadLocal的功能 1.2 ThreadLocal使用示例 二.源码分析-ThreadLocal 2.1 ThreadLocal的类层级关系 2 ...
- Netty源码解析 -- 内存池与PoolArena
我们知道,Netty使用直接内存实现Netty零拷贝以提升性能, 但直接内存的创建和释放可能需要涉及系统调用,是比较昂贵的操作,如果每个请求都创建和释放一个直接内存,那性能肯定是不能满足要求的. 这时 ...
- Thread、ThreadLocal源码解析
今天来看一下Thread和ThreadLocal类的源码. 一.Thread (1)首先看一下线程的构造方法,之后会说每种参数的用法,而所有的构造函数都会指向init方法 //空构造创建一个线程 Th ...
- Netty源码解析 -- 内存对齐类SizeClasses
在学习Netty内存池之前,我们先了解一下Netty的内存对齐类SizeClasses,它为Netty内存池中的内存块提供大小对齐,索引计算等服务方法. 源码分析基于Netty 4.1.52 Nett ...
- ThreadLocal源码解析
主要用途 1)设计线程安全的类 2)存储无需共享的线程信息 设计思路 ThreadLocalMap原理 1)对象存储位置-->当前线程的ThreadLocalMap ThreadLocalMap ...
- 一步一步学多线程-ThreadLocal源码解析
上网查看了很多篇ThreadLocal的原理的博客,上来都是文字一大堆,费劲看了半天,大脑中也没有一个模型,想着要是能够有一张图明确表示出来ThreadLocal的设计该多好,所以就自己看了源码,画了 ...
- Java ThreadLocal 的使用与源码解析
GitHub Page: http://blog.cloudli.top/posts/Java-ThreadLocal-的使用与源码解析/ ThreadLocal 主要解决的是每个线程绑定自己的值,可 ...
- 并发编程(四)—— ThreadLocal源码分析及内存泄露预防
今天我们一起探讨下ThreadLocal的实现原理和源码分析.首先,本文先谈一下对ThreadLocal的理解,然后根据ThreadLocal类的源码分析了其实现原理和使用需要注意的地方,最后给出了两 ...
随机推荐
- orcale用户名的创建及权限设置
oracle数据库的权限系统分为系统权限与对象权限.系统权限( database system privilege )可以让用户执行特定的命令集.例如,create table权限允许用户创建表,gr ...
- 1.改变项目 动态库、静态库属性。 2.使用运行库 /MD、/MT、/LD说明
/MD./MT./LD(使用运行库) 有关 C 运行库以及使用 /clr(公共语言运行时编译) 进行编译时要使用哪些库的更多信息,请参见 CRT 库功能. 传递给链接器的给定调用的所有模块都必须使用相 ...
- java面试题汇总(1)
1)Java 中能创建 volatile 数组吗? 能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组.我的意思是,如果改变引用指向的数组, 将会受到 v ...
- Umbraco examine search media folder 中的pdf文件
可以参考的文章 http://sleslie.me/2015/selecting-media-using-razor-slow-performance-examine-to-the-rescue/ h ...
- JAVA基础学习-集合三-Map、HashMap,TreeMap与常用API
森林森 一份耕耘,一份收获 博客园 首页 新随笔 联系 管理 订阅 随笔- 397 文章- 0 评论- 78 JAVA基础学习day16--集合三-Map.HashMap,TreeMap与常用A ...
- OVS编译
下载源码 # git clone https://github.com/openvswitch/ovs.git # cd ovs # git checkout branch-2.8 下载依赖包 # y ...
- PHP面向对象--接口实例
我们设计一个在线销售系统,用户部分设计如下: 将用户分为,NormalUser, VipUser, InnerUser三种. 要求根据用户的不同折扣计算用户购买产品的价格. 并要求为以后扩展和维护预留 ...
- Listbox 实现Item双击事件
void listBox1_MouseDoubleClick(object sender, MouseEventArgs e) { int index = this.listBox1.IndexFro ...
- 在windows IIS服务商配置asp.net core的服务器坏境,并部署
翻译自https://docs.microsoft.com/en-us/aspnet/core/publishing/iis 另一篇参考的文章 http://www.c-sharpcorner.com ...
- 【面试】迄今为止把同步/异步/阻塞/非阻塞/BIO/NIO/AIO讲的这么清楚的好文章(快快珍藏)
常规的误区 假设有一个展示用户详情的需求,分两步,先调用一个HTTP接口拿到详情数据,然后使用适合的视图展示详情数据. 如果网速很慢,代码发起一个HTTP请求后,就卡住不动了,直到十几秒后才拿到HTT ...