一、前言

  这篇博客来分析一下ThreadLocal的实现原理以及常见问题,由于现在时间比较晚了,我就不废话了,直接进入正题。

二、正文

2.1 ThreadLocal是什么

  在讲实现原理之前,我先来简单的说一说ThreadLocal是什么。ThreadLocal被称作线程局部变量,当我们定义了一个ThreadLocal变量,所有的线程共同使用这个变量,但是对于每一个线程来说,实际操作的值是互相独立的。简单来说就是,ThreadLocal能让线程拥有自己内部独享的变量。举一个简单的例子:

// 定义一个线程共享的ThreadLocal变量
static ThreadLocal<Integer> tl = new ThreadLocal<>(); public static void main(String[] args) { // 创建第一个线程
Thread t1 = new Thread(() -> {
// 设置ThreadLocal变量的初始值,为1
tl.set(1);
// 循环打印ThreadLocal变量的值
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "----" + tl.get());
// 每次打印完让值 + 1
tl.set(tl.get() + 1);
}
}, "thread1"); // 创建第二个线程
Thread t2 = new Thread(() -> {
// 设置ThreadLocal变量的初始值,为100,与上一个线程区别开
tl.set(100);
// 循环打印ThreadLocal变量的值
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "----" + tl.get());
// 每次打印完让值 - 1
tl.set(tl.get() - 1);
}
}, "thread2");
// 开启两个线程
t1.start();
t2.start(); tl.remove();
}

  上面的代码,运行结果如下(注:每次运行的结果可能不同):

thread1----1
thread2----100
thread1----2
thread2----99
thread1----3
thread2----98
thread1----4
thread2----97
thread1----5
thread2----96
thread1----6
thread2----95
thread1----7
thread2----94
thread1----8
thread2----93
thread1----9
thread2----92
thread1----10
thread2----91

  通过上面的输出结果我们可以发现,线程1线程2虽然使用的是同一个ThreadLocal变量存储值,但是输出结果中,两个线程的值却互不影响,线程11输出到10,而线程2100输出到91。这就是ThreadLocal的功能,即让每一个线程拥有自己独立的变量,多个线程之间互不影响。

2.2 ThreadLocal的实现原理

  下面我就就来说一说ThreadLocal是如何做到线程之间相互独立的,也就是它的实现原理。这里我直接放出结论,后面再根据源码分析:每一个线程都有一个对应的Thread对象,而Thread类有一个成员变量,它是一个Map集合,这个Map集合的key就是ThreadLocal的引用,而value就是当前线程在key所对应的ThreadLocal中存储的值。当某个线程需要获取存储在ThreadLocal变量中的值时,ThreadLocal底层会获取当前线程的Thread对象中的Map集合,然后以ThreadLocal作为key,从Map集合中查找value值。这就是ThreadLocal实现线程独立的原理。也就是说,ThreadLocal能够做到线程独立,是因为值并不存在ThreadLocal中,而是存储在线程对象中。下面我们根据ThreadLocal中两个最重要的方法来确认这一点。

2.3 ThreadLocal中的get方法

  get方法的作用非常简单,就是线程向ThreadLocal中取值,下面我们来看看它的源码:

