Java中的ThreadLocal详解

一、ThreadLocal简介

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。

ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题,如下图所示

img

二、ThreadLocal简单使用

下面的例子中,开启两个线程,在每个线程内部设置了本地变量的值,然后调用print方法打印当前本地变量的值。如果在打印之后调用本地变量的remove方法会删除本地内存中的变量,代码如下所示

package test;public class ThreadLocalTest {static ThreadLocal<String> localVar = new ThreadLocal<>();static void print(String str) {//打印当前线程中本地内存中本地变量的值System.out.println(str + " :" + localVar.get());//清除本地内存中的本地变量localVar.remove();}public static void main(String[] args) {Thread t1  = new Thread(new Runnable() {@Overridepublic void run() {//设置线程1中本地变量的值localVar.set("localVar1");//调用打印方法print("thread1");//打印本地变量System.out.println("after remove : " + localVar.get());}});Thread t2  = new Thread(new Runnable() {@Overridepublic void run() {//设置线程1中本地变量的值localVar.set("localVar2");//调用打印方法print("thread2");//打印本地变量System.out.println("after remove : " + localVar.get());}});t1.start();t2.start();}
}

下面是运行后的结果:

img

三、ThreadLocal的实现原理

下面是ThreadLocal的类图结构,从图中可知:Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量,我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为nullimgimg,只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们(后面我们会查看这两个方法的源码)。除此之外,和我所想的不同的是,每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面(前面也说过,该变量是Thread类的变量)。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量。下面我们通过查看ThreadLocal的set、get以及remove方法来查看ThreadLocal具体实怎样工作的

img

1、set方法源码

public void set(T value) {//(1)获取当前线程(调用者线程)Thread t = Thread.currentThread();//(2)以当前线程作为key值,去查找对应的线程变量,找到对应的mapThreadLocalMap map = getMap(t);//(3)如果map不为null,就直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,值为添加的本地变量值if (map != null)map.set(this, value);//(4)如果map为null,说明首次添加,需要首先创建出对应的mapelsecreateMap(t, value);
}

在上面的代码中,(2)处调用getMap方法获得当前线程对应的threadLocals(参照上面的图示和文字说明),该方法代码如下

ThreadLocalMap getMap(Thread t) {return t.threadLocals; //获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上
}

如果调用getMap方法返回值不为null,就直接将value值设置到threadLocals中(key为当前线程引用,值为本地变量);如果getMap方法返回null说明是第一次调用set方法(前面说到过,threadLocals默认值为null,只有调用set方法的时候才会创建map),这个时候就需要调用createMap方法创建threadLocals,该方法如下所示

void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}

createMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了threadLocals中。

2、get方法源码

在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null。

public T get() {//(1)获取当前线程Thread t = Thread.currentThread();//(2)获取当前线程的threadLocals变量ThreadLocalMap map = getMap(t);//(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量return setInitialValue();
}private T setInitialValue() {//protected T initialValue() {return null;}T value = initialValue();//获取当前线程Thread t = Thread.currentThread();//以当前线程作为key值,去查找对应的线程变量,找到对应的mapThreadLocalMap map = getMap(t);//如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值if (map != null)map.set(this, value);//如果map为null,说明首次添加,需要首先创建出对应的mapelsecreateMap(t, value);return value;
}

3、remove方法的实现

remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量

public void remove() {//获取当前线程绑定的threadLocalsThreadLocalMap m = getMap(Thread.currentThread());//如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量if (m != null)m.remove(this);}

4、如下图所示:每个线程内部有一个名为threadLocals的成员变量,该变量的类型为ThreadLocal.ThreadLocalMap类型(类似于一个HashMap),其中的key为当前定义的ThreadLocal变量的this引用,value为我们使用set方法设置的值。每个线程的本地变量存放在自己的本地内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其remove掉。

img

四、ThreadLocal不支持继承性

同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的)

package test;public class ThreadLocalTest2 {//(1)创建ThreadLocal变量public static ThreadLocal<String> threadLocal = new ThreadLocal<>();public static void main(String[] args) {//在main线程中添加main线程的本地变量threadLocal.set("mainVal");//新创建一个子线程Thread thread = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("子线程中的本地变量值:"+threadLocal.get());}});thread.start();//输出main线程中的本地变量值System.out.println("mainx线程中的本地变量值:"+threadLocal.get());}
}

五、InheritableThreadLocal类

在上面说到的ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能,下面是该类的源码

