深入剖析ThreadLocal实现原理以及内存泄漏问题

原创:https://blog.csdn.net/LHQJ1992/article/details/52451136?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.control&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.control

一、概述

在2017京东校园招聘笔试题中遇到了描述ThreadLocal的实现原理和内存泄漏的问题,之前看过ThreadLocal的实现原理,但是网上有很多文章将的很乱,其中有很多文章将ThreadLocal与线程同步机制混为一谈,特别注意的是ThreadLocal与线程同步无关,并不是为了解决多线程共享变量问题!
ThreadLocal官网解释:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy ofthevariable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state witha thread (e.g.,a user ID or Transaction ID)

->翻译过来的大概意思就是:ThreadLocal类用来提供线程内部的局部变量。这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型。
总结:ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性。

ThreadLocal的主要应用场景为按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。例如:同一个网站登录用户,每个用户服务器会为其开一个线程,每个线程中创建一个ThreadLocal,里面存用户基本信息等,在很多页面跳转时,会显示用户信息或者得到用户的一些信息等频繁操作,这样多线程之间并没有联系而且当前线程也可以及时获取想要的数据。

 

二、实现原理

ThreadLocal可以看做是一个容器,容器里面存放着属于当前线程的变量。ThreadLocal类提供了四个对外开放的接口方法,这也是用户操作ThreadLocal类的基本方法:
(1) void set(Object value)设置当前线程的线程局部变量的值。
(2) public Object get()该方法返回当前线程所对应的线程局部变量。
(3) public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
(4) protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次,ThreadLocal中的缺省实现直接返回一个null。

可以通过上述的几个方法实现ThreadLocal中变量的访问,数据设置,初始化以及删除局部变量,那ThreadLocal内部是如何为每一个线程维护变量副本的呢?

其实在ThreadLocal类中有一个静态内部类ThreadLocalMap(其类似于Map),用键值对的形式存储每一个线程的变量副本,ThreadLocalMap中元素的key为当前ThreadLocal对象,而value对应线程的变量副本,每个线程可能存在多个ThreadLocal。

源代码:

