Java ThreadLocal解析

ThreadLocal 线程本地变量, 线程私有, 在 Thread 类中用 ThreadLocal.ThreadLocalMap threadLocals 以数组的形式存储. 因为是线程私有的变量, 所以不会有多线程访问的线程安全问题, 下面就对它开始解析(JDK1.8 的 ThreadLocal).

threadLocalHashCode

在说 ThreadLocal 的使用之前, 先说一个比较重要的东西, 就是 threadLocalHashCode, 这个hashCode 的值用来计算 当前 ThreadLocal 应当位于 ThreadLocalMap 的数组中的下标 , 为什么是应当呢, 请往下看

	// ThreadLocalHashCode 递增值
private static final int HASH_INCREMENT = 0x61c88647; /**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// 每个 ThreadLocal 的 threadLocalHashCode 通过一个静态的原子变量递增而获得
private final int threadLocalHashCode = nextHashCode();

ThreadLocal#set 方法

一个 ThreadLocalMap 可以存储多个 ThreadLocal 变量, 存取的时候将 ThreadLocal 作为key 放入 Entry 中, ThreadLocalMap 存储数据的对象为 Entry[], 计算 key 所属的下标用 int i = key.threadLocalHashCode & (len-1) 计算, 如果该下标已有数据, 根据 key(ThreadLocal变量) 判断是否属于这个 ThreadLocal, 不是同一个 ThreadLocal 则按顺序往后查找为空(或者 ThreadLocal 已经被GC回收)的 Entry设置 value.下面是 set方法的比较重要的部分源代码:

	// 最开始计算的下标i
int i = key.threadLocalHashCode & (len-1);
// 从计算出来的数组中的第 i 个开始往后找, i > len-1 的时候就从0开始
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// nextIndex(i,len): ((i + 1 < len) ? i + 1 : 0); i+1>=len则从下标0开始
ThreadLocal<?> k = e.get();
// 根据 key 判断是否为当前的 ThreadLocal
if (k == key) {
e.value = value;
return;
}
// Entry 的 key 为空, 但是上面循环的判断是 Entry 不为空, 表明作为 Entry 的 key的 ThreadLocal 已经被 GC 回收, 所以此时设置新的 key(ThreadLocal) 和 value.
if (k == null) {
// 替换掉旧的 ThreadLocal
replaceStaleEntry(key, value, i);
return;
}
// 已经找到为空的 Entry, 直接 new 一个 Entry
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 扩容将所有的 Entry 重新设置
rehash();
}

ThreadLocal#get 方法

get 方法跟 set 方法还是有挺多相同处的, 看代码

	private Entry getEntry(ThreadLocal<?> key) {
// 计算下标
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 如果对应下标的 Entry 属于当前 ThreadLocal 的话, 直接返回结果
if (e != null && e.get() == key)
return e;
else
// 会到这里的原因是因为 set 的时候的 hash 冲突, 冲突后会往后查找(超过len-1则从0开始)可以放置的位置, 所以前面初始化 hashCode 的时候是用的应当位于的位置
return getEntryAfterMiss(key, i, e);
}
// e 为应当属于它的 Entry
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//
while (e != null) {
ThreadLocal<?> k = e.get();
// 查找到属于自己的 value
if (k == key)
return e;
// 如果在查找的时候发现有的 Entry 的 key(ThreadLocal) 已经被 GC 回收, 则进行回收
if (k == null)
expungeStaleEntry(i);
else
// 往后查找, 超过 len-1 则从 0 开始继续
i = nextIndex(i, len);
e = tab[i];
}
return null;
}

关于ThreadLocal 的 WeakReference

在说 ThreadLocal 的 WeakReference 之前, 先简单介绍一下 WeakReference .

WeakReference

弱引用不同于 java 中用 new创建(强引用) 的对象, 当对象只有弱引用(weakReference) 的时候, 进行GC的时候, 会对其进行回收(没有强引用的对象均会被gc回收)

	Object o = new Object();
WeakReference weakReference = new WeakReference(o);
System.out.println("beforeGC: "+weakReference.get());
o = null;
System.gc();
System.out.println("afterGC: "+weakReference.get()); Object o2 = new Object();
WeakReference weakReference2 = new WeakReference(o2);
System.out.println("beforeGC2: "+weakReference2.get());
System.gc();
System.out.println("afterGC2: "+weakReference2.get());

输出结果是:

beforeGC: java.lang.Object@5e2de80c

afterGC: null

beforeGC: java.lang.Object@1d44bcfa

afterGC: java.lang.Object@1d44bcfa

从这里可以看出, 即使有 weakFerence 弱引用, 也不能阻止对象被 GC 回收掉(因为它很弱啊哈哈).

ThreadLocal 中使用的 WeakReference

从上面set的方法看到了一行 tab[i] = new Entry(key, value)

Entry 是一个静态类

	// Entry 继承 WeakReference, 多了个 value 记录 ThreadLocal 的值
static class Entry extends WeakReference<ThreadLocal<?>> {
// 这个值就是 ThreadLocal set 进来的值
Object value;
Entry(ThreadLocal<?> k, Object v) {
// 将 ThreadLocal 设置为引用对象
super(k);
value = v;
}
}

super(k)就是将 ThreadLocal 设置为引用对象, 获取的时候,调用父类的get方法, entry.get() --> 返回的就是引用的对象(这里就是 ThreadLocal)

	// super(k)
Reference(T referent) {
this(referent, null);
}
// set reference
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
// get
public T get() {
return this.referent;
}

代码看起来难度不大, 下面说一下 JDK 的 ThreadLocal 使用不当会导致内存泄漏的问题.

在上面的 set 和 get 方法中, 有这么(类似)一段代码:

	for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// nextIndex(i,len): ((i + 1 < len) ? i + 1 : 0); i+1>=len则从下标0开始
ThreadLocal<?> k = e.get();
// 根据 key 判断是否为当前的 ThreadLocal
if (k == key) {
e.value = value;
return;
}
// Entry 的 key 为空, 但是上面循环的判断是 Entry 不为空, 表明作为 Entry 的 key的 ThreadLocal 已经被 GC 回收, 所以此时设置新的 key(ThreadLocal) 和 value.
if (k == null) {
// 替换掉旧的 ThreadLocal
replaceStaleEntry(key, value, i);
return;
}

if(k == null) 这里, 加上外层循环就是 if(e!=null && k == null), Entry不为空表明之前设置过 ThreadLocal 的值, 但是 k(key)==null 也就是 ThreadLocal == null, 为什么ThreadLocal 会为空? 前面我们说到 Entry 里面对 ThreadLocal(key) 是使用的 弱引用 , 也就是 Entry 持有 ThreadLocal 的引用并不会阻止GC 将 ThreadLocal 回收掉, 如果 ThreadLocal 手动置为 null , 则失去了强引用, 若其他地方没有这个 ThreadLocal 的强引用, 这个ThreadLocal 将会被GC回收掉, 此时 Entry 里面的 key == null, 但是 Entry 对 value 是强引用, 所以 value 不会被GC回收掉, 此时就造成了 Entry 的 key==null 但是 value !=null, 如果没有其它的 ThreadLocal 被设置进(或者get)同一个 Entry 的话, 这个 Entry 将会一直持有无用的 value, 造成内存泄漏, 泄漏的过多最终会导致 OOM.

解决办法呢就是在 ThreadLocal 置为 null之前, 先调用 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)]) {
// 找到与自己匹配的 Entry
if (e.get() == key) {
e.clear();
// 这个方法名也说的很清楚了, 删掉陈旧的 Entry
expungeStaleEntry(i);
return;
}
}
}

所以使用 JDK 的 ThreadLocal , 在不需要的时候需要手动调用 remove() 方法, 防止导致内存泄漏, 最终导致 OOM.

最后

这次的内容到这里就结束了,最后的最后,非常感谢你们能看到这里!!你们的阅读都是对作者的一次肯定!!!

觉得文章有帮助的看官顺手点个赞再走呗(终于暴露了我就是来骗赞的(◒。◒)),你们的每个赞对作者来说都非常重要(异常真实),都是对作者写作的一次肯定(double)!!!

JDK ThreadLocal解析的更多相关文章

  1. 并发之线程封闭与ThreadLocal解析

    并发之线程封闭与ThreadLocal解析 什么是线程封闭 实现一个好的并发并非易事,最好的并发代码就是尽量避免并发.而避免并发的最好办法就是线程封闭,那什么是线程封闭呢? 线程封闭(thread c ...

  2. JDK环境解析,安装和目的

    目录 1. JDK环境解析 1.1 JVM 1.2 JRE 1.3 JDK 2. JDK安装 2.1 为什么使用JDK8 2.1.1 更新 2.1.2 稳定 2.1.3 需求 2.2 安装JDK 2. ...

  3. Java并发编程之ThreadLocal解析

    本文讨论的是JDK 1.8中的ThreadLocal ThreadLocal概念 ThreadLocal多线程间并发访问变量的解决方案,为每个线程提供变量的副本,用空间换时间. ThreadLocal ...

  4. Java ThreadLocal解析

    简介 ThreadLocal 类似局部变量,解决了单个线程维护自己线程内的变量值(存.取.删),让线程之间的数据进行隔离.(InheritableThreadLocal 特例) 这里涉及三个类,Thr ...

  5. ThreadLocal解析

    ThreadLocal 如果定义了一个单实例的java bean,它有若干属性,但是有一个属性不是线程安全的,比如说HashMap.并且碰巧你并不需要在不同的线程中共享这个属性,也就是说这个属性不存在 ...

  6. 在Linux下安装JDK图文解析

    参考自http://weixiaolu.iteye.com/blog/1401786 JDK官网下载:http://blog.csdn.net/chongxin1/article/details/70 ...

  7. Ubuntu下安装JDK图文解析

    我们在64位的Ubuntu中安装JDK,选择的是jdk1.6.0_32版本号.安装文件名称为jdk-6u32-linux-x64.bin(这个是64位系统的),假设是32位系统的还须要去官网下载32位 ...

  8. ThreadLocal解析:父线程的本地变量不能传递到子线程详解

    众所周知,ThreadLocal类是java提供线程本地变量的工具类.但父线程的本地变量却不能被子线程使用,代码如下: public static void main(String[] args) { ...

  9. Ubuntu下安装JDK以及相关配置

    1.查看系统位数,输入以下命令即可 getconf LONG_BIT 2.下载对应的JDK文件,我这里下载的是jdk-8u60-linux-64.tar.gz 3.创建目录作为JDK的安装目录,这里选 ...

随机推荐

  1. Mac 使用自带php和Apache 安装配置Xdebug 开启本地调试模式

    Mac 安装配置php xdebug 本地调试 0.原理图 https://paper.seebug.org/308/ 测试demo构建方法 新建空白项目,目录选择Apache默认项目目录 1.下载x ...

  2. Servlet核心内容

    Servlet重要的四个生命周期方法 构造方法: 创建servlet对象的时候调用.默认情况下,第一次访问servlet的时候创建servlet对象只调用1次.证明servlet对象在tomcat是单 ...

  3. Apache中commons包的各种jar的功能说明

    commons-logging.jar -----记录日志,通常和  log4j.jar共同使用 commons-beanutils.jar(1.1)                主要提供Bean的 ...

  4. JS实现判断点是否在多边形范围内

    一.说明 在GIS领域,判断点是否在多边形范围内是一个基础方法,这里主要说下实现原理. 原理比较简单,就是有一个GIS理论,一个点向一个方向发送射线,射线与多边形各个边相交的交点如果是奇数说明点在多边 ...

  5. Java执行cmd命令、bat脚本、linux命令,shell脚本等

    1.Windows下执行cmd命令 如复制 D:\tmp\my.txt 到D:\tmp\my_by_only_cmd.txt 现文件如图示: 执行代码: private static void run ...

  6. AppiumForMac安装2

    安装了两天的Python和Appium,各种不成功,到现在还不知道要怎么才能使用,两者之间的关联性还是没找到. 虽然很乱,还是把这期间用到的知识汇总下: 1.爬墙很重要,下载东西慢的很. 2.最新的M ...

  7. Feign 动态URL 解决记录

    Feign中使用动态URL请求 (应当是spring-cloud-starter-openfeign,不知道和一般的feign有何差别) 在spring项目下,假设有这样个Feign的消费接口,原来写 ...

  8. Codeforces Round #756 (Div. 3)

    本场战绩:+451 题目如下: A. Make Even time limit per test 1 second memory limit per test 256 megabytes input ...

  9. 【Linux/Oracle】ORA-00031:标记要终止的会话 解决

    在PL/SQL操作了一条delete语句用于删除这张1.4亿条数据的表,执行了12个小时还没删完 (经DB指导,量级大的需要使用truncate table table_name 进行删除) --查询 ...

  10. erange.heetian.com 回显任意账号

      首先获取你想登录ID的REG标识符,例如合天课程专家  获取标识符ba84d3c3-a4a1-4cd2-a00d-2f5722ee86a2 一般用户前缀为REG,这个肯定是管理员之类的= =.. ...