博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
深入了解Threadlocal
阅读量:6972 次
发布时间:2019-06-27

本文共 5801 字,大约阅读时间需要 19 分钟。

前言

想必大家都对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函数,主要为了减少散列桶的冲突(不是本文重点,好奇的可以看下这篇博客)。


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

clipboard.png

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

clipboard.png

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说的~~

clipboard.png

总结

再不睡觉,就天明了..

转载地址:http://xuosl.baihongyu.com/

你可能感兴趣的文章
JavaScript数据类型
查看>>
Emacs 番茄钟 pomidor
查看>>
oracle 分组查询 子查询 统计查询 FROM加子查询临时表 查询高于平均工资 示例代码...
查看>>
.Net程序随系统开机启动(仿Foxmail托盘效果控制)
查看>>
mouseleave,mouseout 和mouseover ,mouseenter区别
查看>>
eclipse 快捷键设置
查看>>
c++11 多线程间共享数据 <c++ concurrency in action>
查看>>
py 的 第 24 天
查看>>
BZOJ 3456 城市规划
查看>>
多轨视频编辑技术支持
查看>>
who
查看>>
sql where 1=1
查看>>
HDU2425:Hiking Trip(BFS+优先队列)
查看>>
[Oracle]ORA-01461: can bind a LONG value only for insert into a LONG column
查看>>
xcode6新建工程
查看>>
单向路由算法
查看>>
RabbitMQ系列教程之二:工作队列(Work Queues)
查看>>
scrapy入门教程
查看>>
Linux学习之CentOS(三十三)--DNS基础及域名系统架构
查看>>
leetcode349
查看>>