/** Returns the value in the current thread's copy of this thread-local variable. If the variable has no value for thecurrent thread, it is first initialized to the value returned by an invocation of the {@link #initialValue} method. @return the current thread's value of this thread-local */public T get() { Thread t = Thread.currentThread();//当前线程 ThreadLocalMap map = getMap(t);//获取当前线程对应的ThreadLocalMapif (map != null) { ThreadLocalMap.Entry e = map.getEntry(this);//获取对应ThreadLocal的变量值if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue();//若当前线程还未创建ThreadLocalMap,则返回调用此方法并在其中调用createMap方法进行创建并返回初始值。 } //设置变量的值publicvoidset(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } /** 为当前线程创建一个ThreadLocalMap的threadlocals,并将第一个值存入到当前map中 @param t the current thread @param firstValue value for the initial entry of the map */void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } //删除当前线程中ThreadLocalMap对应的ThreadLocalpublicvoidremove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }

上述是在ThreadLocal类中的几个主要的方法,他们的核心都是对其内部类ThreadLocalMap进行操作,下面看一下该类的源代码:

static class ThreadLocalMap {//map中的每个节点Entry,其键key是ThreadLocal并且还是弱引用,这也导致了后续会产生内存泄漏问题的原因。 static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } /** * 初始化容量为16,以为对其扩充也必须是2的指数 */private static final int INITIAL_CAPACITY = 16; /** * 真正用于存储线程的每个ThreadLocal的数组,将ThreadLocal和其对应的值包装为一个Entry。 */private Entry[] table; ///....其他的方法和操作都和map的类似 }

总之,为不同线程创建不同的ThreadLocalMap,用线程本身为区分点,每个线程之间其实没有任何的联系,说是说存放了变量的副本,其实可以理解为为每个线程单独new了一个对象。

三、内存泄漏问题(参考其他博文)

  在上面提到过,每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.
  所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。

 

ThreadLocal是一个线程内共享变量工具类。 将线程与该线程存放的对象做一个映射,各个线程之间的变量互不干扰。适用于各个线程依赖不同的变量值完成操作的场景,如:Spring声明式数据库事务、shiro的session

ThreadLocal内部结构

 

 

 

在这里插入图片描述

核心机制:
1.每个线程实例中有个threadlocals属性,实际上是个map
2.这个Map中存放的是ThreadLocal实例和Threadlocal实例在该线程中共享的值(value)
3.线程中的Map有ThreadLocal实例维护,由ThreadLocal实例向map中设置和获取值。
3.1 设置值
ThreadLocal实例通过Thread.currentThread获得当前线程实例,自身为key,待设置的值为value组成Entry,放入当前线程实例的map中。
3.2 获取值
ThreadLocal实例通过Thread.currentThread获得当前线程实例,在map中以自身为key,获得对应的value。

从上面的机制中确保threadlocal设置的值,仅在设置时的线程中共享,其它线程无法访问到该线程中设置的值。确保了,1、线程间变量隔离,2、线程内能访问。

内存溢出

当线程实例的生命周期短于ThreadLocal实例的生命周期,threadLocal内存的回收,取决于threadLocal实例的生命周期。而,当线程实例的生命周期长于ThreadLocal实例的生命周期(一般线程池场景)时,thread中ThreadLocalMap的每个Entry的key(对threadLocal的弱引用),在gc时会被回收;然而,Entry中value(强引用)是不会回收。当我们使用ThreadLocal的set方法,要配套进行remove,确保value能及时回收。

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable.  {@code 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)
  • 1

->翻译过来的大概意思就是:ThreadLocal类用来提供线程内部的局部变量。这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型。
总结:ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性。

ThreadLocal的主要应用场景为按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。例如:同一个网站登录用户,每个用户服务器会为其开一个线程,每个线程中创建一个ThreadLocal,里面存用户基本信息等,在很多页面跳转时,会显示用户信息或者得到用户的一些信息等频繁操作,这样多线程之间并没有联系而且当前线程也可以及时获取想要的数据。

二、实现原理

ThreadLocal可以看做是一个容器,容器里面存放着属于当前线程的变量。ThreadLocal类提供了四个对外开放的接口方法,这也是用户操作ThreadLocal类的基本方法:
(1) void set(Object value)设置当前线程的线程局部变量的值。
(2) public Object get()该方法返回当前线程所对应的线程局部变量。
(3) public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
(4) protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次,ThreadLocal中的缺省实现直接返回一个null。

可以通过上述的几个方法实现ThreadLocal中变量的访问,数据设置,初始化以及删除局部变量,那ThreadLocal内部是如何为每一个线程维护变量副本的呢?

其实在ThreadLocal类中有一个静态内部类ThreadLocalMap(其类似于Map),用键值对的形式存储每一个线程的变量副本,ThreadLocalMap中元素的key为当前ThreadLocal对象,而value对应线程的变量副本,每个线程可能存在多个ThreadLocal。

源代码:

/**
 Returns the value in the current thread's copy of this
 thread-local variable.  If the variable has no value for thecurrent thread, it is first initialized to the value returned by an invocation of the {@link #initialValue} method.
  @return the current 
thread's value of this thread-local */
public T get() { Thread t = Thread.currentThread();//当前线程 ThreadLocalMap map = getMap(t);//获取当前线程对应的ThreadLocalMap if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this);//获取对应ThreadLocal的变量值 if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue();//若当前线程还未创建ThreadLocalMap,则返回调用此方法并在其中调用createMap方法进行创建并返回初始值。 } //设置变量的值 public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } /** 为当前线程创建一个ThreadLocalMap的threadlocals,并将第一个值存入到当前map中 @param t the current thread @param firstValue value for the initial entry of the map */ void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } //删除当前线程中ThreadLocalMap对应的ThreadLocal public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

上述是在ThreadLocal类中的几个主要的方法,他们的核心都是对其内部类ThreadLocalMap进行操作,下面看一下该类的源代码:

static class ThreadLocalMap {
  //map中的每个节点Entry,其键key是ThreadLocal并且还是弱引用,这也导致了后续会产生内存泄漏问题的原因。
 static class Entry extends WeakReference<ThreadLocal<?>> {
           Object value;
           Entry(ThreadLocal<?> k, Object v) {
               super(k);
               value = v;
   }
    /**
     * 初始化容量为16,以为对其扩充也必须是2的指数 
     */
    private static final int INITIAL_CAPACITY = 16;
    /**
     * 真正用于存储线程的每个ThreadLocal的数组,将ThreadLocal和其对应的值包装为一个Entry。
     */
    private Entry[] table;


    ///....其他的方法和操作都和map的类似
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

总之,为不同线程创建不同的ThreadLocalMap,用线程本身为区分点,每个线程之间其实没有任何的联系,说是说存放了变量的副本,其实可以理解为为每个线程单独new了一个对象。

三、内存泄漏问题(参考其他博文)

  在上面提到过,每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.
  所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。

 
 
1,spring是一个开源的免费的框架(容器)。 2,spring是一个轻量级的,非入侵式的框架。 ​ 非入侵式:就是项目引入了这个框架之后,(不会改变你代码原来的任何情况)不会对之前的代码有什么影响
        想必很多朋友对ThreadLocal并不陌生,今天我们就来一起探讨下ThreadLocal的使用方法和实现原理。首先,本文先谈一下对ThreadLocal的理解,然后根据ThreadLocal类的源码分析了其实现原理和使用需要注意的地方,最后给出了两个应用场景。 一.对ThreadLocal的理解         ThreadLocal,很多地方叫做线程本地变量,也有些地方...

  • Monkey_D_Jie
    Monkey_D_Jie:谢谢博主的说明。 就是--最后内存泄露部分,讲的不是很清楚耶2年前回复
    3
    • wekajava
      wekajava回复:https://www.jianshu.com/p/b74de925cd7a 这里讲了应用场景和内存泄漏原因及预防4月前回复
  • z15732621582
    码哥赵尽朝:当然一般情况吧ThreadLocal声明成static final,这样可以保持ThreadLocal强引用。2年前回复
ThreadLocal是什么早在JDK 1.2的版本中就提供java.lang.ThreadLocalThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为T
8.2 使用ThreadLocal不当可能会导致内存泄露 基础篇已经讲解了ThreadLocal原理,本节着重来讲解下使用ThreadLocal会导致内存泄露的原因,并讲解使用ThreadLocal导致内存泄露的案例。 8.2.1 为何会出现内存泄露 基础篇我们讲到了ThreadLocal只是一个工具类,具体存放变量的是在线程的threadLocals变量里面,threadLo
Java多线程编程-(8)-多图深入分析ThreadLocal原理_徐刘..._CSDN博客
10-24
这一篇主要学习一下ThreadLocal原理,在下一篇会深入理解一下OOM内存溢出的原理和最佳实践。 ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。
ThreadLocal底层原理实现_奋进的小白粥-CSDN博客
10-2
ThreadLocal是java.lang包中的一个类,用来实现变量的线程封闭性,即只有当前线程可以操作该变量,通过把一个变量存在当前线程的一个Map容器中来实现。当然,这样解释很...
ThreadLocal原理是操作Thread内部的一个ThreadLocalMap,这个Map的Entry继承了WeakReference,设值完成后map中是(WeakReference,value)这样的数据结构。java中的弱引用在内存不足的时候会被回收掉,回收之后变成(null,value)的形式,key被收回掉了。 如果线程执行完之后销毁,value也会被回收,这样也没问题。但如果是...
这里使用的servlet容器是 tomcat如果在web项目中,使用 ThreadLocal 不当,会造成 OutOfMemoryError。说明原因前 1:先讲一下 ThreadLocal,Thead,ThreadLocalMap 三者之间的一个关系。(大家可以去看一下ThreadLocal实现源码,可而参考我的另一篇文章点击查看) ThreadLocalMap 是 ThreadLocal
ThreadLocal原理(简单易懂)_学习即修行-CSDN博客
11-1
下面我们从一个例子开始,循着源码,走入ThreadLocal原理世界吧!(作者目前使用的JDK版本是1.8.0_144版) 使用示例 publicclassThreadLocalTest{publicstaticvoidmain(St...
图解ThreadLocal原理_wo11201432的专栏-CSDN博客
10-31
在项目中不少地方会用到ThreadLocal对象,用来实现线程之间资源隔离。现在通过图的方式接解刨其原理。 publicstaticvoidmain(String[]arg)throwsInterruptedException,IOExc...
前言 ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。但是如果滥用 ThreadLocal,就可能会导致内存泄漏。下面,我们将围绕三个方面来分析 ThreadLocal 内存泄漏问题 ThreadLocal 实现原理 ThreadLocal为什么会内存泄漏 ThreadLocal 最佳...
前言 原理 为什么key使用弱引用 内存泄露 线程池 参阅:http://www.importnew.com/22039.html 前言 ThreadLocal提供了线程独有的局部变量,可以在整个线程存活的过程中随时取用,极大地方便了一些逻辑的实现。常见的ThreadLocal用法有: - 存储单个线程上下文信息。比如存储id等; - 使变量线程安全。变量既然成为...
彻底搞懂ThreadLocal原理_张振伟的博客-CSDN博客
11-1
下面从以下几个方面彻底搞懂ThreadLocal: 应用场景 实现原理 关于内存泄漏 使用场景 维护调用链路的requestID 在分布式系统中,一个面向用户的服务往往由内部系统多次调...
ThreadLocal原理_dakaniu的博客-CSDN博客_threadlocal原理
11-24
ThreadLocal的使用及其原理 ThreadLocal:ThreadLocal是线程局部变量,所谓的线程局部变量,就是仅仅只能被本线程访问,不能在线程之间进行共享访问的变量。 ThreadLocal的...
在这篇文章中,总结了一下面试过程中遇到的关于ThreadLocal的内容。总体上说,这样回答,面试算是过得去了。但是,这样的回答,明显仅仅是背会了答案,而没有去研究ThreadLocal的最根本的实现原理。 一共有两个问题。 1、每个线程的变量副本是存储在哪里的? 2、变量副本是怎么从共享的那个变量赋值出来的?源码中的threadlocal的初始值是什么时机设置的? ===========
西北工业大学计算机组成原理实验课唐都仪器实验帮助,同实验指导书。分为运算器,存储器,控制器,模型计算机,输入输出系统5个章节
ThreadLocal原理详解_karute的博客-CSDN博客_threadlocal原理
11-16
如果在应用上为每个线程分配了同一个对象实例,那ThreadLocal也无法保证线程安全。 4.3.1 ThreadLocal实现原理 ThreadLocal的内部实现,主要关注的就是set()和put()...
深入理解ThreadLocal原理内存泄漏问题_... 永远年..._CSDN博客
11-14
ThreadLocal作用和原理分析: ThreadLocal主要为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。要理解ThreadLocal需要理解下面三个问题: ...
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页
 
posted @ 2020-11-26 15:02  wzbbky  阅读(595)  评论(0编辑  收藏  举报