ThreadLocal的理解(二)

ThreadLocalMap源码分析

在分析ThreadLocal方法的时候,我们了解到ThreadLocal的操作实际上是围绕ThreadLocalMap展开的。

ThreadLocalMap的源码相对比较复杂,我们从以下三个方面进行讨论。

基本结构

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。
在这里插入图片描述

成员变量


/**

* 初始容量 - 必须是2的整次幂

**/

private static final int INITIAL_CAPACITY = 16;



/**

*存放数据的table ,Entry类的定义在下面分析,同样,数组的长度必须是2的整次幂

**/

private Entry[] table;



/**

*数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值

**/

private int size = 0;



/**

*进行扩容的阈值,表使用量大于它的时候进行扩容

**/

private int threshold; // Default to 0

跟HashMap类似,INITIAL_CAPACITY代表这个Map的初始容量;table是一个Entry类型的数组,用于存储数据;size代表表中的存储数目;threshold代表需要扩容时对应的size的阈值。

存储结构 - Entry


/*

*Entry继承WeakRefefence,并且用ThreadLocal作为key.

如果key为nu11(entry.get()==nu11),意味着key不再被引用,

*因此这时候entry也可以从table中清除。

*/

static class Entry extends weakReference<ThreadLocal<?>>{



object value;Entry(ThreadLocal<>k,object v){

    super(k);

    value = v;

}}

在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。

另外,Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。

弱引用和内存泄漏

有些程序员在使用ThreadLocal的过程中会发现有内存泄漏的情况发生,就猜测这个内存泄漏跟Entry中使用了弱引用的key有关系。这个理解其实是不对的。

我们先来回顾这个问题中涉及的几个名词概念,再来分析问题。

内存泄漏相关概念

Memory overflow:内存溢出,没有足够的内存提供申请者使用。

Memory leak:内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统溃等严重后果。I内存泄漏的堆积终将导致内存溢出。

弱引用相关概念

Java中的引用有4种类型:强、软、弱、虚。当前这个问题主要涉及到强引用和弱引用:

