刨ThreadLocal的坟
ThreadLocal是大家比较常用到的,在多线程下存储线程相关数据十分合适。可是很多时候我们并没有深入去了解它的原理。
首选提出几个问题,稍后再针对这些问题一一解答。
- 提到ThreadLocal,大家常说ThreadLocal是弱引用,那么ThreadLocal究竟是如何实现弱引用的呢?
 - ThreadLocal是如何做到可以当做线程局部变量的呢?
 - 大家创建ThreadLocal变量时,为什么都要用static修饰?
 - 大家争论不止的ThreadLocal内存泄漏是什么鬼?
 
进入正题,先简单了解下ThreadLocal 和 Thread,ThreadLocal的类结构:

可以看到,ThreadLocal有个内部类ThreadLocalMap,ThreadLocalMap又有个内部类Entry。
Thread类有这样一段源码:
class Thread implements Runnable {
    ...省略若干代码
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
通过Thread源码我们了解到,Thread持有的对象是ThreadLocal的ThreadLocalMap,这一点特别重要,线程相关数据都是通过ThreadLocalMap存储的,而不是ThreadLocal。
此时我们得到的结论如下图所示:
Thread的threadLocals属性直接关联的ThreadLocal.ThreadLocalMap,和ThreadLocal没有丝毫关系

那么ThreadLocal是做什么的呢?其实ThreadLocal可以看做线程操作ThreadLocalMap的工具类,ThreadLocal暴漏了两个公共方法get()和set(T)用来获取和设置ThreadLocalMap。
了解一下set方法源码:
     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时,使用了当前Thread对象 t 作为参数。
 
getMap(t)方法的实现很简单:
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
它返回的是Thread的 threadLocals 属性,代码上验证了:“线程局部变量”是存储在Thread对象的threadLocals属性中,和 ThreadLocal 本身没什么关系。ThreadLocal 可以当做访问的工具类。
这里我们第2个问题:ThreadLocal是如何做到可以当做线程局部变量的已经有答案啦,所有的操作其实都是对Thread 下 threadLocals 的操作,所以跨线程操作也不会产生问题的,因为getMap()永远返回当前线程的threadLocals属性。
- ThreadLocalMap是一个类似Map键值对的结构,此处传入的key是固定值this,这个this不是线程对象哟,是当前的ThreadLocal对象,value即我们传入的参数。
 
小伙伴们是不是很奇怪为什么要把this当做key呢?这就扯到我们文章开头的第一个问题了:弱引用!
跟进map.set(this, value);源码一看究竟:
         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();
         }
查看23行Entry的构造方法:
         static class Entry extends WeakReference<ThreadLocal<?>> {
             /** The value associated with this ThreadLocal. */
             Object value;
             Entry(ThreadLocal<?> k, Object v) {
                 super(k);
                 value = v;
             }
         }
Entry只有一个构造方法,该构造方法接受两个参数k和v,k就是当前ThreadLocal对象,v是我要存储的线程相关数据。通过上述代码标红部分我们可以了解到对 k 使用了弱引用,但是value不是,value是强引用。至此第一个问题已经真相了,大家所说的ThreadLocal弱引用其实是ThreadLocalMap和ThreadLocal是弱引用关系。
为什么要这么设计呢?
首选我们整理下当前引用关系如下图:
    
value一般是线程相关的数据,线程回收后value -> null,强引用就不存在了。但是ThreadLocal对象的生命周期不一定和线程相关,可能线程消亡后ThreadLocal对象仍然被其它线程引用,如果使用强引用的话,ThreadLocalMap对象就无法释放内存,发生内存泄漏的情况。使用弱引用就安全的多了,发生gc时弱引用指向的对象会被内存回收。
问题1和2已经在上文中提到,继续看问题3,创建ThreadLocal对象时为什么要用static修饰呢?
个人感觉是基于两点的考虑:
- 第一是避免重复创建ThreadLocal对象,使用同一个ThreadLocal对象和多个ThreadLocal对象对代码本身没什么影响,实在没必要重复创建多个对象。
 - 延长ThreadLocal的生命周期,方便使用。
 
网上很多地方把static和内存泄漏联系起来,原谅我没看出来这两者有什么关系。
最后来到第四个问题,也大家都关心的内存泄漏啦,。
通过上面的引用关系图我们了解到存在两个引用关系,分别是key的弱引用和value的强引用。弱引用首选不可能导致内存泄漏,因为gc发生时弱引用的对象就有可能被回收了。所以。。。内存泄漏发生在强引用这个关系上。
因为现在线程切换的开销比较大,大家现在普遍使用线程池的技术去避免线程的频繁创建。在线程池中,线程不会消亡,会被重复使用,so。。。。上边的强引用得不到释放了,内存泄漏就这样发生了。其实我在JDK8上看到的是java已经为此做了一些工作了,比如执行下次set操作时遍历key是null的Entry对象并释放value的引用。虽然java本身做了一些工作,仍然强烈建议使用完ThreadLocal执行remove方法主动消除引用关系。
文章结束了,如有纰漏,欢迎指出。
刨ThreadLocal的坟的更多相关文章
- ThreadLocal源码阅读笔记
		
