资讯专栏INFORMATION COLUMN

深入了解Threadlocal

qiangdada / 2900人阅读

摘要:通过将保存在中,每个线程都会拥有属于自己的,代码如下所示然后你就可以安心地调用了,不用考虑线程安全问题。这样设计的好处就是,当线程死掉之后,没有强引用,方便收集器回收。

前言

想必大家都对Threadlocal很熟悉吧,今天我们就一起来深入学习一下。Threadlocal我更倾向于将其翻译成线程局部变量。它有什么用处呢?Threadlocal对象通常用于防止对可变的单实例变量或全局变量进行共享。在spring中,通过将事务上下文保存在静态的threadlocal中,当框架代码需要判断当前运行的是哪一个事务时,只需要从ThreadLocal对象中获取事务上下文,这种机制很方便,避免了在每个方法都要传递上下文信息。

一个小例子

众所周知,SimpleDateFormat不是一个线程安全的类,在多线程环境下使用同一个实例是不正确的。通过将SimpleDateFormat保存在Threadlocal中,每个线程都会拥有属于自己的SimpleDateFormat,代码如下所示:

public class DateFormatUtil {

    public static ThreadLocal dataFormatlocal = new ThreadLocal() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat();
        }
    };

    public static SimpleDateFormat getInstance() {
        return dataFormatlocal.get();
    }

}

然后你就可以安心地调用了,不用考虑线程安全问题。当然你也可以在每个线程内部调用new SimpleDateFormat(),但现实情况是大部分开发可能并不知道它是线程不安全的,并且将其封装成一个单例的工具类。至于如何使用,全凭各位读者爱好,这个例子主要用来演示Threadloca的基本用法。

脑洞大开---自己设计个threadlocal

通过之前的演示,我们已经大致知道threadlocal怎么用,并且清楚threadlocal保证了每个线程都有一个局部变量副本。如果以此为需求,让我们自己去设计,会是什么样子的呢?

static Map threadlocal = new HashMap();

这就是我设计的....好吧(╯▽╰),原谅我水平有限....在这个hashMap中,threadId为key,Object为value,也是可以实现Threadlocal的。那么我们现在要来考虑几个问题。jdk是这样设计的吗?这样设计很low,但是low在哪了?
答:大师们当然不是这样设计了。如果这样设计,每个线程的变量都会永久的保存在hashMap中,存在内存泄漏。

好奇宝宝---jdk是如何实现threadlocal的

low的写法我们已经见过了,现在我们一起来分下jdk是如何设计的,本文引用jdk1.8。
让我们看下threadlocal的结构图:

类核心方法set、get、initialValue、setInitialValue、remove,后面主要围绕着这几个方法介绍。

类核心变量threadLocalHashCode,nextHashCode,HASH_INCREMENT,其中nextHashCode和HASH_INCREMENT都是静态的,所以对于一个threadlocal对象,成员变量只有一个threadLocalHashCode,这是一个自定义的hash函数,主要为了减少散列桶的冲突(不是本文重点,好奇的可以看下这篇博客https://www.cnblogs.com/ilell...)。

那到底Threadlocal将变量存到哪了呢?眼尖的同学肯定已经发现了ThreadLocalMap,再来看下ThreadlocalMap的结构:

相信读过hashMap源码同学的一定会觉得非常眼熟,ThreadlocalMap的主体也是Entry[],有resize()和rehash(),区别仅仅就是在于没有使用链表结构和红黑树来处理散列冲突了。Entry的结构我们也很有必要了解一下:

Entry继承了一个弱引用,这里简单介绍下弱引用的知识。弱引用是用来描述非必需对象的,被弱引用关联的对象只能生存到下次垃圾收集发生之前。无论当前内存是否足够,都会回收掉只被弱引用关联的对象。很多面试官都喜欢问Threadlocal是怎么发生内存泄漏的,其实就是在问这个,这个需要我们对Threadlocal有一个整体的了解才能明白,所以放在最后讲。

当我们看完Threadlocal的结构了,我们发现ThreadlocalMap是用来存储的数据结构,那它是怎么和线程关联起来的呢?这里我们开始研究Threadlocal.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);
    }

