ThreadLocal源码及相关问题分析
前言
在高并发的环境下,当我们使用一个公共的变量时如果不加锁会出现并发问题,例如SimpleDateFormat,但是加锁的话会影响性能,对于这种情况我们可以使用ThreadLocal。ThreadLocal是将公共变量copy一份到线程私有内存中以消除并发问题,ThreadLocal是JDK内部提供的高效解决并发问题的工具类之一,本文介绍ThreadLocal的重要方法的源码实现以及相关问题的分析。
数据结构
由上图可以看出,在Thread中维护了一个Entry的列表,Entry存储的是公共变量的copy,这个列表是由ThreadLocal维护,每次从ThreadLocal中获取的是一个公共变量的副本,所以避免了并发问题,接下来在看实现方法之前我们先看一下上面提到的类的定义。
变量
首先来看一下ThreadLocal重要的成员变量。
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}
threadLocalHashCode是ThreadLocal实例的ID,其主要用于做散列(后面会讲到),从上面可以看出来threadLocalHashCode是所有ThreadLocal贡献的原子变量nextHashCode加上一个固定的HASH_INCREMENT生成的。为什么
HASH_INCREMENT的值是0x61c88647?
前面说了,ThreadLocal是使用散列做存储的,而这个数字可以让生成出来的ThreadLocal的ID较为均匀地分布在大小为2的N次方的数组中。
接下来看一下ThreadLocal中真正存储数据的Entry类。
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
} private static final int INITIAL_CAPACITY = 16; private Entry[] table; private int size = 0; private int threshold;
}
ThreadLocalMap是ThreadLocal的内部类,Entry继承WeakReference,前面提到Entry是K-V形式,key就是WeakReference的成员变量referent,value就是Entry的value。我们可以发现,key是一个弱引用,它的生命周期到下一个GC就结束了,为什么要这样设计呢?在后面的内存泄漏中会提到。
table是一个大小为2的N次方的数组,threshold是数组扩容的临界点(与HashMap一个作用),默认是数组的大小的2/3,数组每次扩容是将长度扩大1倍。
方法
ThreadLocal重要的方法如下:
public T get(); //获取值 public void set(T value); //设置值 public void remove(); //删除值
首先来看set()方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //获取当前Thread中的ThreadLocalMap
if (map != null)
map.set(this, value); //设置值
else
createMap(t, value); //创建一个ThreadLocalMap
}
在Thread类中有 ThreadLocal.ThreadLocalMap threadLocals = null; 这样的成员变量,就是用来存储当前线程设置的值,所有的ThreadLocal都是在操作这个成员变量。
接下来看一下map.set()这个方法:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len - 1); // 计算hash散列 // 从i开始遍历列表
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到了key值
if (k == key) {
e.value = value;
return;
}
// key=null表示该key值已被回收
if (k == null) {
replaceStaleEntry(key, value, i); return;
}
}
// 当遍历到一个可以插入数据的空位置时
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) // 清理列表中已经被GC回收的
&& sz >= threshold) // 判断是否需要扩容
rehash();// 扩容并重新计算hash散列
}
ThreadLocal通过遍历Entry数组,如果当前key已存在则覆盖,没有则新增,如果在遍历过程中遇到已经被GC回收的key,则会清除掉无效的key对应的值。
从上面的循环可以看出来,ThreadLocal采用hash散列的线性探测存储,这种方式实现简单但是无法存储大量的数据,所以不建议用ThreadLocal存储大量的数据。这里还有一个问题是,nextIndex()方法是循环的获取数组下标,所以如果table满的就会导致无限循环,所以threshold的值是小于table的大小并且每次set之后都会清理一次数组的无效数据。
我们先看一下cleanSomeSlots()方法:
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i); // 从下标i开始清除数组中的无效元素
}
} while ( (n >>>= 1) != 0); // 该循环会循环log2(n)次
return removed;
}
该方法是通过线性查找,从下标为i开始查找已经被GC清理的key对应的值,一般情况下会查找log2(n)次,expungeStaleEntry()是真正的清理方法,源码如下:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length; // 清理对应数据
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--; // 重新计算hash散列,直到遇见null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
前面提到,ThreadLocal存储数据的方式是使用线性探测法,所以expungeStaleEntry()方法在清理掉一个数据之后会将该下标之后的所有非null位置重新计算一次hash散列,经过这样的操作之后,数组中的元素最终会符合hash散列的要求,如果不重新计算一次hash散列,那么最终数组结果可能不符合hash散列的要求,比如:元素a、b计算后得到存储位置冲突,通过线性探测法,最终结果是元素a在下标0,元素b在下标1,如果删除a之后,b就应该放在下标0而不是下标1上。
我们在回过头来看看map.set()方法中的replaceStaleEntry()方法:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// slotToExpunge用来记录清除无效key的开始位置,初始值等于staleSlot,staleSlot的值是无效key的下标
int slotToExpunge = staleSlot;
// 向前查找无效的key
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i; // 向后查找无效的key,直到遍历到一个有效key
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get(); // 如果在查找的过程中找到了我们需要找的key,则将无效key与该值替换
if (k == key) {
e.value = value; tab[i] = tab[staleSlot];
tab[staleSlot] = e; // 判断在向前查找无效key的过程中有没有找到
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 清理无效key
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果向前没有查找到无效key并且当前key为无效key
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
} // 将需要插入的数据插入到staleSlot位置
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value); // 如果向前查找到了无效key或者向后查找到了无效key,则清理无效key
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
该方法时在set数据值遍历到了一个无效key执行的方法,ThreadLocal认为,出现无效key的位置附近也会出现无效key,所以在清理该无效key的时候回查找附近连续的无效key。
值得注意的是,代码中找到了我们需要找的key的时候,将无效key与有效key交换的原因是,清除方法expungeStaleEntry()只能够清除连续的无效的key,如果向前没有找到无效key,向后找到了无效key的情况下,会出现如下情况:
SS是staleSlot,SE是slotToExpunge,如果不交换,则无法清理到SS位置的无效key,而且需要找的key后面同样也可能会出现无效key,同样无法清除到。
除了这种情况,还有一种情况是向前找到了无效key,会出现如下情况:
这种情况下,交换了SS跟需要找的key之后会出现无法清理SS位置的无效key的请款,所以代码中不止调用了一次expungeStaleEntry(),还会调用cleanSomeSlots(),源码在前面已经分析过了,这样就可以把SS以及之后的无效key都清掉。
以上set()方法的源码就分析完了,接下来是get()方法,get()方法是调用ThreadLocalMap的getEntry()方法,所以我们直接分析getEntry()方法:
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); // hash散列之后的位置不是需要找的key,即发生了hash碰撞
} private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length; while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i); // 清理无效key
else
i = nextIndex(i, len); // 向后遍历
e = tab[i];
}
return null;
}
get()方法较为简单,这里不做过多赘述,接下来是remove()方法,同样该方法是直接调用的ThreadLocalMap的remove()方法,源码如下:
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();// 将Entry的key置为null
expungeStaleEntry(i); // 清理该key
return;
}
}
}
为什么会内存泄漏
我们已经知道Entry的key是一个弱引用,当他被GC之后,那么这个Entry就是一个无效的对象,无法被外部获取到。而在Thread对象中是持有了ThreadLocalMap的引用,而ThreadLocalMap又持有了Entry数组的引用,所以该无效的Entry是无法被回收的,如果不手动的删除就会发生内存泄漏,好在ThreadLocal在set(),get()方法中都会主动去清理无效的Entry防止内存泄漏,但是依然会存在内存泄漏的风险,所以在编程中需要注意remove()不需要的值
为什么使用弱引用
弱引用可以防止内存泄漏。试想,如果一个线程中的ThreadLocal已经不需要了,所以将指向ThreadLocal的强引用删掉,希望GC清理调它。如果Entry是强引用的话GC就无法清理该对象,因为还存在一个Thread->ThreadLocalMap->Entry->ThreadLocal的强引用链,导致整个Entry都不会被回收,从而发生内存泄漏。
线程池与ThreadLocal共有引发的问题
为了方便分析,先假设线程池是固定大小的,当我们在从线程池中拿了一个线程运行如下逻辑:
if(threadLocal.get()!=null){
//doSomething
}else{
threadLocal.set(data);
}
或许你在最开始程序可以正常运行,但是后面你会发现代码 threadLocal.get() 永远都不为null,这是因为线程池中的线程时复用的,所以Thread就不会被回收,Thread不回收那么对应ThreadLocal也不会被回收,所以最后就出现了上述问题。在使用Tomcat等使用线程池的技术时需要注意这一点。
ThreadLocal源码及相关问题分析的更多相关文章
- 并发编程(四)—— ThreadLocal源码分析及内存泄露预防
今天我们一起探讨下ThreadLocal的实现原理和源码分析.首先,本文先谈一下对ThreadLocal的理解,然后根据ThreadLocal类的源码分析了其实现原理和使用需要注意的地方,最后给出了两 ...
- Java多线程学习之ThreadLocal源码分析
0.概述 ThreadLocal,即线程本地变量,是一个以ThreadLocal对象为键.任意对象为值的存储结构.它可以将变量绑定到特定的线程上,使每个线程都拥有改变量的一个拷贝,各线程相同变量间互不 ...
- Java并发编程之ThreadLocal源码分析
## 1 一句话概括ThreadLocal<font face="微软雅黑" size=4> 什么是ThreadLocal?顾名思义:线程本地变量,它为每个使用该对象 ...
- ThreadLocal详解,ThreadLocal源码分析,ThreadLocal图解
本文脉路: 概念阐释 ----> 原理图解 ------> 源码分析 ------> 思路整理 ----> 其他补充. 一.概念阐述. ThreadLocal 是一个为 ...
- 【JAVA】ThreadLocal源码分析
ThreadLocal内部是用一张哈希表来存储: static class ThreadLocalMap { static class Entry extends WeakReference<T ...
- 并发-ThreadLocal源码分析
ThreadLocal源码分析 参考: http://www.cnblogs.com/dolphin0520/p/3920407.html https://www.cnblogs.com/coshah ...
- Java -- 基于JDK1.8的ThreadLocal源码分析
1,最近在做一个需求的时候需要对外部暴露一个值得应用 ,一般来说直接写个单例,将这个成员变量的值暴露出去就ok了,但是当时突然灵机一动(现在回想是个多余的想法),想到handle源码里面有使用过Th ...
- ThreadLocal源码分析-黄金分割数的使用
前提 最近接触到的一个项目要兼容新老系统,最终采用了ThreadLocal(实际上用的是InheritableThreadLocal)用于在子线程获取父线程中共享的变量.问题是解决了,但是后来发现对T ...
- Threadlocal源码分析以及其中WeakReference作用分析
今天在看Spring 3.x企业应用开发实战,第九章 Spring的事务管理,9.2.2节ThreadLocal的接口方法时,书上有提到Threadlocal的简单实现,我就去看了下JDK1.8的Th ...
随机推荐
- python爬虫案例:使用XPath爬网页图片
用XPath来做一个简单的爬虫,尝试爬取某个贴吧里的所有帖子,并且将该这个帖子里每个楼层发布的图片下载到本地. # -*- coding:utf-8 -*- import urllib import ...
- 这42个Python小例子,太走心
告别枯燥,60秒学会一个Python小例子.奔着此出发点,我在过去1个月,将平时经常使用的代码段换为小例子,分享出来后受到大家的喜欢. 一.基本操作 1 链式比较 i = 3print(1 < ...
- IfcWallStandardCase 构件吊装模拟
wall_node = (osg::Node*)(index_node->clone(osg::CopyOp::DEEP_COPY_ALL));vc_mobileCrane->tranMo ...
- opengl读取灰度图生成三维地形并添加光照
转自:https://www.cnblogs.com/gucheng/p/10152889.html 准备第三方库 glew.freeglut.glm.opencv 准备一张灰度图 最终效果 代码如下 ...
- SpringBoot系列教程web篇之Freemaker环境搭建
现在的开发现状比较流行前后端分离,使用springboot搭建一个提供rest接口的后端服务特别简单,引入spring-boot-starter-web依赖即可.那么在不分离的场景下,比如要开发一个后 ...
- Maven专题
Maven 教程之 settings.xml 详解
- LeetCode 513. 找树左下角的值(Find Bottom Left Tree Value)
513. 找树左下角的值 513. Find Bottom Left Tree Value 题目描述 给定一个二叉树,在树的最后一行找到最左边的值. LeetCode513. Find Bottom ...
- ES6新增的一些特性
1.let关键字,用来代替 var的关键字,特点: 1.变量不允许被重复定义 2.不会进行变量声明提升 3.保留块级作用域中i的 2.const定义常量,特点:1.常量值不允许被改变 2.不会进行变量 ...
- [转帖]Redis性能解析--Redis为什么那么快?
Redis性能解析--Redis为什么那么快? https://www.cnblogs.com/xlecho/p/11832118.html echo编辑整理,欢迎转载,转载请声明文章来源.欢迎添加e ...
- python 之 Django框架(路由系统、include、命名URL和URL反向解析、命名空间模式)
12.36 Django的路由系统 基本格式: from django.conf.urls import url urlpatterns = [ url(正则表达式, views视图函数,参数,别名) ...