并发编程之ThreadLocal汇总小结

在多线程开发过程中,我们往往不能忽略并发安全的问题。处理线程安全的问题有很多,例如让变量不可变使用线程安全的集合使用JUC包下的相关类(如Atomic包)等等,这里的话我们简单总结一下ThreadLocal这个常用的类。

解决的问题

首先来看一些很常见的场景,当我们开发一个Web网站。不同的用户进来能显示不同的信息,并且各自的用户信息都能存在各自的Session中

第二,如果我们想在某个方法的下游获取一些信息,但是又不想通过传参的方式将数据带下去

针对以上两种情况,我们可以使用ThreadLocal来解决。

引入ThreadLocal

ThreadLocal音译是线程本地,其实本质是线程独享

也就是说,每个线程都各自拥有,某一个线程的线程并不会影响另外一个线程的值。

我们来看一段示例代码:

public class TestThreadLocal {
    static  ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
    public static void main(String[] args) {
        // 第一次赋值
        stringThreadLocal.set("我是主线程");
        // 开启一个子线程修改值
        new Thread(()->{
            // 第二次赋值
            stringThreadLocal.set("我是子线程");
            System.out.println(Thread.currentThread().getName()+" : "+ stringThreadLocal.get());

        }).start();
        try {
            // 休眠一秒
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+" : "+stringThreadLocal.get());
		// 第三次赋值
        stringThreadLocal.set("我是修改后的");

        System.out.println(Thread.currentThread().getName()+" : "+stringThreadLocal.get());
    }
}

来看结果:

在这里插入图片描述

为了确保子线程能成功对ThreadLocal变量进行操作,特地休眠了一秒让子线程执行完。(也可以用join来插队)

我们可以看出,子线程对ThreadLocal变量的操作丝毫不会影响主线程,主线程是怎么赋值的那么在读取的时候还是什么。

并且,如果我们在主线程修改的话,再次获取到的值就是主线程修改的值

那么接下来,我们就要从其数据结构来研究ThreadLocal是如何做到对线程的控制的。

数据结构

首先,我们通过一张图来看ThreadLocal和线程的关系

从下图我们可以看到,每一个线程维护着一个ThreadLocalMap,然后ThreadLocalMap中维护着多组以ThreadLocal为键的节点
在这里插入图片描述

下面,我们从源码层面来看结构:

既然是线程独有的,可以从Thread类中找到这个threadLocals变量,发现他的类型就是ThreadLocalMap,不过是由ThreadLocal类来维护的
在这里插入图片描述

我们去ThreadLocal中找到这个内部类:

发现这个类内部维护了一个Entry节点tables数组,看到这里几乎可以确定,他跟HashMap结构类似了。

但是,HashMap解决冲突时拉链式法,凡是Hash冲突并且值不相等的就以链表接下去,形成一条链。而ThreadLocalMap则是线性探测法,凡是Hash冲突,就让后者往下找数组空位插入即可。

并且,这个Entry类继承WeakReference这个弱应用,通过构造方法看到他的健是一个弱引用(这里要稍微了解一下JVM的强软弱虚引用在什么时候会被垃圾回收)。也就是这一步导致内存泄漏的。
在这里插入图片描述

到这里,基本也可以看出了。

每个线程各自维护一个Map,这个Map内部数组存放Entry节点,而Entry以ThreadLocal为键,Object对象为值。

接下来,通过了解他的相关方法,可以更加明白他的机制。

相关常用方法

以下是一些常用的方法

getMap

我们先来看getMap这个方法,通过传入一个线程,返回这个线程对应的threadlocalmap
在这里插入图片描述

为什么要先讲这个方法呢,因为这个方法是后面所有方法的爹(详解看后文)。

set方法

接下来,我们来到set方法。他先获取当前线程,在通过getMap方法来获取到线程对应的map进行插入操作

map存在就正常插入,不存在则创建。

后面具体的方法跟HashMap大同小异,除了Hash冲突的解决。这里就不过多解释了。
在这里插入图片描述

get方法

也是先通过getMap方法传入当前线程获取map

如果map不为空,则通过调用get方法的threalocal来获取存在当前线程map的值

如果为空,则通过setInitialValue来初始化这个map
在这里插入图片描述

在这里插入图片描述

initialValue方法

initialValue默认返回空值,可以进行重写

如果当前threadlocal没有进行set操作就get就会触发initialValue;如果threadlocal先进行set操作在get,那么这个initialValue不会执行,除非清空了所有元素重来。
在这里插入图片描述

remove方法

移除该thredlocal在此线程map的节点
在这里插入图片描述

空指针和内存泄漏

内存泄漏

简单介绍完ThreadLocal常用的方法后,下面说一下他的问题

前面也有提到过,ThreadLocalMap中的节点是弱引用,也就意味着,下一次GC的时候就把这个threadlocal键给回收了此时,因为线程还存在,所以map还存在,map还存在Entry节点就存在,那么键的引用丢失,我们找不到这个值,但是值是强引用,所以存在内存泄漏在这里插入图片描述
下面通过GC前后的对象引用状态来看:

在这里插入图片描述

在这里插入图片描述

GC后键被回收了,而value只能根据键来获取,所以最后带来了内存泄漏

空指针问题

这个的话一般不会带来问题,只不过要注意的是如果要对threadlocal进行封装处理,要注意自己封装get的返回值

示例代码:

public static long get(){
        return longThreadLocal.get();
    }

    public static void set(long l){
         longThreadLocal.set(l);
    }

public static void main(String[] args) {
        //set(1L);
        System.out.println(get());
    }

执行结果:
在这里插入图片描述

我们可以看到,出现了空指针异常

因为本来用ThreadLocal传入的泛型就是对象类型或基本类型的包装类,正常获取的话要么是NULL,要么是set进去的值。这里进行了一层封装,返回基本类型,但是我们把set方法注释掉了,就造成了装箱拆箱的时候报出空指针异常

应用

ThreadLocal在Spring的上下文中有内用。根据前面存入的信息,可以在后层service服务层通过上下文来获取存入的信息。

小结

最后,对ThreadLocal简单小结。
首先,结构类似Map,处理Hash冲突使用线性探测法
其次,能保证数据的安全,因为线程独享,互不干扰
问题,带来内存泄漏和空指针问题,内存泄漏是因为键的弱引用;空指针则是基本类型的装箱拆箱
最后,如果不需要用ThreadLocal尽量不用,因为也会带来一些不必要的性能损耗。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值