public T get() {
// 获取当前线程的Thread对象
Thread t = Thread.currentThread();
// getMap方法传入Thread对象,此方法将返回Thread对象中存储的一个Map集合
// 这个Map集合的类型为ThreadLocalMap,这是ThreadLoacl的一个内部类
// 当前线程存放在ThreadLocal中的值,实际上存放在这个Map集合中
ThreadLocalMap map = getMap(t);
// 如果当前Map集合已经初始化,则直接从Map集合中查找
if (map != null) {
// ThreadLocalMap的key其实就是ThreadLoacl对象的引用
// 所以要找到线程在当前ThreadLoacl中存放的值,就需要以当前ThreadLoacl作为key
// getEntry方法就是通过key获取map中的一个key-value,而这里使用的key就是this
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果返回值不为空,表示查找成功
if (e != null) {
@SuppressWarnings("unchecked")
// 于是获取对应的value并返回
T result = (T)e.value;
return result;
}
}
// 若当前线程的ThreadLocalMap还未初始化,或者查找失败,则调用以下方法
return setInitialValue();
} private T setInitialValue() {
// 此方法默认返回null,但是可以由子类进行重新,根据需求返回需要的值
T value = initialValue();
// 获取当前线程的Thread对象
Thread t = Thread.currentThread();
// 获取对应的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果Map已经初始化了,就直接往map中加入一个key-value
// key就是当前ThreadLocal对象的引用,而value就是上面获取到的value,默认为null
if (map != null)
map.set(this, value);
// 若还没有初始化,则调用createMap创建ThreadLocalMap对象
else
createMap(t, value);
// 返回initialValue方法返回的值,默认为null
return value;
} void createMap(Thread t, T firstValue) {
// 创建ThreadLocalMap对象,构造方法传入的是第一对放入其中的key-value
// 这个key也就是当前线程第一次调用get方法的ThreadLocal对象,也就是当前ThreadLocal对象
// 而firstValue则是initialValue方法的返回值,默认为null
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

  上面的代码非常直观的验证了我之前说过的ThreadLocal的实现原理。通过上面的代码,我们可以非常直观的看到,线程向ThreadLocal中存放的值,最后都放入了线程自己的ThreadLocalMap中,而这个mapkey就是当前ThreadLocal的引用。而ThreadLocal中,获取线程的ThreadLocalMap的方法getMap的代码如下:

ThreadLocalMap getMap(Thread t) {
// 直接返回Thread对象的threadLocals成员变量
return t.threadLocals;
}

  我们再看看Thread类中的threadLocals变量:

/** 可以看到,ThreadLocalMap是ThreadLocal的内部类 */
ThreadLocal.ThreadLocalMap threadLocals = null;

2.4 ThreadLocal中的set方法

  下面再来看一看ThreadLocalset方法的实现,set方法用来使线程向ThreadLocal中存放值(实际上是存放在线程自己的Map中):

public void set(T value) {
// 获取当前线程的Thread对象
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 若map已经初始化,则之际将value放入Map中,对应的key就是当前ThreadLocal的引用
if (map != null)
map.set(this, value);
// 若没有初始化,则调用createMap方法,为当前线程t创建ThreadLocalMap,
// 然后将key-value放入(此方法已经在上面讲解get方法是看过)
else
createMap(t, value);
}

  这就是set方法的实现,比较简单。看完上面两个关键方法的实现,相信大家对ThreadLocal的实现已经有了一个比较清晰的认识,下面我们来更加深入的分析ThreadLocal,看看ThreadLocalMap的一些实现细节。

2.5 ThreadLocalMap的中的弱引用

  ThreadLocalMap的实现其实就是一个比较普通的Map集合,它的实现和HashMap类似,所以具体的实现细节我们就不一一讲解了,这里我们只关注它最特别的一个地方,即它内部的节点Entry。我们先来看看Entry的代码:

// Entry是ThreadLocalMap的内部类,表示Map的节点
// 这里继承了WeakReference,这是java实现的弱引用类,泛型为ThreadLocal
// 表示在这个Map中,作为key的ThreadLocal是弱引用
// (这里value是强引用,因为没用WeakReference)
static class Entry extends WeakReference<ThreadLocal<?>> {
/** 存储value */
Object value; Entry(ThreadLocal<?> k, Object v) {
// 将key的值传入父类WeakReference的构造方法,用弱引用来引用key
super(k);
// value则直接使用上面的强引用
value = v;
}
}

  可以看到,上面的Entry比较特殊,它继承自WeakReference类型,这是Java实现的弱引用。在具体讲解前,我们先来介绍一下不同类型的引用:

强引用:这是Java中最常见的引用,在没有使用特殊引用的情况下,都是强引用,比如Object o = new Object()就是典型的强引用。能让程序员通过强引用访问到的对象,不会被JVM垃圾回收,即使内存空间不够,JVM也不会回收这些对象,而是抛出内存溢出异常;

软引用:软引用描述的是一些还有用,但不是必须的对象。被软引用所引用的对象,也不会被垃圾回收,直到JVM将要发生内存溢出异常时,才会将这些对象列为回收对象,进行回收。在JDK1.2之后,提供了SoftReference类实现软引用;

弱引用:弱引用描述的是非必须的对象,被弱引用所引用的对象,只能生存到下一次垃圾回收前,下一次垃圾回收来临,此对象就会被回收。在JDK1.2之后,提供了WeakReference类实现弱引用(也就是上面Entry继承的类);

虚引用:这是最弱的一种引用关系,一个对象是否有虚引用,完全不会对其生存时间产生影响,我们也不能通过一个虚引用访问对象,使用虚引用的唯一目的就是,能在这个对象被回收时,受到一个系统的通知。JDK1.2之后,提供了PhantomReference实现虚引用;

  介绍完各类引用的概念,我们就可以来分析一下Entry为什么需要继承WeakReference类了。从代码中,我们可以看到,Entrykey值,也就是ThreadLocal的引用传入到了WeakReference的构造方法中,也就是说在ThreadLocalMap中,key的引用是弱引用。这表明,当没有其他强引用指向key时,这个key将会在下一次垃圾回收时被JVM回收。

  为什么需要这么做呢?这么做的目的自然是为了有利于垃圾回收了。如果了解过JVM的垃圾回收算法的应该知道,JVM判断一个对象是否需要被回收,判断的依据是这个对象还能否被我们所使用,举个简单的例子:

public static void main(String[] args) {
Object o = new Object();
o = null;
}

  上面的代码中,我们创建了一个对象,并使用强引用o指向它,然后我们将o置为空,这个时候刚刚创建的对象就丢失了,因为我们无法通过任何引用找到这个对象,从而使用它,于是这个对象就需要被回收,这种判断依据被称为可达性分析。关于JVM的垃圾回收算法,可以参考这篇博客:Java中的垃圾回收算法详解

  好,回归正题,我们开始分析为什么ThreadLocalMap需要让key使用弱引用。假设我们创建了一个ThreadLocal,使用完之后没有用了,我们希望能够让它被JVM回收,于是有了下面这个过程:

// 创建ThreadLocal对象
ThreadLocal tl = new ThreadLocal(); // .....省略使用的过程... // 使用完成,希望被JVM回收,于是执行以下操作,解除强引用
tl = null;

  我们在使用完ThreadLocal之后,解除对它的强引用,希望它被JVM回收。但是JVM无法回收它,因为我们虽然在此处释放了对它的强引用,但是它还有其它强引用,那就是Thread对象的ThreadLocalMapkey。我们之前反复说过,ThreadLocalMapkey就是ThreadLocal对象的引用,若这个引用是一个强引用,那么在当前线程执行完毕,被回收前,ThreadLocalMap不会被回收,而ThreadLocalMap不会被回收,它的key引用的ThreadLocal也就不会回收,这就是问题的所在。而使用弱引用就可以保证,在其他对ThreadLocal的强引用解除后,ThreadLocalMap对它的引用不会影响JVM对它进行垃圾回收。这就是使用弱引用的原因。

2.6 ThreadLocal造成的内存溢出问题

  上面描述了对ThreadLocalMapkey使用弱引用,来避免JVM无法回收ThreadLocal的问题,但是这里却还有另外一个问题。我们看上面Entry的代码发现,key值虽然使用的弱引用,但是value使用的却是强引用。这会造成一个什么问题?这会造成key被JVM回收,但是value却无法被收,key对应的ThreadLocal被回收后,key变为了null,但是value却还是原来的value,因为被ThreadLocalMap所引用,将无法被JVM回收。若value所占内存较大,线程较多的情况下,将持续占用大量内存,甚至造成内存溢出。我们通过一段代码演示这个问题:

public class Main {

    public static void main(String[] args) {
// 循环创建多个TestClass
for (int i = 0; i < 100; i++) {
// 创建TestClass对象
TestClass t = new TestClass(i);
// 调用反复
t.printId();
// *************注意此处,非常关键:为了帮助回收,将t置为null
t = null;
}
} static class TestClass {
int id;
// 每个TestClass对象对应一个很大的数组
int[] arr = new int[100000000];
// 每个TestClass对象对应一个ThreadLocal对象
ThreadLocal<int[]> threadLocal = new ThreadLocal<>(); TestClass(int id) {
this.id = id;
// threadLocal存放的就是这个很大的数组
threadLocal.set(arr);
} public void printId() {
System.out.println(id);
}
}
}

  上面的代码多次创建所占内存非常大的对象,并在创建后,立即解除对象的强引用,让对象可以被JVM回收。按道理来说,上面的代码运行应该不会发生内存溢出,因为我们虽然创建了多个大对象,占用了大量空间,但是这些对象立即就用不到了,可以被垃圾回收,而这个对象被垃圾回收后,对象的id,数组,和threadLocal成员都会被回收,所以所占内存不会持续升高,但是实际运行结果如下:

0
1
2
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at Main$TestClass.<init>(Main.java:23)
at Main.main(Main.java:10)

  可以看到,很快就发生了内存溢出异常。为什么呢?需要注意到,在TestClass的构造方法中,我们将数组arr放入了ThreadLocal对象中,也就是被放进了当前线程的ThreadLocalMap中,作为value存在。我们前面说过,ThreadLocalMapvalue是强引用,这也就意味着虽然ThreadLocal可以被正常回收,但是作为value的大数组无法被回收,因为它仍然被ThreadLocalMap的强引用所指向。于是TestClass对象的超大数组就一种在内存中,占据大量空间,我们连续创建了多个TestClass,内存很快就被占满了,于是发生了内存溢出。而JDK的开发人员自然发现了这个问题,于是有了下面这个解决方案:

public class Main {

    public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
TestClass t = new TestClass(i);
t.printId();
// **********注意,与上面的代码只有此处不同************
// 此处调用了ThreadLocal对象的remove方法
t.threadLocal.remove();
t = null;
}
} static class TestClass {
int id;
int[] arr;
ThreadLocal<int[]> threadLocal; TestClass(int id) {
this.id = id;
arr = new int[100000000];
threadLocal = new ThreadLocal<>();
threadLocal.set(arr);
} public void printId() {
System.out.println(id);
}
}
}

  上面的代码中,我们在将t置为空时,先调用了ThreadLocal对象的remove方法,这样做了之后,再看看运行结果:

