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条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS关闭SELinux安全模块
- CentOS8安装Docker,最新的服务器搭配容器使用
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- Hadoop3单机部署,实现最简伪集群
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Linux系统CentOS6、CentOS7手动修改IP地址
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Windows10,CentOS7,CentOS8安装Nodejs环境
- 设置Eclipse缩进为4个空格,增强代码规范