Java并发-ThreadLocal

原文地址:cmsblogs.com/?p=2442

ThreadLocal介绍

ThreadLocal提供了一种解决多线程环境下成员变量的问题,但是它并不是解决多线程共享变量的问题。那么ThreadLocal到底是什么呢?

API是这样介绍的:This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

该类提供了线程局部(thread-local)变量。这些变量不同于普通对应物,因为访问某个变量(通过其get或set方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal实例通常是类中的private static字段,它们希望将状态与某一个线程(例如,用户ID或事务ID)相关联。

ThreadLocal与线程同步机制不同,线程同步机制是多个线程共享同一个变量,而ThreadLocal为了每一个线程创建一个单独的变量副本,故而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本。

ThreadLocal使用示例,代码如下:

public class SeqCount {private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>() {// 实现initialValue()public Integer initialValue() {return 0;}};public int nextSeq() {seqCount.set(seqCount.get() + 1);return seqCount.get();}public static void main(String[] args) {SeqCount seqCount = new SeqCount();SeqThread thread1 = new SeqThread(seqCount);SeqThread thread2 = new SeqThread(seqCount);SeqThread thread3 = new SeqThread(seqCount);SeqThread thread4 = new SeqThread(seqCount);thread1.start();thread2.start();thread3.start();thread4.start();}private static class SeqThread extends Thread {private SeqCount seqCount;SeqThread(SeqCount seqCount) {this.seqCount = seqCount;}public void run() {for (int i = 0; i < 3; i++) {System.out.println(Thread.currentThread().getName() + " seqCount :"+ seqCount.nextSeq());}}}
}
复制代码

ThreadLocal实现原理

ThreadLocal的实现是这样的:每个Thread维护一个ThreadLocalMap映射表,这个映射表的keyThreadLocal实例本身,value是真正需要存储的Object

也就是说ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取 value。值得注意的是图中的虚线,表示ThreadLocalMap是使用ThreadLocal的弱引用作为Key的,弱引用的对象在GC时会被回收。

ThreadLocal源码分析

ThreadLocalMap

ThreadLocalMap的构造函数如下:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);
}
复制代码

由上可知,ThreadLocalMap其内部利用Entry来实现key-value的存储,如下:

static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}
复制代码

可以看出Entry的key就是ThreadLocal,而value就是值。同时,Entry也继承WeakReference,所以说Entry所对应key(ThreadLocal实例)的引用为一个弱引用

接下来,看看ThreadLocalMap最核心的方法set(ThreadLocal> key, Object value)、getEntry()方法。

1、set(ThreadLocal<?> key, Object value)

private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;// 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置int i = key.threadLocalHashCode & (len-1);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,但是存在值(因为此处的e != null),说明之前的ThreadLocal对象已经被回收if (k == null) {// 用新元素替换陈旧的元素replaceStaleEntry(key, value, i);return;}}// ThreadLocal对应的key实例不存在则创建tab[i] = new Entry(key, value);int sz = ++size;// cleanSomeSlots 清楚陈旧的Entry(key == null)// 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehashif (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}复制代码

set()操作除了存储元素外,还有一个很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),这两个方法可以清除掉key == null 的实例,防止内存泄漏。在set()方法中还有一个变量很重要:threadLocalHashCode,定义如下:

private final int threadLocalHashCode = nextHashCode();
复制代码

从名字上面我们可以看出threadLocalHashCode应该是ThreadLocal的散列值,定义为final,表示ThreadLocal一旦创建其散列值就已经确定了,生成过程则是调用nextHashCode():

private static AtomicInteger nextHashCode = new AtomicInteger();private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
}
复制代码

nextHashCode表示分配下一个ThreadLocal实例的threadLocalHashCode的值,HASH_INCREMENT则表示分配两个ThradLocal实例的threadLocalHashCode的增量。

2、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;elsereturn getEntryAfterMiss(key, i, e);
}
复制代码

采用了开放定址法,所以当前key的散列值和元素在数组的索引并不是完全对应的,首先取一个探测数(key的散列值),如果所对应的key就是我们所要找的元素,则返回,否则调用getEntryAfterMiss()

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;// 当key == null时,调用了expungeStaleEntry()方法,该方法用于处理key == null,// 有利于GC回收,能够有效地避免内存泄漏。if (k == null)expungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}return null;}
复制代码

ThreadLocal核心方法

set(T value):设置当前线程的线程局部变量的值

public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}
复制代码

获取当前线程所对应的ThreadLocalMap,如果不为空,则调用ThreadLocalMap的set()方法,key就是当前ThreadLocal,如果不存在,则调用createMap()方法创建。

get():返回当前线程所对应的线程变量

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;}}// 如果ThreadLocalMap不存在,返回初始值。return setInitialValue();
}
复制代码

首先通过当前线程获取所对应的成员变量ThreadLocalMap,然后通过ThreadLocalMap获取当前ThreadLocalEntry,最后通过所获取的Entry获取目标值result。

