ThreadLocal这个类在并发编程中越来越多的被使用到,为什么呢?怎么用呢?原理是什么呢?,如果你对这些问题也存在疑惑,那么不妨看完这篇文章,应该或多或少会帮助到你😀
1 什么是ThreadLocal
ThreadLocal是存在于java.lang包下的一个类,我们熟知的java.util.concurrent包下面的AtomicInteger,spring框架中的AOP中就存在对该类的使用。
2 ThreadLocal能干什么
一句话描述ThreadLocal:ThreadLocal实现了线程之间内存空间的隔离
其实ThreadLocal说白了就是多个线程各自使用各自的空间对数据进行操作,防止因并发(多线程)导致出现数据安全问题,避免对共享资源的锁定
举个例子:现有一个需求,让1000个线程同时处理数据,求一分钟内这1000个线程共计处理了多少条数据。
你会怎么做呢?
方案1:定义一个计数变量,让1000个线程共同操作这个变量进行计算,同时为了避免多线程导致的安全问题,每个线程在对这个共享变量操作的同时进行加锁。
问题:这样会导致在一个线程在进行共享变量操作的时候其余999个线程都在等待 这样显然不行 pass!!!
方案2:为每一个线程创建一个单独的变量,每个线程各自操作自己的变量,最终把所有的变量进行加和统计总的数量。
问题:1000个线程创建一千个变量,太反人类了 也不行 pass!!!
通过上述的问题我们知道了改需求而的痛点就是怎么在线程安全的情况下简单的对共享变量进行操作。这样就引出了我们的主角ThreadLocal也就是方案3:每个线程操作 “存在Threadlocal中的共享变量” 为什么要用引号引起来呢?这是个坑,具体情况将会在下面的原理中说明。
3 ThreadLocal原理是什么
我们对ThreadLocal原理的讲解方式是什么样的呢?
带着结论来读源码
结论:每个Thread(线程)内部有一个map存放本Thread(线程)专属数据其中map的key是Threadlocal对象而value是具体要保存的数据
我们平常用ThreadLocal大致就是
void test() {
//创建ThreadLocal 存放每个线程自己的Integer
ThreadLocal<Integer> 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 values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
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内存问题
那有的小伙伴就要问了
这个Enty数组中的key(threadLocal)是一个对象 同时还会出现在其他thread中的Enty数组中,这样会不会出现问题呢?其他位置也会使用这个对象,如果对这个对象进行操作会不会出现问题呢?
没错 是会出现问题 而且是内存上的问题,这样的操作会导致内存泄漏
下面我们想想问题是如何发生的呢?
假设我们出现了线程重用问题(线程池),那么之前业务中使用到了ThreadLocal,但是任务执行完成之后该ThreadLocal设置为null 不想使用了,但是别忘了线程并没有销毁,线程里面还是这个ThreadLocalMap 同时ThreadlocalMap里面还是有这个ThreadLocal的引用,导致GC回收发现这个堆中的ThreadLocal还有引用所以判断为这不是垃圾,导致一直存在内存中无法释放,这样问题该怎么办呢?
当然这样的问题人家设计者也想到了啊,所以引出我们下一个概念“弱引用”
我们仔细看Entry这个类
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看到该类继承自weakReference这个弱引用的父类,所以Entry中的key也就是对ThreadLocal为弱引用,在GC的时候判断如果改ThreadLocal已经没有其他强引用了而Entry的key被标为弱引用,就会被直接回收,这样就不会出现ThreadLocal无法回收的情况了。
那有的眼尖的小伙伴又会发现这样的话不就会导致Entry中key为null了吗?
对 没错
接下来就引出ThreadLocal中我们要注意的一个点remove方法,在执行完操作后不要忘记调用这个方法帮助GC就会避免出现key为null value还有值的现象了
虽然我们在使用ThreadLocal的set和get方法时候会自动进行remove但是会因为 不确定调用set和get的时间导致上述问题得复现,所以为了安全,小伙伴们还是手动remove吧!