threadlocal使用场景_ThreadLocal,似如爱的logo

ThreadLocal概述

ThreadLocal 字面意思:“线程变量”,即它为每个使用该变量的线程都提供了一个独有的副本,互不影响且决了线程并发访问的冲突。

  1. 线程并发:在多线程并发场景下使用。
  2. 传递数据:可以通过ThreadLocal在同一线程,不同组件中传递公共变量。
  3. 线程隔离:每个线程变量都是独立的,不会相互影响。

我接触ThreadLocal最多的就是上下文数据的访问:

public class ContextUtil{
 private static final NamedThreadLocal REQUEST_INFO_THREAD_LOCAL = new NamedThreadLocal<>("requestInfo");
    ...
}

这样可能你就有疑问了,“那我们用的Synchronized 关键字不也是为了解决线程并发访问的冲突么?他俩有什么区别么?”

Synchronized和ThreadLocal的区别

Synchronized虽然能起到相同的作用,但是它消耗的时间即降低时间来解决访问冲突

而ThreadLocal是通过每个线程单独一份存储空间、降低空间利用率、消耗内存来解决访问冲突

两者重要的区别在此,这也同样决定了两者在应用场景会有大的不同

ThreadLocal原理

每个Thread内部都有一个ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,里面用Entry存储着当前ThreadLocal对象的key和对应的值,所以我们调动ThreadLocal的set(T)方法的时候,其实只是把当前的ThreadLocal对象作为key,把value放在每个线程的ThreadLocalMap中,ThreadLocal其实就是把每个变量在不同的线程中都创建了一个副本,正因为如此,ThreadLocal才能够起到内存隔离。

32d4609f8931ecd315f301c234d89e4a.png

set方法

首先获取线程,然后获取线程的Map。如果Map不为空则将当前ThreadLocal的引用作为key设置到Map中。如果Map为空,则创建一个Map并设置初始值。ec5191cc4dcbbf0d0a2793a175aa4fa5.png

get方法

首先获取当前线程,然后获取Map。如果Map不为空,则Map根据ThreadLocal的引用来获取Entry,如果Entry不为空,则获取到value值,返回。如果Map为空或者Entry为空,则初始化并获取初始值value,然后用ThreadLocal引用和value作为key和value创建一个新的Map。ebf9ebc10919b0498272061dd00d6931.png

remove方法

删除当前线程中保存的ThreadLocal对应的实体entry。

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

initialValue方法

该方法的第一次调用发生在当线程通过get方法访问线程的ThreadLocal值时。除非线程先调用了set方法,在这种情况下,initialValue才不会被这个线程调用。每个线程最多调用依次这个方法。该方法只返回一个null,如果想要线程变量有初始值需要通过子类继承ThreadLocal的方式去重写此方法,通常可以通过匿名内部类的方式实现。这个方法是protected修饰的,是为了让子类覆盖而设计的。

protect T initialValue(){
 return null;
}

应用

  • spring中的@Transcation注解
  • PageHelper

Page Helper使用ThreadLocal的原理:在你要使用分页查询的时候,先使用PageHelper.startPage这样的语句在当前线程上下文中设置一个ThreadLocal变量,再利用mybatis提供的拦截器(插件)实现一个com.github.pagehelper.PageInterceptor接口,这个分页拦截器拦截到后会从ThreadLocal中拿到分页的信息,如果有分页信息,这进行分页查询,最后再把ThreadLocal中的东西清除掉。

ThreadLocal的内存泄漏

当然,ThreadLocal也有弊端,就是容易造成内存泄漏

  • 内存泄漏:程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等验证后。内存泄漏的堆积会导致内存溢出。
  • 内存溢出:没有足够的内存供申请者提供

细心的朋友们可能发现了ThreadLocalMap这个类的K是一个WeakReference(弱引用)包括的泛型

static class Entry extends WeakReference<ThreadLocal>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

弱引用:垃圾回收器一旦发现了弱引用的对象,不管内存是否足够,都会回收它的内存。

因为线程的生命周期是很长的,你的项目不崩的话线程可能一直存在。

每个线程内都有个ThreadLocalMap,所以说ThreadLocalMap的生命周期也是同样长。

如果这个K是强引用,那么就无法被回收,最终就会造成内存泄漏。

当然现在使用ThreadLocal真正造成你内存泄漏的是没有及时的remove(),因为ThreadLocalMap中使用的key为ThreadLocal的弱引用,但是value是强引用,如果你不remove的话,value也是不会被GC清理的。

// ThreadLocal.remove
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
// ThreadLocalMap
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;
                }
            }
        }

这个其实也被优化了,如果你忘记调用remove方法,弱引用比强引用可以多一层保障,弱引用的ThreadLocal会被回收,对应的value会在下一次ThreadLocalMap调用get、set、remove方法的时候被清除,从而避免了内存泄漏。你说体贴不体贴。

ThreadLocalMap解决Hash冲突

ThreadLocalMap构造方法

0447f34ed7a10cb47a3f1b964dedcb68.png
image.png

构造函数创建一个长度为16的Entry数组,然后计算firstKey的索引,存储到table中,设置size和threshold。firstKey.threadLocalHashCode & (INITIAL_CAPACITY-1)用来计算索引,nextHashCode是Atomicinteger类型的,Atomicinteger类是提供原子操作的Integer类,通过线程安全的方式来加减,适合高并发使用。每次在当前值上加上一个HASH_INCREMENT值,这个值和斐波拉契数列有关,主要目的是为了让哈希码可以均匀的分布在2的n次方的数组里,从而尽量的避免冲突。

当size为2的幂次的时候,hashCode & (size - 1)相当于取模运算hashCode % size,位运算比取模更高效一些。为了使用这种取模运算, 所有size必须是2的幂次。这样一来,在保证索引不越界的情况下,减少冲突的次数。

ThreadLocalMap的set方法

d19e8aaabad7a713107a725a305f0b25.png

ThreadLocalMao使用了线性探测法来解决冲突。线性探测法探测下一个地址,找到空的地址则插入,若整个空间都没有空余地址,则产生溢出。例如:长度为8的数组中,当前key的hash值是6,6的位置已经被占用了,则hash值加一,寻找7的位置,7的位置也被占用了,回到0的位置。直到可以插入为止,可以将这个数组看成一个环形数组

(下篇内容详细给大家说下强软弱虚四种引用的区别和作用,这里埋个土哈。)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值