0
1
2
// ....神略中间部分
98
99

  做了上面的修改后,没有再发生内存溢出异常,程序正常执行完毕。这是为什么呢?ThreadLocalremove方法究竟有什么作用。其实remove方法的作用非常简单,执行remove方法时,会从当前线程的ThreadLocalMap中删除key为当前ThreadLocal的那一个记录,keyvalue都会被置为null,这样一来,就解除了ThreadLocalMapvalue的强引用,使得value可以正常地被JVM回收了。所以,今后如果我们确认不再使用的ThreadLocal对象,一定要记得调用它的remove方法。

  我们之前说过,如果我们没有调用remove方法,那就会导致ThreadLocal在使用完毕后,被正常回收,但是ThreadLocalMap中存放的value无法被回收,此时将会在ThreadLocalMap中出现keynull,而value不为null的元素。为了减少已经无用的对象依旧占用内存的现象,ThreadLocal底层实现中,在操作ThreadLocalMap的过程中,线程若检测到keynull的元素,会将此元素的value置为null,然后将这个元素从ThreadLocalMap中删除,占用的内存就可以让JVM将其回收。比如说在getEntry方法中,或者是Map扩容的方法中等。

三、总结

  ThreadLocal实现线程独立的方式是直接将值存放在Thread对象的ThreadLocalMap中,Mapkey就是ThreadLocal的引用,且为了有助于JVM进行垃圾回收,key使用的是弱引用。在使用ThreadLocal后,一定要记得调用remove方法,有助于JVMvalue的回收。