initialValue():返回该线程局部变量的初始值

protected T initialValue() {return null;
}
复制代码

这个方法将在一个线程第一次使用get方法访问变量时被调用,除非线程先前调用了set方法,在这种情况下,线程不会调用initialValue方法。通常情况下,每个线程最多调用一次此方法,但在后续调用removeget时,可能会再次调用此方法。

默认实现返回null,如果程序员希望线程局部变量具有非null的初始值,则必须对ThreadLocal进行子类化,并重写此方法。

remove():将当前线程局部变量的值删除

public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);
}
复制代码

该方法的目的是减少内存的占用。当然,我们不需要显示调用该方法,因为一个线程结束后,它所对应的局部变量就会被垃圾回收。

ThreadLocal为什么会内存泄漏

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC的时候,这个ThreadLocal势必会被回收,ThreadLocalMap中就会出现keynullEntry,就没有办法访问这些keynullEntryvalue。如果当前线程再迟迟不结束的话,这些key为nullEntryvalue就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocalget(),set(),remove()的时候都会清除线程ThreadLocalMap里所有keynullvalue

但是这些被动的预防措施并不能保证不会内存泄漏:

  • 使用staticThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。

  • 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

理解了ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?

  • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

参考资料

  1. 【死磕Java并发】—–深入分析ThreadLocal

  2. 深入分析 ThreadLocal 内存泄漏问题


如果读完觉得有收获的话,欢迎点赞、关注、加公众号【牛觅技术】,查阅更多精彩历史!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/538632.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue 监听map数组变化_解决vue无法侦听数组及对象属性的变化问题

一、数组1、可以监听到的情况如push、splice、赋值(array[1,2,3])2、无法监听到的情况使用下标修改某个元素(这种比较常见)array[index] 1object.a 3直接修改数组lengtharray.length 53、解决方案this.$set(array, index, data) - 这是个深度的修改&#xff0c;某些情况下可…

哈维玛德学院 计算机,这些美国大学名气不高,却有最顶级的工程专业

上一期我们盘点了有博士学位的Top50 大学工程专业排名今天我们继续盘点 2018USNEWS 工程专业排名不过这个排名主要针对的是没有博士学位的 Top50 大学和上一期那些大名鼎鼎的大学相比这里面很多大学并不怎么为人熟知因为这个榜单大部分都是区域性大学事实上&#xff0c;工程专业…

Hbase介绍

1、为什么出现hbase&#xff1f;hadoop 的NameNode适合大文件&#xff0c;不适合小文件。HDFS不适合大量小文件的存储&#xff0c;因namenode将文件系统的元数据存放在内存中&#xff0c;因此存储的文件数目受限于 namenode的内存大小。HDFS中每个文件、目录、数据块占用150Byt…

math 向上取整_自我说明:关于Math和File类的具体说明.

Math类:Math类&#xff0c;不允许有子类&#xff0c;它直接继承于object.Math类包含执行基本数字运算的方法.如基本指数&#xff0c;对数&#xff0c;平方根和三角函数.Math的基本方法&#xff1a;System.out.pintln(“1.绝对值&#xff1a;”Math.abs(16)”t”Math.abs(-16)”…

眼图 非差分线_LVDS低电压差分信号简介

LVDS低电压差分信号简介1. 名词解释1.1. 背景随着数据传输速率越来越高&#xff0c;现在计算机系统中的数据传输接口基本上都串行化了&#xff0c;像USB、PCIe、SATA、DP等等外部总线将并行总线挤压到只剩下内存总线这个最后的堡垒。当然&#xff0c;就算是并行传输总线最后的倔…

无内存在优盘可以启动计算机吗,没有U盘不要紧,内存卡做启动盘装Win7方法

现在最流行的win7系统安装方法大多以U盘来安装&#xff0c;那么如果你没有U盘呢&#xff1f;为了安装个Win7系统是不是要去买个U盘&#xff1f;当然不用&#xff0c;如果你还有空闲不用的手机内存卡&#xff0c;那么也是可以制作U盘启动盘来安装Win7系统的。准备工具&#xff1…

微信小程序request请求动态获取数据