强引用("Strong"Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。

弱引用(WeakReference),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

如果key使用强引用,那么会出现内存泄漏?

假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄漏吗?

此时ThreadLocal的内存图(实线表示强引用)如下:
在这里插入图片描述
假设在业务代码中使用完ThreadLocal,threadLocal Ref被回收了

但是因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收。

在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。

也就是说,ThreadLocalMap中的key使用了强引用,是无法完全避免内存泄漏的。

如果key使用弱引用,那么会出现内存泄漏?

在这里插入图片描述
同样假设在业务代码中使用完ThreadLocal,threadLocal Ref被回收了。

由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例,所以threadloca就可以顺利被gc回收,此时Entry中的key=null。

但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry-> value,value不会被回收,而这块value永远不会被访问到了,导致value内存泄漏。

也就是说,ThreadLocalMap中的key使用了弱引用,也有可能内存泄漏。

出现内存泄漏的真实原因

比较以上两种情况,我们就会发现,内存泄漏的发生跟ThreadLocalMap中的key是否使用弱引用是没有关系的。那么内存泄漏的的真正原因是什么呢?

细心的同学会发现,在以上两种内存泄漏的情况中,都有两个前提:

  • 没有手动删除这个Entry
  • CurrentThread依然运行

第一点很好理解,只要在使用完ThreadLocal,调用其remove方法删除对应的Entry,就能避免内存泄漏。

第二点稍微复杂一点,由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完ThreadLocal的使用,如果当前Thread也随之执行结束,ThreadLocalMap自然也会被gc回收,从根源上避免了内存泄漏。

综上,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread-样长,如果没有手动删除对应key就会导致内存泄漏。

为什么要使用弱引用?

根据刚才的分析,我们知道了:无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。

要避免内存泄漏有两种方式:

  • 使用完ThreadLocal,调用其remove方法删除对应的Entry
  • 使用完ThreadLocal,当前Thread也随之运行结束

Hash冲突的解决

hash冲突的解决是Map中的一个重要内容。我们以hash冲突的解决为线索,来研究一下ThreadLocalMap的核心源码。

首先从ThreadLocal的set方法入手


public void set(T value){

	Threadt=Thread.currentThread();

    ThreadLoca1.ThreadLocalMap map=getMap(t);

    if(mapl @= nu11)

//调用了ThreadLocalMap的set方法I 

        map.set(this,value);

    else 

        createMap(t,value);

}



ThreadLocal.ThreadLocalMap getMap(Thread t){

	return t.threadLocals;

}



void createMap(Thread t,T firstValue){

//调用了ThreadLocalMap的构造方法

t.threadlocals=new ThreadLocal.ThreadtocalMap(this,firstValue);

这个方法我们刚才分析过,其作用是设置当前线程绑定的局部变量

首先获取当前线程,并根据当前线程获取一个Map

如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)(这里调用了ThreadLocalMap的set方法)

如果Map为空,则给该线程创建Map,并设置初始值(这里调用了ThreadLocalMap的构造方法)

这段代码有两个地方分别涉及到ThreadLocalMap的两个方法,我们接着分析这两个方法

构造方法

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
在这里插入图片描述
构造函数首先创建一个长度为16的Entry数组,然后计算出firstKey对应的索引,然后存储到table中,并设置size和threshold。

重点分析:int i = firstKey.threadLocalHashCode & ( INITIAL_CAPACITY - 1)

关于:threadLocalHashCode
在这里插入图片描述
这里定义了一个Atomiclnteger类型,每次获取当前值并加上HASHINCREMENT,HASH_INCREMENT =

0x61c88647,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里,也就是EntryI table中,这样做可以尽量避免hash冲突。

关于&(INITIAL_CAPACITY-1)

计算hash的时候里面采用了hashCode&(size-1)的算法,这相当于取模运算hashCode%size的一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证保证在索引不越界的前提下,使得hash发生冲突的次数减小。

Get方法

在这里插入图片描述
在这里插入图片描述
代码执行流程

  • 首先还是根据key计算出索引i,然后查找位置上的Entry,
  • 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值,
  • 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry,
  • 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。

线性探测法解决Hash冲突

该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。

举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将1401得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

按照上面的描述,可以把Entry table看成一个环形数组。

ThreadLocal使用场景

源码使用场景

ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。

例如,用于 Spring实现事务隔离级别的源码

Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在TransactionSynchronizationManager这个类里面,代码如下所示:


private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);



	private static final ThreadLocal<Map<Object, Object>> resources =

			new NamedThreadLocal<>("Transactional resources");



	private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =

			new NamedThreadLocal<>("Transaction synchronizations");



	private static final ThreadLocal<String> currentTransactionName =

			new NamedThreadLocal<>("Current transaction name");

Spring的事务主要是ThreadLocal和AOP去做实现的,我这里提一下,大家知道每个线程自己的链接是靠ThreadLocal保存的就好了

用户使用场景1

除了源码里面使用到ThreadLocal的场景,你自己有使用他的场景么?

之前我们上线后发现部分用户的日期居然不对了,排查下来是SimpleDataFormat的锅,当时我们使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。

其实要解决这个问题很简单,让每个线程都new 一个自己的 SimpleDataFormat就好了,但是1000个线程难道new1000个SimpleDataFormat?

所以当时我们使用了线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。

用户使用场景2

我在项目中存在一个线程经常遇到横跨若干方法调用,需要传递的对象,也就是上下文(Context),它是一种状态,经常就是是用户身份、任务信息等,就会存在过渡传参的问题。

使用到类似责任链模式,给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,对象参数就传不进去了,所以我使用到了ThreadLocal去做了一下改造,这样只需要在调用前在ThreadLocal中设置参数,其他地方get一下就好了。


before

  

void work(User user) {

    getInfo(user);

    checkInfo(user);

    setSomeThing(user);

    log(user);

}



then

  

void work(User user) {

try{

	  threadLocalUser.set(user);

	  // 他们内部  User u = threadLocalUser.get(); 就好了

    getInfo();

    checkInfo();

    setSomeThing();

    log();

    } finally {

     threadLocalUser.remove();

    }

}

我看了一下很多场景的cookie,session等数据隔离都是通过ThreadLocal去做实现的

在Android中,Looper类就是利用了ThreadLocal的特性,保证每个线程只存在一个Looper对象。


static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

private static void prepare(boolean quitAllowed) {

    if (sThreadLocal.get() != null) {

        throw new RuntimeException("Only one Looper may be created per thread");

    }

    sThreadLocal.set(new Looper(quitAllowed));

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值