早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。ThreadLocal是指作用域为Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。此博客很多内容参考了(这篇博客https://www.cnblogs.com/fsmly/p/11020641.html).

介绍

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。 ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题,如下图所示:

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() {
@Override
public void run() {
//设置线程1中本地变量的值
localVar.set("localVar1");
//调用打印方法
print("thread1");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
}); Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程1中本地变量的值
localVar.set("localVar2");
//调用打印方法
print("thread2");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
}); t1.start();
t2.start();
}
}

ThreadLocal的实现原理

从上一节中我们可以看出,ThreadLocal主要有set和get方法,用于设置和获取线程中的变量,那么ThreadLocal是怎么实现这个功能的呢?和ThreadLocal实现相关的类主要有三个:ThreadLocal、Thread、ThreadLocalMap,三者之间的关系同样如下图所示:

  1. ThreadLocalMap:名字上看是Map,实际上是一个数组,不过它的功能和Map类似,可以按照key查找数据。
  2. Thread:线程大家应该都知道,那么在ThreadLocal中他起什么作用呢?一个Thread中会包含两个ThreadLocalMap,分别用于存储本线程和父线程的ThreadLocal数据。每一个ThreadLocal变量会在线程中对应一条ThreadLocalMap的key-value,其中key是ThreadLocal的唯一Hash值。
  3. ThreadLocal:每个ThreadLocal都会有一个唯一的Hash值,用于查找这个ThreadLocal在ThreadLocalMap中的值;ThreadLocal提供了方法用于获取当前线程的ThreadLocal数据。

数据存放的位置

ThreadLocal只是一层访问线程数据的壳,ThreadLocal get和set的数据不会在ThreadLocal的实例中存放,而是存放在线程Thread中的ThreadLocalMap,ThreadLocal只是提供了一个访问这些数据的途径。

ThreadLoca的set方法将value添加到调用线程的ThreadLocalMap中,当调用线程调用get方法时候能够从它的ThreadLocalMap中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的ThreadLocalMap中,所以不使用本地变量的时候需要调用remove方法将ThreadLocalMap中删除不用的本地变量。

set方法存放数据

ThreadLocal方法的set可以向当前线程的ThreadLocalMap中放入数据,存放数据的源码如下所示,Set过程分为以下步骤:

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

get方法获取数据

ThreadLocal方法的get可以获取当前线程ThreadLocalMap中存放的数据,获取存放数据的源码如下所示,get过程分为以下步骤:

  1. 获取当前线程
  2. 从当前线程中获取ThreadLocalMap变量。
  3. 如果ThreadLocalMap变量不为null,就可以在map中查找到本地变量的值。
  4. 如果ThreadLocalMap变量为null,那么就初始化当前线程的ThreadLocalMap。
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值,去查找对应的线程变量,找到对应的map
ThreadLocalMap map = getMap(t);
//如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
if (map != null)
map.set(this, value);
//如果map为null,说明首次添加,需要首先创建出对应的map
else
createMap(t, value);
return value;
}

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() {
@Override
public void run() {
System.out.println("子线程中的本地变量值:"+threadLocal.get());
}
});
thread.start();
//输出main线程中的本地变量值
System.out.println("mainx线程中的本地变量值:"+threadLocal.get());
}
}

InheritableThreadLocal类

在上面说到的ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能,下面是该类的源码,InheritableThreadLocal类继承了ThreadLocal类,并重写了childValue、getMap、createMap三个方法。我们接下来分别介绍一下三种方法的用处。

  1. createMap:当线程中不存在ThreadLocalMap变量,但是调用set或者get方法设置值的时候,需要初始化ThreadLocalMap变量时调用该方法。
  2. getMap:需要获取线程的ThreadLocalMap时调用该方法,这里返回的ThreadLocalMap始终为InheritableThreadLocalMap。
  3. childValue:在创建新线程的时候,如果父线程有ThreadLocalMap变量并且允许inherite ThreadLocalMap,那么程序会复制父线程的InheritableThreadLocal到子线程中,childValue表示在复制过程中如何根据父线程中得到数据生成线程中的数据。


