java源码-ThreadLocal
开篇
ThreadLocal主要解决的问题是给每个线程绑定自己的值,这个值是和线程绑定的,是线程的局部变量,是其他线程没法访问的。
ThreadLocal的源码的核心知识点在于ThreadLocal变量如何跟线程绑定和ThreadLocal如何实现gc垃圾回收,这篇文章希望能够讲解清楚这两个知识点。
ThreadLocal的实现原理:每一个线程在使用ThreadLocal的时候实际上是以ThreadLocal对象作为key值共享的对象为value值保存在Thread.threadLocalMaps变量中也就是ThreadLocalMap实例,因此ThreadLocal仅仅是作为一个key值来保存,多线程在使用同一个ThreadLocal或者不同的ThreadLocal但是保存相同的共享对象时他们的threadLocalMaps值是相同的,因此如果有共享资源发生冲突问题,ThreadLocal并不能解决!如果要解决并发冲突的问题要么使用安全对象,要么使用上锁机制来保证多线程顺序访问!
ThreadLocal的案例
通过下面例子可以对ThreadLocal的有一个直观的了解。
- 案例一:主线程main当中存在对象StudentInfo而子线程Thread-0当中不存在该变量,因为主线程通过set设置对象。
- 案例二:主线程main当中的对象StudentInfo通过set设置而子线程Thread-0当中的对象是初始化函数提供的,两者也不相同。
/** 创建一个ThradLocal实例 */
private static ThreadLocal<StudentInfo> threadLocal = new ThreadLocal<StudentInfo>();
public static void main(String[] args) {
StudentInfo info = new StudentInfo("sdew23", "张三", "男");
// 为主线程保存一个副本StudentInfo对象
threadLocal.set(info);
System.out.println(threadLocal.get());
System.out.println(Thread.currentThread().getName());
// 开启子线程
new Thread(new Runnable() {
@Override
public void run() {
// threadLocal.set(info);
System.out.println(threadLocal.get());
System.out.println(Thread.currentThread().getName());
}
}).start();
}
/** 创建一个ThradLocal实例 */
private static ThreadLocal<StudentInfo> threadLocal = new ThreadLocal<StudentInfo>() {
@Override
public StudentInfo initialValue() {
return new StudentInfo("sss", "小西", "女");
}
};
public static void main(String[] args) {
StudentInfo info = new StudentInfo("sdew23", "张三", "男");
// 为主线程保存一个副本StudentInfo对象
threadLocal.set(info);
System.out.println(threadLocal.get());
System.out.println(Thread.currentThread().getName());
// 开启子线程
new Thread(new Runnable() {
@Override
public void run() {
// threadLocal.set(info);
System.out.println(threadLocal.get());
System.out.println(Thread.currentThread().getName());
}
}).start();
}
ThreadLocal线程隔离的原理
ThreadLocal之所以能够实现线程隔离主要是因为在Thread的类变量当中存在一个ThreadLocal.ThreadLocalMap threadLocals类型对象,这样保证了每个线程能够单独维护一份ThreadLocal对象的map,也就自然而然实现了线程隔离。
ThreadLocal对象的set操作获取当前线程的ThreadLocal.ThreadLocalMap对象,以ThreadLocal对象作为key,以ThreadLocal保存的对象如下例中的StudentInfo为value,保存到ThreadLocalMap当中,每个线程执行set操作都是把对象保存至对应的ThreadLocalMap,所以也就解释了为什么能够隔离了。
ThreadLocal.ThreadLocalMap采用环形数组实现,也就是Entry[] table,set操作就是创建一个Entry对象然后放到环形数组table当中,通过线性探测方法解决冲突问题。
// 每个线程包含一个ThreadLocal.ThreadLocalMap对象
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
{
private static ThreadLocal<StudentInfo> threadLocal = new ThreadLocal<StudentInfo>();
StudentInfo info = new StudentInfo("sdew23", "张三", "男");
threadLocal.set(info);
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
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();
}
}
}
ThreadLocal的get操作
ThreadLocal的get操作从当前Thread获取ThreadLocal.ThreadLocalMap threadLocals对象,在ThreadLocalMap中以ThreadLocal对象作为key返回Entry也即ThreadLocal当中保存的value。
ThreadLocal的get操作内部考虑线性探测方法来解决存储时候的冲突问题,第一次以hash值取值成功则直接返回,如果未取到那么就通过线性探测法继续查找直到null值为止。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
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();
}
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);
}
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);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
ThreadLocal的gc清理
ThreadLocal的key回收
ThreadLocal的垃圾回收机制是它的一大设计亮点,也是面试当中经常会被问到的问题,这里我们从两个维度进行说明。
- ThreadLocal当中的Entry的key实现了WeakReference弱引用,所以gc机制可以对key进行回收
- ThreadLocal当中的Entry的value由于没实现WeakReference弱引用,所以只有程序主动进行回收,在set/get操作中实现回收。
- 关于回收的细节可以参考文章《一篇文章,从源码深入详解ThreadLocal内存泄漏问题》,作者讲解的非常清楚给我很大启发。
// 关于key的弱引用实现机制
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal的value回收-get阶段
getEntryAfterMiss过程当中会调用expungeStaleEntry()方法删除过期数据,判断过期数据的标准是对Entry.get()方法返回的key为null(被jvm的gc主动回收了),这个时候需要把对应的value也进行删除。
ThreadLocal的expungeStaleEntry()方法内部除了删除staleSlot指定的过期数据外,还负责检查往后遍历直至遇到数组元素为null停止。在删除过程中还会对某些未过期的数据进行重hash,我认为之所以重hash有可能之前通过线性探测法往后放的,通过重hash后如果发现原来位置为空则可以直接放置hash值对应的下标位置。之所以这么做我个人觉得是因为get或者set的依据都是以null作为结束依据。
get时候重hash是为了把元素放置到hash值第一次对应的位置当中(未经过线性探测法解决碰撞冲突),这样就可以支持以null作为结尾的依据了。
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);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter 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;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
ThreadLocal的value回收过程
cleanSomeSlots以i作为起始地址,expungeStaleEntry()完成i位置的元素的gc回收,然后继续遍历回收直至遇到null元素的下标i,然后由cleanSomeSlots继续nextIndex()下移到下一个位置继续查找。
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);
}
} while ( (n >>>= 1) != 0);
return removed;
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter 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;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
说明:
1、如图当前n等于hash表的size即n=10,i=1,在第一趟搜索过程中通过nextIndex,i指向了索引为2的位置,此时table[2]为null,说明第一趟未发现脏entry,则第一趟结束进行第二趟的搜索。
2、第二趟所搜先通过nextIndex方法,索引由2的位置变成了i=3,当前table[3]!=null但是该entry的key为null,说明找到了一个脏entry,先将n置为哈希表的长度len,然后继续调用expungeStaleEntry方法,该方法会将当前索引为3的脏entry给清除掉(令value为null,并且table[3]也为null),但是该方法可不想偷懒,它会继续往后环形搜索,往后会发现索引为4,5的位置的entry同样为脏entry,索引为6的位置的entry不是脏entry保持不变,直至i=7的时候此处table[7]位null,该方法就以i=7返回。至此,第二趟搜索结束;
3、由于在第二趟搜索中发现脏entry,n增大为数组的长度len,因此扩大搜索范围(增大循环次数)继续向后环形搜索;
4、直到在整个搜索范围里都未发现脏entry,cleanSomeSlot方法执行结束退出。
参考文章

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
Java记忆篇 - 关键字与保留字
共“53”个关键字(含2个保留字) 保留字 1).const有道释义:n.常量,常数 用于修改字段或局部变量的声明。它指定字段或局部变量的值是常数,不能被修改 2).goto有道释义:vi.转到 指定跳转到标签,找到标签后,程序将处理从下一行开始的命令。 访问修饰符的关键字(共3个) 定义类、接口、抽象类和实现接口、继承类的关键字、实例化对象(共6个) 包的关键字(共2个) 数据类型的关键字(共12个) 条件循环(流程控制)(共12个) 修饰方法、类、属性和变量(共9个) volatile 1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去; 2.这个写会操作会导致其他线程中的缓存无效。 上面的例子只需将status声明为volatile,即可保证在线程A将其修改为true时,线程B可以立刻得知 volatile boolean status = false; 错误处理(共5个) 其他 publicenumColor{RED,BLUE,GREEN,BLACK;}
-
下一篇
Python数据分析及可视化-小测验
本文中测验需要的文件夹下载链接: https://pan.baidu.com/s/1OqFM2TNY75iOST6fBlm6jw 密码: rmbt 下载压缩包后解压如下图所示: image.png 首先将5题的文件复制形成副本,如下图所示: image.png 在资源管理器的路径中输入cmd,如下图所示: image.png 在上图中输入后,按Enter键运行进入cmd窗口。 在cmd窗口中输入并运行命令: jupyter notebook,如下图所示: image.png 在上图中输入后,按Enter键运行自动打开浏览器并且进入jupyter notebook编程界面。 在jupyter notebook中,点击 第一题,ipynb和 第一题-副本.ipynb。 浏览器会新建两个标签页,如下图所示: image.png 在两个标签页中,读者可以对照题目要求完成做题。 下面是5道题目作者的答案和解析。 1.第一大题 1.1 第一步:导入相应的模块 最后2行代码可以使作图时不出现编码错误,分别用来正常显示中文标签和正常显示负号。 import pandas as pd from pand...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker容器配置,解决镜像无法拉取问题
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- 2048小游戏-低调大师作品
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- MySQL数据库在高并发下的优化方案
- Dcoker安装(在线仓库),最新的服务器搭配容器使用
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题