四、参考

并发——深入分析ThreadLocal的实现原理的更多相关文章

  1. Java并发—–深入分析synchronized的实现原理

    记得刚刚开始学习Java的时候,一遇到多线程情况就是synchronized,相对于当时的我们来说synchronized是这么的神奇而又强大,那个时候我们赋予它一个名字“同步”,也成为了我们解决多线 ...

  2. 并发——深入分析CountDownLatch的实现原理

    一.前言   最近在研究java.util.concurrent包下的一些的常用类,之前写了AQS.ReentrantLock.ArrayBlockingQueue以及LinkedBlockingQu ...

  3. 聊聊并发(一)深入分析Volatile的实现原理

    本文属于作者原创,原文发表于InfoQ:http://www.infoq.com/cn/articles/ftf-java-volatile 引言 在多线程并发编程中synchronized和Vola ...

  4. 源码|ThreadLocal的实现原理

    ThreadLocal也叫"线程本地变量"."线程局部变量": 其作用域覆盖线程,而不是某个具体任务: 其"自然"的生命周期与线程的生命周期 ...

  5. (转载)java高并发:CAS无锁原理及广泛应用

    java高并发:CAS无锁原理及广泛应用   版权声明:本文为博主原创文章,未经博主允许不得转载,转载请注明出处. 博主博客地址是 http://blog.csdn.net/liubenlong007 ...

  6. Java并发机制的底层实现原理之volatile应用,初学者误看!

    volatile的介绍: Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现 ...

  7. 《Java并发编程的艺术》Java并发机制的底层实现原理(二)

    Java并发机制的底层实现原理 1.volatile volatile相当于轻量级的synchronized,在并发编程中保证数据的可见性,使用 valotile 修饰的变量,其内存模型会增加一个 L ...

  8. Java并发编程笔记之ConcurrentHashMap原理探究

    在多线程环境下,使用HashMap进行put操作时存在丢失数据的情况,为了避免这种bug的隐患,强烈建议使用ConcurrentHashMap代替HashMap. HashTable是一个线程安全的类 ...

  9. 那些年读过的书《Java并发编程的艺术》一、并发编程的挑战和并发机制的底层实现原理

    一.并发编程的挑战 1.上下文切换 (1)上下文切换的问题 在处理器上提供了强大的并行性就使得程序的并发成为了可能.处理器通过给不同的线程分配不同的时间片以实现线程执行的自动调度和切换,实现了程序并行 ...