public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* Creates an inheritable thread local variable.
*/
public InheritableThreadLocal() {} /**
* Computes the child's initial value for this inheritable thread-local
* variable as a function of the parent's value at the time the child
* thread is created. This method is called from within the parent
* thread before the child is started.
* <p>
* This method merely returns its input argument, and should be overridden
* if a different behavior is desired.
*/
protected T childValue(T parentValue) {
return parentValue;
} /**
* Get the map associated with a ThreadLocal.
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
} /**
* Create the map associated with a ThreadLocal.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}

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

ThreadLocal内存泄漏

通过前面的分析我们知道,ThreadLocal的线程数据是存放在ThreadLocalMap中的,所以如果ThreadLocal出现内存泄漏,那么肯定是ThreadLocalMap中存储的数据出现了泄露,我们需要看看ThreadLocalMap中的数据结构。ThreadLocalMap的数据结构如下所示,ThreadLocalMap中的数据存储在一个Entry数组中,Entry中有对ThreadLocal的WeakReference。

什么情况下会出现内存泄露呢?

  1. 当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。
  2. 如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。
  3. 考虑这个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方法避免内存泄漏。

Java中的四种引用类型

上文中我们说到了WeakReference,大家可能对这个词有点陌生,Java中有四种引用类型:

  1. 强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。
  2. 软引用:简言之,如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中。
  3. 弱引用(这里讨论ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null;
  4. 虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象)。

我是御狐神,欢迎大家关注我的微信公众号:wzm2zsd

本文最先发布至微信公众号,版权所有,禁止转载!

java基础之ThreadLocal的更多相关文章

  1. Java基础:ThreadLocal及其原理

    ThreadLocal的用处 ThreadLocal是一个多线程的辅助工具类,目的是方便开发者维护多线程中的共享变量.我们知道如果我们想要在一个线程中一直访问一个变量或者在线程上下文中保存一个变量,我 ...

  2. 【java基础】ThreadLocal的实现原理

    [一]:ThreadLocal对象的大体实现原理===>当前线程对象有一个ThreadLocal.ThreadLocalMap属性.===>声明的ThreadLocal对象最终存储在当前线 ...

  3. java基础解析系列(七)---ThreadLocal原理分析

    java基础解析系列(七)---ThreadLocal原理分析 目录 java基础解析系列(一)---String.StringBuffer.StringBuilder java基础解析系列(二)-- ...

  4. Java基础(1)——ThreadLocal

    1. Java基础(1)--ThreadLocal 1.1. ThreadLocal ThreadLocal是一个泛型类,当我们在一个类中声明一个字段:private ThreadLocal<F ...

  5. Java基础--ThreadLocal

    Java中的ThreadLocal 可以看做以线程标识为key的Map,在多线程开发中使用非常方便. 示例 class ThreadEnv { // 用匿名内部类覆盖ThreadLocal的initi ...

  6. JAVA基础系列:ThreadLocal

    1. 思路 什么是ThreadLocal?ThreadLocal类顾名思义可以理解为线程本地变量.也就是说如果定义了一个ThreadLocal,每个线程往这个ThreadLocal中读写是线程隔离,互 ...

  7. [Java面经]干货整理, Java面试题(覆盖Java基础,Java高级,JavaEE,数据库,设计模式等)

    如若转载请注明出处: http://www.cnblogs.com/wang-meng/p/5898837.html   谢谢.上一篇发了一个找工作的面经, 找工作不宜, 希望这一篇的内容能够帮助到大 ...

  8. Java基础加强之多线程篇(线程创建与终止、互斥、通信、本地变量)

    线程创建与终止 线程创建 Thread类与Runnable接口的关系 public interface Runnable { public abstract void run(); } public ...

  9. java基础-java核心知识库

    本人从事java开发6年左右,主要从事互联网相关的开发,目前还是奋战在一线的码农,痛并快乐着.受互联网产品热潮的影响,关注高性能低成本架构,互联网开发框架,以下是我认为作为一个资深java程序员应该掌 ...

随机推荐

  1. jquery 对HTML标签的克隆、删除

    <table width="100%" class="table_form"> <tr> <td>奖励深度(<a hr ...

  2. NCB | 定量蛋白质组学揭示细胞外泌体通用标志物Syntenin-1

    外泌体 (exosomes) 是由哺乳动物细胞通过"内吞-融合-外排"等机制,主动向胞外释放的纳米级 (直径40~60 nm) 双层囊泡小体,携带蛋白质.核酸.脂质等多种生物活性分 ...

  3. Http Request Smuggling - Note

    http请求走私漏洞 访问Burp靶场速度感人..都要哭了(如果没有账户的先创建账户) 基础补充 pipeline http1.1有了Pipeline,就不需要等待Server端的响应了.浏览器默认不 ...

  4. 做Android开发怎么才能不被淘汰?

    1.Jetpack架构组件从入门到精通 Android Jetpack - Navigation Android Jetpack - Data Binding Android Jetpack - Vi ...

  5. 面试必备:Android Activity启动流程源码分析

    最近大致分析了一把 Activity 启动的流程,趁着今天精神状态好,把之前记录的写成文章. 开门见山,我们直接点进去看 Activity 的 startActivity , 最终,我们都会走到 st ...

  6. J-Link cmd的使用

    ​01.WHY 为什么要使用到J-LinkCommander呢???大部分情况下,我们使用J-link都是在IDE中debug使用的,出现问题,直接debug复现然后解决.这是最常见的开发方式. 但是 ...

  7. vue 快速入门 系列 —— vue-cli 下

    其他章节请看: vue 快速入门 系列 Vue CLI 4.x 下 在 vue loader 一文中我们已经学会从零搭建一个简单的,用于单文件组件开发的脚手架:本篇,我们将全面学习 vue-cli 这 ...

  8. 【笔记】简谈L1正则项L2正则和弹性网络

    L1,L2,以及弹性网络 前情提要: 模型泛化与岭回归与LASSO 正则 ridge和lasso的后面添加的式子的格式上其实和MSE,MAE,以及欧拉距离和曼哈顿距离是非常像的 虽然应用场景不同,但是 ...

  9. 附件携马之CS免杀shellcode过国内主流杀软

    0x01 写在前面 其实去年已经写过类似的文章,但是久没用了,难免有些生疏.所谓温故而知新,因此再详细的记录一下,一方面可以给各位看官做个分享,另一方面等到用时也不至于出现临阵磨枪的尴尬场面. 0x0 ...

  10. Java调用阿里云短信接口发送手机验证码

    前五步可参考阿里云服务文档:https://help.aliyun.com/document_detail/59210.html?spm=a2c4g.11174283.4.1.2b152c42DoJ7 ...