微信小程序开发文档链接 1 后台代码: clickButton:function(){var that this;wx.request({url: http://localhost:9096/admin.php/index/jj,method : POST,header: { content-type: application/x-www-form-urlencoded },data : {},success: function (res) {//console.log(re…

HBase中的HMaster、HRegionServer、Zookeeper

原文链接&#xff1a;http://blog.csdn.net/mm_bit/article/details/51304233 ----------------------------------- HMaster是Hbase主/从集群架构中的中央节点。通常一个HBase集群存在多个HMaster节点,其中一个为Active Master,其余为Backup Master. Hbase每时每刻只有一个hm…

spyder python调试_使用spyder编译器单步调试python

1、将需要进行单步调试的函数在脚本中进行调用&#xff08;十分重要的一步&#xff09;。由于python是解释型语言&#xff0c;在进行单步调试的时候需要告诉系统你使用了这个函数&#xff0c;单步调试才会进入你所需要调式的函数。如下图所示&#xff0c;我们定义了createDataS…

label qt 自动换行_QT编写一个登录界面

前言继上篇&#xff1a;一起学Qt之基础篇---入门今天上手实操用QT编写一个登录界面~系统权限这个词大家肯定不陌生&#xff0c;你进入一个网站也是&#xff0c;如果不登录&#xff0c;就是以游客的身份进去的&#xff0c;要想看到某些信息肯定需要进行登录&#xff0c;更完善的…

我的世界服务器怎么修改书与笔,我的世界书与笔怎么做 我的世界书与笔怎么用...

第一步先收集甘蔗&#xff0c;然后合成纸&#xff0c;甘蔗在河边&#xff0c;池塘边&#xff0c;沼泽地一般都会生成&#xff0c;我们也可以拿回家种植&#xff0c;但是甘蔗必须种在水边&#xff0c;其他地方种植不了&#xff0c;其他地方玩家怎么右键甘蔗都是没有反应的。第二…

python csv模块用法_python使用csv模块如何将数据存放在一张表的不同行?

def save2csv(file_nameNone, headerNone, dataNone): """ 保存成CSV格式文件,方便Excel直接打开 :param file_name: 保存的文件名 :param header: 表头,每一列的名字 :param data: 具体填充数据 :return: """ if file_name is None or isinstan…

虚拟机 服务器 root,虚拟机切换到root账户

虚拟机切换到root账户 内容精选换一换一、安装虚拟机1、下载VMware workstation 14 &#xff0c;安装时按照默认配置安装2、下载ubuntu-18.04.1-desktop-amd64.iso镜像文件3、打开 VMware workstation &#xff0c;新建虚拟机4、配置虚拟机内存&#xff0c;磁盘&#xff0c;网络…

每一行末尾添加分号

文本内容如下&#xff1a; TMP_TBX_100_0_A1 TMP_TBX_100_0_A10 TMP_TBX_100_0_A12 TMP_TBX_100_0_A13 TMP_TBX_100_0_A14 TMP_TBX_100_0_A15 TMP_TBX_100_0_A15_2 TMP_TBX_100_0_A16 TMP_TBX_100_0_A17 TMP_TBX_100_0_A18 TMP_TBX_100_0_A19 TMP_TBX_100_0_A19_2 TMP_TBX_100…

壮观霉素抗性基因原理_基因组学深入挖掘·研究方案(下篇)

前情回顾上次小编为大家讲解了四种以基因组为基础的多组学联合研究方案&#xff08;基因组与转录组&#xff0c;深入挖掘基因表达信息&#xff1b;基因组联合代谢组与转录组&#xff0c;锁定关键通路&#xff1b;基因组与群体进化&#xff0c;解析物种发展历程&#xff1b;基因…

Shell脚本大量示例

几乎所有的脚本里都有某种流控制结构&#xff0c;很少有例外。流控制是什么&#xff1f;假定有一个脚本&#xff0c;包含下列几个命令&#xff1a; #!/bin/sh # make a directory mkdir /home/dave/mydocs # copy all doc files cp *.docs /home/dave/docs # delete all doc fi…

Spark 常见问题小结

原文地址&#xff1a;http://www.aboutyun.com/thread-9946-1-1.html -------------------------------------- 问题导读 1、当前集群的可用资源不能满足应用程序的需求&#xff0c;怎么解决&#xff1f; 2、内存里堆的东西太多了&#xff0c;有什么好办法吗&#xff1f; …

dataframe数据标准化处理_数据预处理——标准化/归一化(实例)

这次我们来说说关于数据预处理中的数据标准化及归一化的问题。主要以理论实例的方式为大家展示。本次实验也将会展示部分数据以及代码&#xff0c;有兴趣的小伙伴可以自己动手试试~在本次实例过程中&#xff0c;我们使用的数据是&#xff1a;2010-2018年间广州市经济与环境的时…

python实现自动打电话软件_全自动手势联系软件 让你轻轻松松打电话

电话号码超级多的童鞋们&#xff0c;由于手机里存储的电话太多&#xff0c;每次要找某个人的电话很难找&#xff0c;有木有同感的&#xff1f;小编今天像大家推荐一款新潮的帮助你轻松找到想要的电话的模式&#xff0c;有没有很想知道呢&#xff1f;其实小编本来是想卖个关子的…

电脑的虚拟服务器位置,如何配置基于IP地址的虚拟主机

满意答案虚拟主机简介&#xff1a;1&#xff0e; 把一台运行在互联网上的服务器分成多个虚拟的服务器。2&#xff0e; 每一个虚拟主机都具有独立的域名和完整的Internet服务器(支持WWW&#xff0c;FTP&#xff0c;E-mail等)。3&#xff0e; 一台服务器上的不同虚拟主机是各自独…