功能描述 ThreadLocal解决了访问共享变量的阻塞问题,并且不需要像CAS操作一样牺牲CPU资源,它为每一个线程维护了一个变量副本,每个线程在访问ThrealLocal里面的变量时实际上访问的是 ...
 - ThreadLocal原理简单刨析
		
ThreadLocal原理简单刨析 ThreadLocal实现了各个线程的数据隔离,要知道数据是如何隔离的,就要从源代码分析. ThreadLocal原理 需要提前说明的是:ThreadLocal只是 ...
 - ThreadLocal简单理解
		
在java开源项目的代码中看到一个类里ThreadLocal的属性: private static ThreadLocal<Boolean> clientMode = new Thread ...
 - Android线程管理之ThreadLocal理解及应用场景
		
前言: 最近在学习总结Android的动画效果,当学到Android属性动画的时候大致看了下源代码,里面的AnimationHandler存取使用了ThreadLocal,激起了我很大的好奇心以及兴趣 ...
 - Threadlocal使用Case
		
Threadlocal能够为每个线程分配一份单独的副本,使的线程与线程之间能够独立的访问各自副本.Threadlocal 内部维护一个Map,key为线程的名字,value为对应操作的副本. /** ...
 - 多线程映射工具——ThreadLocal
		
ThreadLocal相当于一个Map<Thread, T>,各线程使用自己的线程对象Thread.currentThread()作为键存取数据,但ThreadLocal实际上是一个包装了 ...
 - ThreadLocal 工作原理、部分源码分析
		
1.大概去哪里看 ThreadLocal 其根本实现方法,是在Thread里面,有一个ThreadLocal.ThreadLocalMap属性 ThreadLocal.ThreadLocalMap t ...
 - ThreadLocal<T>的是否有设计问题
		
一.吐槽 ThreadLocal<T>明显是.NET从JAVA中来的一个概念,但是这种设计是否出现了问题. 很明显,在JAVA中threadLocal直接是Thread的成员,当然随着th ...
 - 理解ThreadLocal —— 一个map的key
		
作用: 当工作于多线程中的对象使用ThreadLocal维护变量时,threadLocal为每个使用该变量的线程分配一个独立的变量副本. 接口方法: protected T initialValue( ...
 
随机推荐
- java读取Excel —— XSSFWorkbook 找不到该类
 - Solve Hibernate Lazy-Init issue with hibernate.enable_lazy_load_no_trans
			
I have been suffering from infamous hibernate exception org.hibernate.LazyInitializationException: c ...
 - Java中关于泛型集合类存储的总结
			
集合类存储在任何编程语言中都是很重要的内容,只因有这样的存储数据结构才让我们可以在内存中轻易的操作数据,那么在Java中这些存储类集合结构都有哪些?内部实现是怎么样?有什么用途呢?下面分享一些我的总结 ...
 - Java连载33-对象的创建和使用、内存分析
			
一.创建一个学生类 每个学生都有学号信息,但是每一个学生的学号都是不同的,所以要访问这个学号必须先创建对象,通过对象去访问学号信息,学号信息不能直接通过“类”去访问,所以这种成员变量又被称为“实例变量 ...
 - .netCore+Vue 搭建的简捷开发框架 (3)-- Services层实现
			
继续交作业: 上一篇作业中我们实现了 Repository仓储层的应用.并为我们的框架引入了EFCore 详见: .netCore+Vue 搭建的简捷开发框架 (2)--仓储层实现和EFCore 的使 ...
 - python小基础
			
1.计算机基础知识 中央处理器 CPU 人的大脑 内存 缓存数据 临时记忆 硬盘 储存数据 永久记忆 什么是操作系统 ? 控制计算机工作的流程 什么是应用程序? 安装在操作系统之上的软件 2.pyth ...
 - JDK 13 都已经发布了,Java 8 依然是最爱
			
在 JDK 版本的世界里,从来都是 Oracle 发他的新版本,我们继续用我们的老版本.三年之前用 JDK 7,后来终于升级到了 JDK 8.自从升级了没多久,JDK 就开始了半年发一个新版本的节奏, ...
 - 让tomcat使用指定JDK
			
一,前言 我们都知道,tomcat启动前需要配置JDK环境变量,如果没有配置JDK的环境变量,那么tomcat启动的时候就会报错,也就是无法启动. 但是在我们的工作或者学习过程中,有的时候会出现tom ...
 - java8 Optional使用总结
			
[前言] java8新特性 java8 函数接口 java8 lambda表达式 Java 8 时间日期使用 java8 推出的Optional的目的就是为了杜绝空指针异常,帮助开发者开发出更优雅的代 ...
 - linux 设置查看文本行数
			
在一般模式下,即摁下esc按键下的模式: 设置行数为:set nu(此处的冒号需要带上) 取消行号为:set nonu(此处的冒号需要带上)