public class InheritableThreadLocal<T> extends ThreadLocal<T> {protected T childValue(T parentValue) {return parentValue;}ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}void createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}
}

从上面代码可以看出,InheritableThreadLocal类继承了ThreadLocal类,并重写了childValue、getMap、createMap三个方法。其中createMap方法在被调用(当前线程调用set方法时得到的map为null的时候需要调用该方法)的时候,创建的是inheritableThreadLocal而不是threadLocals。同理,getMap方法在当前调用者线程调用get方法的时候返回的也不是threadLocals而是inheritableThreadLocal。

下面我们看看重写的childValue方法在什么时候执行,怎样让子线程访问父线程的本地变量值。我们首先从Thread类开始说起

private void init(ThreadGroup g, Runnable target, String name,long stackSize) {init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {//判断名字的合法性if (name == null) {throw new NullPointerException("name cannot be null");}this.name = name;//(1)获取当前线程(父线程)Thread parent = currentThread();//安全校验SecurityManager security = System.getSecurityManager();if (g == null) { //g:当前线程组if (security != null) {g = security.getThreadGroup();}if (g == null) {g = parent.getThreadGroup();}}g.checkAccess();if (security != null) {if (isCCLOverridden(getClass())) {security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);}}g.addUnstarted();this.group = g; //设置为当前线程组this.daemon = parent.isDaemon();//守护线程与否(同父线程)this.priority = parent.getPriority();//优先级同父线程if (security == null || isCCLOverridden(parent.getClass()))this.contextClassLoader = parent.getContextClassLoader();elsethis.contextClassLoader = parent.contextClassLoader;this.inheritedAccessControlContext =acc != null ? acc : AccessController.getContext();this.target = target;setPriority(priority);//(2)如果父线程的inheritableThreadLocal不为nullif (inheritThreadLocals && parent.inheritableThreadLocals != null)//(3)设置子线程中的inheritableThreadLocals为父线程的inheritableThreadLocalsthis.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);this.stackSize = stackSize;tid = nextThreadID();
}

在init方法中,首先(1)处获取了当前线程(父线程),然后(2)处判断当前父线程的inheritableThreadLocals是否为null,然后调用createInheritedMap将父线程的inheritableThreadLocals作为构造函数参数创建了一个新的ThreadLocalMap变量,然后赋值给子线程。下面是createInheritedMap方法和ThreadLocalMap的构造方法

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {return new ThreadLocalMap(parentMap);
}private ThreadLocalMap(ThreadLocalMap parentMap) {Entry[] parentTable = parentMap.table;int len = parentTable.length;setThreshold(len);table = new Entry[len];for (int j = 0; j < len; j++) {Entry e = parentTable[j];if (e != null) {@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();if (key != null) {//调用重写的方法Object value = key.childValue(e.value);Entry c = new Entry(key, value);int h = key.threadLocalHashCode & (len - 1);while (table[h] != null)h = nextIndex(h, len);table[h] = c;size++;}}}
}

在构造函数中将父线程的inheritableThreadLocals成员变量的值赋值到新的ThreadLocalMap对象中。返回之后赋值给子线程的inheritableThreadLocals。总之,InheritableThreadLocals类通过重写getMap和createMap两个方法将本地变量保存到了具体线程的inheritableThreadLocals变量中,当线程通过InheritableThreadLocals实例的set或者get方法设置变量的时候,就会创建当前线程的inheritableThreadLocals变量。而父线程创建子线程的时候,ThreadLocalMap中的构造函数会将父线程的inheritableThreadLocals中的变量复制一份到子线程的inheritableThreadLocals变量中。

六、从ThreadLocalMap看ThreadLocal使用不当的内存泄漏问题

1、基础概念

首先我们先看看ThreadLocalMap的类图,在前面的介绍中,我们知道ThreadLocal只是一个工具类,他为用户提供get、set、remove接口操作实际存放本地变量的threadLocals(调用线程的成员变量),也知道threadLocals是一个ThreadLocalMap类型的变量,下面我们来看看ThreadLocalMap这个类。在此之前,我们回忆一下Java中的四种引用类型,相关GC只是参考前面系列的文章(JVM相关)

①强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。

②软引用:简言之,如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中

③弱引用(这里讨论ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null

④虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象)

img

2、分析ThreadLocalMap内部实现

上面我们知道ThreadLocalMap内部实际上是一个Entry数组img,我们先看看Entry的这个内部类

/*** 是继承自WeakReference的一个类,该类中实际存放的key是* 指向ThreadLocal的弱引用和与之对应的value值(该value值* 就是通过ThreadLocal的set方法传递过来的值)* 由于是弱引用,当get方法返回null的时候意味着坑能引用*/
static class Entry extends WeakReference<ThreadLocal<?>> {/** value就是和ThreadLocal绑定的 */Object value;//k:ThreadLocal的引用,被传递给WeakReference的构造方法Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}
//WeakReference构造方法(public class WeakReference<T> extends Reference<T> )
public WeakReference(T referent) {super(referent); //referent:ThreadLocal的引用
}//Reference构造方法
Reference(T referent) {this(referent, null);//referent:ThreadLocal的引用
}Reference(T referent, ReferenceQueue<? super T> queue) {this.referent = referent;this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

在上面的代码中,我们可以看出,当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中的key为ThreadLocal的弱引用。当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。

考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。

总结:
THreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。

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

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

相关文章

计算机专用英语词汇pdf,计算机专用英语词汇1500词.pdf

计算机专用英语词汇1500 词 36. memory n. 记忆存储&#xff0c;存储器37. which pron. 哪个&#xff0c;a. 那一个Sample TextSample Text 38. all a. 全&#xff0c;全部&#xff1b;ad. 完全电脑日常用语和术语大集合~~ &#xff01;&#xff01;&#xff01; 39. on ad. 接…

谈谈对ThreadLocal的理解?(基于jdk1.8)

在java的多线程模块中&#xff0c;ThreadLocal是经常被提问到的一个知识点&#xff0c;提问的方式有很多种&#xff0c;可能是循序渐进也可能是就像我的题目那样&#xff0c;因此只有理解透彻了&#xff0c;不管怎么问&#xff0c;都能游刃有余。 这篇文章主要从以下几个角度来…

江苏学考计算机,学长建议 江苏考生 位次在4000-10000 想报考计算机的同学

本人对高考志愿填报有点执念&#xff0c;毕竟是第一次真正重大的抉择&#xff0c;以后每年都会看看高校录取情况&#xff0c;加上今年刚找工作&#xff0c;对互联网企业的选人标准比较了解&#xff0c;所以斗胆来提提建议。核心观点&#xff0c;多看看985的分校区&#xff0c;比…

mybatis if test判断 list不为空

<if test"list!null and list.size()!0"></if>注意 如果使用list.isNotEmpty()会报错&#xff0c;提示不是一个方法

数字时钟html5 js,html5 canvas js(数字时钟)实例代码

canvas dClock您的浏览器太古董了&#xff0c;升级吧&#xff01;var clock document.getElementById("clock");var cxt clock.getContext("2d");//显示数字时钟function showTime(m, n) {cxt.clearRect(0, 0, 500, 500);var now new Date;var hour no…

简单理解BigDecimal.valueof(Double t)与BigDecimal.valueof(String t)的区别——BigDecimal

上面的代码主要的区别在于 初始化BigDecimal时形参是double、String和float的区别 从上面可以看到&#xff0c;当double 和 float 时&#xff0c;实际保存的值并不是是准确的0.99&#xff0c;这是为什么呢 大致的原因是&#xff1a; BigDecimal(double val)将会把double型二…

在线计算机标准版,NCRE全国计算机等级一标准版级考试复习资料.doc

全国计算机等级一级考试复习资料内容&#xff1a;计算机基础知识、文字处理软件WORD复习题类型&#xff1a;填空题、选择题、综合题复习题数量&#xff1a;复习时间&#xff1a;12月5日—12月9日考试时间&#xff1a;12月9日(周三下午第三节课)参考对象&#xff1a;电子高专的学…

mysql 统计当天,本周,本月,上一月的数据

今天 select * from ht_invoice_information where year(create_date)year(date_sub(now(),interval 1 year)); select * from 表名 where to_days(时间字段名) to_days(now());昨天 select * from 表名 where to_days( now( ) ) - to_days( 时间字段名) < 1近期7天 sel…

下图中的蓝月亮为科学家用计算机,2018年高一地理前半期课时练习试卷带答案和解析...

目前人类可以观察到的最高级别天体系统是A. 总星系 B. 银河系 C. 太阳系 D. 地月系【答案】A【解析】本题考查天体系统的层次。距离相近的天体因相互吸引和相互绕转&#xff0c;构成不同级别的天体系统&#xff0c;天体系统的层次为&#xff1a;最高一级为总星系(即目前所知的宇…

邮政计算机网络,邮政计算机网络论文(共2018字).doc

邮政计算机网络论文(共2018字)邮政计算机网络论文(共2018字)一、邮政计算机网络基本现状分析邮政网络系统资源不足&#xff0c;数据传输技术滞后随着邮政储蓄代收付业务和电子邮政业务的快速发展&#xff0c;市场需要变化加快&#xff0c;现有网络系统资源不足&#xff0c;设备…

HashMap的put方法返回值问题

API文档中的描述&#xff1a; 先看一个例子 Map<Character, Integer> map new HashMap<Character, Integer>(); System.out.println(map.put(a, 0)); // null System.out.println(map.put(a, 1)); // 0 System.out.println(map.put(a, 2)); // 1 System.out.pri…

资金时间价值的计算机应用视频讲解,第八章资金时间价值与方案经济比选20161018讲解.ppt...

第八章资金时间价值与方案经济比选20161018讲解(二)净年值(NAV) 1、含义 净年值也称净年金(记作NAV)&#xff0c;它是把项目寿命期内的净现金流量按设定的折现率折算成与其等值的各年年末的等额净现金流量值。 2&#xff0e;计算 先求该项目的净现值&#xff0c;然后乘以资金回…

[Microsoft][ODBC SQL Server Driver][SQL Server]对象名 ‘***‘无效问题的解决方案器

[Microsoft][ODBC SQL Server Driver][SQL Server]对象名 ***无效问题的解决方案 在用Java进行SQL server数据编程时出现数据库连接成功后对表进行操作时报错“表名无效”。在网上搜了相关问题后均未解决&#xff0c;最后通过在表名前加数据库名的方式得以解决&#xff0c;记录…

Java中将Map转换为JSON

一个注意的地方&#xff1a;要选对jar包 Map map new HashMap();map.put("success", "true");map.put("photoList", photoList);map.put("currentUser", "zhang");//net.sf.json.JSONObject 将Map转换为JSON方法JSONObject…

掌上通计算机一级考试在线安装,计算机一级掌上通

计算机一级掌上通app是一款计算机等级考试学习的软件&#xff0c;让你在线学习计算机的操作知识&#xff0c;便于通过等级考试&#xff0c;快速准确&#xff1b;软件提供海量选择题的题库&#xff0c;随时随地做题&#xff0c;简单又方便&#xff0c;还有计算机基本操作讲解&am…

java获取时间,本周,本月,本季度的起始

package com.yong.util; import java.util.Calendar; import java.util.Date;public class TestDate {public static void main(String[] args) {System.out.println("当前时间&#xff1a;" new Date().toLocaleString());System.out.println("当天0点时间&…

东莞理工学院计算机ccf,中国计算机学会东莞分部成立

为更全面和更好地服务东莞计算机领域专业人士的学术和职业发展&#xff0c;在中国计算机学会(CCF)总部和广州、深圳分部的指导和协助下&#xff0c;由东莞理工学院和中美融易孵化器牵头&#xff0c;联合东莞市各大高校、学会、企业&#xff0c;共同发起成立中国计算机学会东莞分…

mybatis获取表名——mybatis动态调用表名和字段名#{},${}

一直在使用Mybatis这个ORM框架&#xff0c;都是使用mybatis里的一些常用功能。今天在项目开发中有个业务是需要限制各个用户对某些表里的字段查询以及某些字段是否显示&#xff0c;如某张表的某些字段不让用户查询到。这种情况下&#xff0c;就需要构建sql来动态传入表名、字段…

浙大计算机学院辅导员,浙大博士应聘辅导员被指丢脸 月薪仅1000元

浙江在线05月02日讯 浙江大学动物营养与饲料科学专业博士生冯一秦这两天正在准备博士论文答辩&#xff0c;他已经找好了工作单位——在工商大学当大学生辅导员&#xff0c;一度成为浙大BBS头条新闻——高职低聘&#xff0c;很丢脸吗&#xff1f;网友跟帖&#xff1a;这样的博士…

html5 中 video 标签,H5页面中 video 标签的坑

兼容性差因为是原生组件层级最高iOS系统 和 安卓系统展示方式不一样整理了一些有效的方法关于 标签的 css 样式//全屏按钮video::-webkit-media-controls-fullscreen-button {display: none;}//播放按钮video::-webkit-media-controls-play-button {display: none;}//进度条vid…