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. 入门 - k8s伸缩应用程序 (六)

    目标 使用 kubectl 伸缩应用程序. Scaling(伸缩)应用程序 在之前的文章中,我们创建了一个 Deployment,然后通过 服务 提供访问 Pod 的方式.我们发布的 Deployme ...

  2. winform 获得局域网内在线IP和计算机名,获取IP,多线程网络编程

    转载请注明来源:https://www.cnblogs.com/hookjc/ using System; using System.Collections.Generic; using System ...

  3. UITableView的全部属性、方法以及代理方法执行顺序,看过之后肯定有收获---董鑫

    UITableView-------表视图--继承UIScrollView并遵守NSCoding协议 属性 frame-------------设置控件的位置和大小 backgroundColor-- ...

  4. K8s多节点部署+负载均衡+keepalived ——囊萤映雪

    K8s多节点部署+负载均衡+keepalived --囊萤映雪 1.多节点master2 部署 2.负载均衡部署+keepalived 1.多节点master2部署: #从master01节点上拷贝证 ...

  5. async同步异步

    1.同步:var async = require("async"); async.series([step1, step2, step3],function(err, values ...

  6. 基于Oracle数据库登陆界面及功能实现 Java版

    首先要在Oracle数据库创建表文件,包括建立表头以及关键字(唯一标识符),此次程序所用的表名称为SW_USER_INFO,表头有UNAME.UKEY.USEX等,关键字为UCC,然后添加一条记录,用 ...

  7. MySQL 快速入门(一)

    目录 MySQL快速入门 简介 存储数据的演变过程 数据库分类 概念介绍 MySQL安装 MySQL命令初始 环境变量配置 MySQL环境变量配置 修改配置文件 设置新密码 忘记密码的情况 基本sql ...

  8. 7、Linux基础--权限、查看用户信息

    笔记 1.晨考 1.Linux系统中的文件"身份证号"是什么 index node 号码 2.什么是硬链接,什么是软连接 硬链接是文件的入口,软连接是快捷方式. 3.硬链接中保存的 ...

  9. Solution -「ARC 104F」Visibility Sequence

    \(\mathcal{Description}\)   Link.   给定 \(\{x_n\}\),对于满足 \(h_i\in[1,x_i]\) 的序列 \(\{h_n\}\),定义序列 \(\{p ...

  10. 如何在TypeScript/JavaScript项目里引入MD5校验和

    摘要:MD5校验和则是其中一种数学算法,通常是使用工具对文件计算得出的一组32 个字符的十六进制字母和数字. 本文分享自华为云社区<TypeScript/JavaScript项目里如何做MD5校 ...