欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

ThreadLocal原理分析及内存问题

时间:2023-06-30

ThreadLocal这个类在并发编程中越来越多的被使用到,为什么呢?怎么用呢?原理是什么呢?,如果你对这些问题也存在疑惑,那么不妨看完这篇文章,应该或多或少会帮助到你

1 什么是ThreadLocal

ThreadLocal是存在于java.lang包下的一个类,我们熟知的java.util.concurrent包下面的AtomicInteger,spring框架中的SimpleInstantiationStrategy 中就存在对该类的使用。

2 ThreadLocal能干什么

一句话描述ThreadLocal:ThreadLocal实现了线程之间内存空间的隔离其实说说白了就是多个线程各自使用各自的空间对数据进行操作,防止因并发(多线程)导致出现数据安全问题,避免对共享资源的锁定举个例子:有一个需求,现在有1000个线程同时处理数据,求一分钟内这100个线程共计处理了多少条数据。你会怎么做呢?方案1:定义一个计数变量,让1000个线程共同操作这个变量进行计算,同时为了避免多线程导致的安全问题,每个线程在对这个共享变量操作的同时进行加锁。问题:这样会导致在一个线程在进行共享变量操作的时候其余999个线程都在等待方案2:为每一个线程创建一个单独的变量,每个线程各自操作自己的变量,最终把所有的变量进行加和统计总的数量。问题:1000个线程创建一千个变量,太反人类了

通过上述的问题我们知道了我们的痛点就是怎么在线程安全的情况下简单的对共享变量进行操作。这样就引出了我们的主角ThreadLocal也就是方案3:每个线程操作 “Threadlocal中的共享变量” 为什么要用引号引起来呢?这是个坑,具体情况将会在原理中说明。

3 ThreadLocal原理是什么

我们对ThreadLocal原理的讲解方式是什么样的呢?
带着结论来读源码

结论:每个Thread(线程)内部有一个map存放本Thread(线程)专属数据其中map的key是Threadlocal对象而value是具体要保存的数据

我们平常用ThreadLocal大致就是

void test() {//创建ThreadLocal 存放每个线程自己的Integer ThreadLocal tl = new ThreadLocal<>();//thread1线程对自己的Integer进行自增 Thread thread1 = new Thread(()->{ Integer integer = tl.get(); tl.set(integer+1); }); //thread2线程对自己的Integer进行自增 Thread thread2 = new Thread(()->{ Integer integer = tl.get(); tl.set(integer+1); }); thread1.start(); thread2.start(); }

上面的例子中thread1和thread2中的自增Integer互不影响
上面的例子中直面理解就是ThreadLocal会提供一个共享的map而每个thread都会向这个map中存放自己线程的“私有数据”,那么理所应当的是map中的key是threadID value为私有数据,但是上面理解是片面的不正确的,下面我们通过查看原码来说明
在Thread类中存在这样一个属性threadLocals

ThreadLocal.ThreadLocalMap threadLocals = null;

而我们可以看出来threadlocals的类型为ThreadLocal.ThreadLocalMap也就是ThreadLocal下的内部类ThreadLocalMap
知道这个之后我们开始从set方法入手来查看整个存储过程

public void set(T value) {//首先thread1线程调用set方法进来//1.获取当前线程-->其实就是Thread1 Thread t = Thread.currentThread(); //2.通过getMap方法获得ThreadLocalMap而这个getMap方法是从什么地方得到的ThreadLocalMap呢? ThreadLocalMap map = getMap(t); //为了方便阅读直接把getMap方法贴到下方 ThreadLocalMap getMap(Thread t) { //3.可以看出是从Thread中找到ThreadLocalMap 而这个t就是thread1 return t.threadLocals; } //4.判断当前map是否被存放过了私有值 if (map != null) //5.1继续向ThreadLocalMap中存放值 map.set(this, value); else //5.2创建一个ThreadLocalMap createMap(t, value); }

我们首先展开5.2的createMap方法看看他是怎么创建一个ThreadLocalMap的

void createMap(Thread t, T firstValue) {//5.2.1 开始创建ThreadLocalMap给thread1的threadLocals 其中的this是当前的ThreadLocal 也就是上面所描述的tl t.threadLocals = new ThreadLocalMap(this, firstValue); }

下面我们进入ThreadLocalMap的构造方法

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//小伙伴们看到这里估计又会出现疑问这个Entry是什么呢//其实这个Entry就是一个普通对象里面有两个属性//一个是key泛型为ThreadLocal//另一个是object//5.2.2 下面就是创建了一个初始大小为16的Entry数组 table = new Entry[INITIAL_CAPACITY]; //定位当前tl应该落在数组的位置 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //存放数据 table[i] = new Entry(firstKey, firstValue); size = 1; //设置数组负载因子为16的2/3 setThreshold(INITIAL_CAPACITY); }

读完上面源码是不是就对整个Threadlocal的结构有了一个了解,所以解释了为什么说 “Threadlocal中的共享变量” 是一个坑

4 ThreadLocal内存问题

那有的小伙伴就要问了那这个key和外面那个tl都使用同一个对象不会出现问题吗?
是的会出现问题内存上的问题,这样的操作会导致内存泄漏
下面我们想想问题是如何发生的呢?
假设我们出现了线程重用问题(线程池),那么之前业务中使用到了ThreadLocal,但是任务执行完成之后该ThreadLocal设置为null 不想使用了,但是别忘了线程并没有销毁,里面还是又这个ThreadLocalMap里面还是有这个ThreadLocal的引用,导致GC回收发现这个堆中的ThreadLocal还有引用所以判断为这不是垃圾,导致一直存在内存中无法释放,这样问题该怎么办呢?
当然这样的问题人家设计者也想到了啊,所以引出我们下一个概念“软引用”
我们仔细看Entry这个类

static class Entry extends WeakReference> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }

可以看到该类继承自weakReference这个软引用的父类,所以该类中的key就被置为软引用,指向了ThreadLocal堆内对象,在GC的时候直接对其进行GC回收(前提是没有强引用)这样就不会出现ThreadLocal无法回收的情况了。

那有的眼尖的小伙伴又会发现这样的话不就会导致Entry中key为null了吗?
对 没错
这就引出ThreadLocal卡法中我们要注意的一个点remove方法,在执行完操作后不要忘记调用这个方法帮助GC就会避免出现key为nullvalue还有值得问题了
虽然我们在使用ThreadLocal set和get方法得时候会自动进行remove但是会因为不确定调用set和get导致上述问题得出现,所以为了安全,小伙伴们还是手动remove吧!

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。