美文网首页程序员代码改变世界程序员
对ThreadLocal实现原理的一点思考

对ThreadLocal实现原理的一点思考

作者: 张丰哲 | 来源:发表于2017-08-04 07:19 被阅读5911次

前言

《透彻理解Spring事务设计思想之手写实现》中,已经向大家揭示了Spring就是利用ThreadLocal来实现一个线程中的Connection是同一个,从而保证了事务。本篇博客将带大家来深入分析ThreadLocal的实现原理。

ThreadLocal是什么、有什么、能做什么?

ThreadLocal提供一个线程(Thread)局部变量,访问到某个变量的每一个线程都拥有自己的局部变量。说白了,ThreadLocal就是想在多线程环境下去保证成员变量的安全。

ThreadLocal提供的方法

ThreadLocal API

对于ThreadLocal而言,常用的方法,就是get/set/initialValue方法。

我们先来看一个例子

demo

运行结果

是你想象中的结果么?

很显然,在这里,并没有通过ThreadLocal达到线程隔离的机制,可是ThreadLocal不是保证线程安全的么?这是什么鬼?

虽然,ThreadLocal让访问某个变量的线程都拥有自己的局部变量,但是如果这个局部变量都指向同一个对象呢?这个时候ThreadLocal就失效了。仔细观察下图中的代码,你会发现,threadLocal在初始化时返回的都是同一个对象a!

看一看ThreadLocal源码

我们直接看最常用的set操作:

set 线程局部变量 createMap

你会看到,set需要首先获得当前线程对象Thread;

然后取出当前线程对象的成员变量ThreadLocalMap;

如果ThreadLocalMap存在,那么进行KEY/VALUE设置,KEY就是ThreadLocal;

如果ThreadLocalMap没有,那么创建一个;

说白了,当前线程中存在一个Map变量,KEY是ThreadLocal,VALUE是你设置的值。

看一下get操作:

get

这里其实揭示了ThreadLocalMap里面的数据存储结构,从上面的代码来看,ThreadLocalMap中存放的就是Entry,Entry的KEY就是ThreadLocal,VALUE就是值。

ThreadLocalMap.Entry:

弱引用?

在JAVA里面,存在强引用、弱引用、软引用、虚引用。这里主要谈一下强引用和弱引用。

强引用,就不必说了,类似于:

A a = new A();

B b = new B();

考虑这样的情况:

C c = new C(b);

b = null;

考虑下GC的情况。要知道b被置为null,那么是否意味着一段时间后GC工作可以回收b所分配的内存空间呢?答案是否定的,因为即便b被置为null,但是c仍然持有对b的引用,而且还是强引用,所以GC不会回收b原先所分配的空间!既不能回收利用,又不能使用,这就造成了内存泄露

那么如何处理呢?

可以c = null;也可以使用弱引用!(WeakReference w = new WeakReference(b);)

分析到这里,我们可以得到:

内存结构图

这里我们思考一个问题:ThreadLocal使用到了弱引用,是否意味着不会存在内存泄露呢?

首先来说,如果把ThreadLocal置为null,那么意味着Heap中的ThreadLocal实例不在有强引用指向,只有弱引用存在,因此GC是可以回收这部分空间的,也就是key是可以回收的。但是value却存在一条从Current Thread过来的强引用链。因此只有当Current Thread销毁时,value才能得到释放。

因此,只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间内不会被回收的,就发生了我们认为的内存泄露。最要命的是线程对象不被回收的情况,比如使用线程池的时候,线程结束是不会销毁的,再次使用的,就可能出现内存泄露。

那么如何有效的避免呢?

事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。我们也可以通过调用ThreadLocal的remove方法进行释放!

好了,到这里,ThreadLocal的剖析就完成了,自己对ThreadLocal的认识又深入了些,^_^

相关文章

网友评论

  • sadamu0912:运行main方法,并不能实现并发数据安全啊,多运行几次,你会发现,到不了25。和并发资源访问不加synchronized一样的结果。总之,和并发的数据安全没有啥关系
  • a1fbf0f3b871:大牛请问为什么破坏了线程隔离的机制呢
    张丰哲:@愤怒的苹果_64a7 恩恩,知道就好啊,欢迎常来看看~
    a1fbf0f3b871:我知道了,ThreadLocal可以给一个初始值,而每个线程都会获得这个初始化值的一个副本
    private static final ThreadLocal<A> threadLoclal = new ThreadLocal<A>(){
    protected A initialValue() {
    return a;
    };
    };
    这里,返回的是已经定义好的对象a,而不是new A()
    a1fbf0f3b871:为什么指向同一个对象会失效
  • vincent_ren:您好,看了你的文章,我对ThreadLocal也有了一个更深的认识,有两个问题想请教一下,1. 我在ThreadLocalMap中的set/getEntry方法中为什么没有看到对key为null的判断?(jdk版本1.8.0_131)
    2. 您是如何使用ThreadLocal的,ThreadLocal何时会被置为null?我是通过static final 持有一个ThreadLocal实例的,这样应该不会存在为nul的情况吧l
    张丰哲:@vincent_ren
    private void set(ThreadLocal key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    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) {==================》如果KEY为NULL,那么会进行擦除操作
    replaceStaleEntry(key, value, i);
    return;
    }
    }
    vincent_ren:@张丰哲 您好,我看了jdk1.7的java.lang.ThreadLocal.ThreadLocalMap#set方法,
    private void set(ThreadLocal key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1); // 这里直接使用key了
    //省略
    }
    我不明白你说的判断null隐藏在哪?可能我的问题比较小白,烦请指导!谢谢。
    张丰哲:第一个问题:key为 null的判断隐藏在ThreadLocalMap的set方法中,你可以去看一下,我用的是JDK1.7。
    第二个问题:ThreadLocal一般是为了方便在线程中进行参数传递。本文是分析下使用ThreadLocal可能造成的内存泄露情况,可以手动remove下或者置为null,
    :blush:

本文标题:对ThreadLocal实现原理的一点思考

本文链接:https://www.haomeiwen.com/subject/cryulxtx.html