步骤1:获取当前线程
步骤2:通过当前线程获取ThreadLocalMap
步骤3:如果map不为null,将当前线程和value放到map中,否则创建一个map。
如何和线程绑定的玄机就在getMap(t)中。

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

看到这里,我们就知道了原来Threadlocal有一个内部类ThreadlocalMap,对于任何一个线程都会有唯一的一个ThreadlocalMap来对应,而这个map实际并不存储在Threadlocal中,而是存在Thread当中,只不过由Threadlocal暴露了一套api来维护Thread的ThreadLocalMap。这样设计的好处就是,当线程死掉之后,ThreadLocalMap没有强引用,方便收集器回收。

继续来看get()

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

如果前面的流程看懂了,这就很简单了。当ThreadLocalMap为null或者Entry为null的时候将会调用setInitialValue();

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;
    }

流程也很简单,调用initialValue()初始化一个值,获取当前线程的ThreadLocalMap,后面的流程之前都已经介绍过了,这里不再重复,我们主要来看下initialValue()

protected T initialValue() {
        return null;
    }

请注意,protected修饰,return null;这是一个初始化值得方法,也就意味着如果业务允许的话,需要我们自己实现initialValue();

ThreadLocal的remove实现

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

ThreadLocalMap的remove实现

private void remove(ThreadLocal key) {
            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)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

都非常简单,就留给大家自己看了~。~.

Threadlocal容易发生内存泄露?

先回顾一下ThreadlocalMap的结构

static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
        .....
        .....
}

然后套用网上的一张图(实线表示强引用,虚线表示虚引用)

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:ThreadLocal Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄露。其实jdk已经考虑到了这种情况,ThreadLocalMap的genEntry函数或者set函数会去遍历将key为null的给移除掉,但这明显不是所有情况都成立的,所以需要调用者自己去调用remove函数,手动删除掉需要的threadlocal,防止内存泄露。然后jdk并不建议在栈内声明threadlocal,而是建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
放图证明这是jdk说的~~

总结

再不睡觉,就天明了..

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/69665.html

相关文章

  • 深入理解Python中的ThreadLocal变量(下)

    摘要:具体怎么实现的呢,思想其实特别简单,我们在深入理解中的变量上一文的最后有提起过,就是创建一个全局字典,然后将线程或者协程标识符作为,相应线程或协程的局部数据作为。 在上篇我们看到了 ThreadLocal 变量的简单使用,中篇对python中 ThreadLocal 的实现进行了分析,但故事还没有结束。本篇我们一起来看下Werkzeug中ThreadLocal的设计。 Werkzeug...

    dadong 评论0 收藏0
  • 深入理解Python中的ThreadLocal变量(中)

    摘要:在深入理解中的变量上中我们看到的引入,使得可以很方便地在多线程环境中使用局部变量。特别需要注意的是,基类的并不会屏蔽派生类中的创建。到此,整个源码核心部分已经理解的差不多了,只剩下用来执行清除工作。 在 深入理解Python中的ThreadLocal变量(上) 中我们看到 ThreadLocal 的引入,使得可以很方便地在多线程环境中使用局部变量。如此美妙的功能到底是怎样实现的?如果你...

    DataPipeline 评论0 收藏0
  • 追踪解析 ThreadLocal 源码

    摘要:虽然类名中带有字样,但是实际上并不是接口的子类。是弱连接接口,这意味着如果仅有指向某一类,其任然有可能被回收掉。这里使用弱连接的意义,是为了防止业务代码中置空对象,但是由于存在连接可达,所以仍然无法回收掉该对象的情况发生。 零 前期准备 0 FBI WARNING 文章异常啰嗦且绕弯。 1 版本 JDK 版本 : OpenJDK 11.0.1 IDE : idea 2018.3 2 T...

    wawor4827 评论0 收藏0
  • Java面试题必备知识之ThreadLocal

    摘要:方法,删除当前线程绑定的这个副本数字,这个值是的值,普通的是使用链表来处理冲突的,但是是使用线性探测法来处理冲突的,就是每次增加的步长,根据参考资料所说,选择这个数字是为了让冲突概率最小。 showImg(https://segmentfault.com/img/remote/1460000019828633); 老套路,先列举下关于ThreadLocal常见的疑问,希望可以通过这篇学...

    Maxiye 评论0 收藏0

发表评论

0条评论

qiangdada

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<