Java 多线程--ThreadLocal基于源码流程分析

一、ThreadLocal作用与数据结构

ThreadLocal简称人手一支笔,可以在自己的线程保存一份变量,每个线程Thread都拥有一份副本变量,多个线程之间互不干扰。

作用

在这里插入图片描述

用线程隔离的思想做到线程安全

以往多个线程对共享变量进行操作的时候,一般都是加锁。

比如用synchronized或者ReentranLock这种锁,但是除了加锁,还有一种思想,让每一个线程都拥有自己一份专属变量副本,人手一支笔,用线程隔离的思想就做到了线程安全。

线程内需要保持全局变量

比如在web场景下,在某个service中的方法获取到了用户的信息,但是这个方法里面还需要用到用户的信息,一般的做法都是通过参数的形式传递下去:

public void service(){
    User user = new User();
    service2(user);
    service3(user);
}
public void service2(User user){}
public void service3(User user){}

这样不好维护,因此把变量保存在ThreadLocal中,做到线程内的全局变量

数据结构

路线:Thread-> ThreadLocalMap-> Entry-> key | value

Thread类当中有一个threadLocals的变量,这个变量的类型是TheadLocalMapThreadLocalMap来自ThreadLocal的静态内部类,也就是说每个Thread类都有一份map,真正做到线程隔离。

ThreadLocalMap的底层跟HashMap有点类似,map的底层是Entry数组,跟HashMap一样会发生Hash冲突,但是跟HashMap的哈希冲突解决方法不一样,在set方法中都有体现,而且还涉及到探测性清除跟启发式清除。

底层的Entry里面是没有大家常说的key的,只有value,那么大家常说的key是来自于Entry继承的弱引用weakReference,里面有一个Refenct字段指向ThreadLocal,可以近似看做成key是ThreadLocal,但是实际上是ThreadLocal的弱引用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qZr7sCFd-1600503985156)(61225E9B45D94165B6A2046FC8D77747)]

二、内存泄露问题吗?

正常情况下,如果线程终止,value也会被回收,不会存在内存泄露问题。但是如果Thread是类似像线程池这种的线程,线程一直存在的,具体流程可以按照这么分析:

Thread-> ThreadLocalMap-> entry-> key|value

下次GC时,key被回收了,但是value还存在着强引用,没办法被回收,就存在内存泄露了。

为什么要设计成弱引用?
主要是ThreadLocalMap的生命周期跟Thread有关,如果一个线程长期不关闭,像线程池这种,那么就会一直存在像上面那样的强引用关系链,无法回收,假如使用弱引用机制,那么下次GC时就可以回收key。虽然弱引用会发生泄漏的问题,但是里面提供了探测性清理、启发式清理以及remove方法都能清除value。

如何避免内存泄露(阿里手册)
调用remove方法,删除对应的Entry对象。

三、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);
}

进入到set方法里,获取当前的线程,获取当前线程的ThreadLocalMap,如果ThreadLocalMap为空则会创建。

Hash计算

可以看到,hash值的计算是通过threadLocalHashCode& 长度-1来实现的,那么threadLocalHashCode是跟一个黄金分隔数(HASH_INCREMENT)有关,每次通过整形原子类来获取该黄金分隔数的getAndAdd,使用黄金分隔数目的是为了让整个hash更加分布均匀。


回到set源码上面来,这个时候计算好index后,开始put数据,会发生下面四种情况:

  • 当前槽位为空,那么直接将数据放到槽位即可。
  • 当前槽位不为空,但是key相等,更新value即可。
  • 当前槽位不为空,key也不等,那么会往后遍历,找到一个Entry为null的槽,将数据放到该槽上即可。
  • 当前槽位不为空,key也不等,那么会往后遍历,但是找到Entry为null的槽之前遇到了key过期的,也就是key为null的结点,因此进行下一个阶段replaceStaleEntry

replaceStaleEntry