随机推荐

  1. Checkbox 勾上 不让勾下 同步手动刷新复选框状态 iview

    <Checkbox v-show="!disabledForm" ref="youwubianhuaRef" :value="youwubian ...

  2. computed 里面 不能写箭头函数 都要写 function () {} 否则页面会都不显示

    computed 里面 不能写箭头函数 都要写 function () {} 否则页面会都不显示

  3. Leetcode_面试题 17.24. 最大子矩阵

    最大子矩阵问题,n是200,枚举上下行,O(N)求一下最大子段和. code class Solution { public: vector<int> getMaxMatrix(vecto ...

  4. MySQL windows中的存储备份

    数据备份对于经常在运维部署方面的工作者来说,是一件相对简单的事情,都可以通过某一个SQL工具进行备份,但是如果在项目运行当中,我们需要对数据进行实时,或者是每隔一星期,一个月,等等进行数据的备份,这样 ...

  5. 9. selenium+request方式的cookie绕过

    1. 首先确认POST请求的content-type类型 2. 查看cookies数据 3. 找到对应的headers数据 4. 如果是application/json,传入的json数据需要时jso ...

  6. 数据库-第三章 关系数据库标准语言SQL-3.3 数据查询

    数据查询 例: 一.单表查询 1.定义 是指仅涉及一个表的查询 2.选择表中的若干列 查询指定列 例: 查询全部列 例: 查询经过计算的值 例: 3.选择表中的若干元组 消除取值重复的行 例: 查询满 ...

  7. Redhat 线下赛 WEB WP

    赛制 给每个参赛队伍所有题目的gamebox,参赛队伍在开赛时就能获取到所有题目的源码,可以选择先防御后攻击或先攻击后防御,只要拿到gamebox上的flag,机器人就会自动帮你攻击场上所有未防御选手 ...

  8. VIM不正常退出产生的swp文件

    VIM不正常退出产生的swp文件 当你非正常关闭vim编辑器时(比如直接关闭终端或者电脑断电),会生成一个.swp文件,这个文件是一个临时交换文件,用来备份缓冲区中的内容. 第一次产生的交换文件名为“ ...

  9. java获取近几天的日期

    最近在写接口的时候老遇见从mysql中获取近几天数据的需求,获取日期这块不是很熟,网上看了很多但是代码量都太大,还是问了下别人,写了三行代码就解决了,不多说 贴代码了 下面是我获取近十天,每天的日期: ...

  10. Leetcode——回溯法常考算法整理

    Leetcode--回溯法常考算法整理 Preface Leetcode--回溯法常考算法整理 Definition Why & When to Use Backtrakcing How to ...