这个阶段分为两步,首先设置两个变量:

  • slotToExpunge:记录当前槽位最前的一个key为null的index下标,目的是看看当前槽位之前有没有过期的key,方便后面做探测性检测。
  • staleSlot:当前槽位。

在这里插入图片描述

向前迭代:从当前槽位向前迭代,如果keynull,则设置slotToExpunge为当前下标,直到entrynull为止。


向后迭代

在这里插入图片描述

从当前槽位向后迭代,分为两种情况:

1.往后迭代的过程中遇到key相等的情况,交换槽的内容,更新value值就OK了,如上图所示。

2.往后迭代的过程中没有遇到key相等情况,则新建Entry,并取代之前staleSlot槽位的内容,如下图所示。

在这里插入图片描述


可以发现无论是哪种情况,最后面都会执行cleanSomeSlots(expungeStaleEntry(slotToExpunge), len),也就是第四种情况添加完成后,会利用之前的slotToExpunge变量来进行探测式清理。

在这里插入图片描述

探测式清理–expungeStaleEntry

探测式清理的方法名叫expungeStaleEntry,大概逻辑如下:

从之前的slotToExpunge出发。

如果遇到key==null,则将当前Entry设置为null,并且size–。

如果遇到key!=null,此时会对当前的key重新hash计算一下index。

- 如果index!=当前下标,表明之前没有发生哈希冲突,便会重新将该Entry放回应该属于他的位置,  
如果之前的位置有值,那么就按照开放地址法解决,在后面找一个Entry为null的槽位存储起来。  
这样操作之后,key不等于null的结点也就会离原来的位置更近了,查找更加方便。  


- 如果index==当前下标,表明之前没有发生哈希冲突,那么没事发生。  

探测式清理一边清理过期的entry,一边对不为null的key进行hash定位,放在离原本位置更近的地方。

探测式清理遇到Entry为null的结点就会停下来,并返回当前的下标index。

之后从当前下标开始进行启发式清理。

启发式清理–cleanSomeSlots

从名字就可以看出cleanSomeSlots,清理一些肮脏的槽位,刚才说到,探测式清理遇到Entry为null的结点会停止清理,并返回当前index,但是不能保证index后面还有没有过期的结点,所以开启了启发式清理。

启发式清理利用当前下标的一个位预算,每次清理一个,会将当前下标通过位运算右移两位,直到右移结果为0。

在这里插入图片描述


自此,刚才说的set是第四种情况下,会开启向前迭代和向后迭代,并且完成后会开启探测性清理和启发式清理,并回到set函数中。调用逻辑如下:

在这里插入图片描述

扩容前的准备

如果当前的size >= thresShlod(size的3分之2),那么会进行一次探测式清理。

清理完成之后如果size >= threShold的4分之3,那么就会开启resize。

在这里插入图片描述

resize挺简单的,扩容无非就是newTable,旧table rehash就可以了。

四、get()源码详解

在这里插入图片描述

key通过原子类累加黄金分隔数得到threadLocalHashCode得到Hash值定位到槽位,如果当前槽位有值并且key相等则直接return。

在这里插入图片描述

否则会往后遍历,如果遇到key相等的情况就直接返回。

如果遇到了过期key的情况,那么开启一次探测性清理,清理完成之后再判断key相不相等。

五、ThreadLocal的实际使用场景

除了开头提到的,用线程隔离的思想做到线程安全,以及线程内需要线程的全局变量。

在Spring中的应用

在实际应用中也有存在,比如我们知道Spring中bean基本上都是单例的,而我们把web服务部署在Tomcat的时候,实际上Tomcat是通过多线程处理请求的方式来访问我们的bean,也就是说是多线程处理单例对象,如果涉及到多线程对单例对象中的成员变量进行写操作时,肯定会发生线程安全,因此可以用ThreadLocal这种方式。

在Spring中就有DateTimeContextHolder或者requestContextHolder等等都是对ThreadLocal的封装。

参考

https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/Multithread/ThreadLocal.md#threadlocal%E4%BB%A3%E7%A0%81%E6%BC%94%E7%A4%BA

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值
>