ThreadLocal是什么?
ThreadLocal是线程本地变量,可以为多线程的并发问题提供一种解决方式,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal使用场景
①多个线程去获取一个共享变量时,要求获取的是这个变量的初始值的副本。②每个线程存储这个变量的副本,对这个变量副本的改变不去影响变量本身。③适用于多个线程依赖不同变量值完成操作的场景。
ThreadLocal类常用接口
void set(T value):设置当前线程的线程局部变量的值
T get():获取当前线程所对应的线程局部变量
void remove():删除当前线程局部变量的值,目的是为了减少内存的占用
T initialValue():该线程局部变量的初始值(默认值为null),该方法是一个protected的懒加载方法,线程第1次调用get()或set(T value)时才执行在,而且也是为了让子类覆盖而设计的。
使用案例:
Demo①:ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。
public class ThreadLocalDemo {
private static ThreadLocal<Index> index = new ThreadLocal(){
@Override
protected Object initialValue() {
return new Index();
}
};
private static class Index{
private int num;
public void incr(){
num++;
}
}
public static void main(String[] args) {
for(int i=0; i<5; i++){
new Thread(() ->{
Index local = index.get();
local.incr();
System.out.println(Thread.currentThread().getName() + " " + index.get().num);
}, "thread_" + i).start();
}
}
}
输出结果:
thread_1 1
thread_0 1
thread_3 1
thread_4 1
thread_2 1
Demo②:SimpleDateFormat是非线程安全(共享变量calendar访问没有做到线程安全),ThreadLocal 可以确保每个线程都可以得到单独的一个 SimpleDateFormat 的对象,那么自然也就不存在竞争问题了。
ThreadLocal工作原理
ThreadLocal内部维护的是一个类似Map的ThreadLocalMap数据结构,而每个Thread类,都有一个ThreadLocalMap成员变量。ThreadLocalMap将线程本地变量(ThreadLocal)作为key,线程变量的副本作为value,如图所示:
实现原理:ThreadLocal底层实现是ThreadLocalMap数据结构,当使用ThreadLocal维护变量时,ThreadLocalMap将线程本地变量(ThreadLocal)作为key,线程变量的副本作为value。每个线程去使用共享变量时,实际调用threadLocal的get()方法,获取当前线程对应的ThreadLocalMap,然后在根据key获取value值,就实现了线程安全的操作变量副本的值了。
ThreadLocal源码解析
想要熟悉和理解 Threadlocal 的源码的话,我建议先思考这么三个问题:
1、 Threadlocal 为什么能实现每个线程能有一个独立的变量副本;
2、每个线程的变量副本的储存位置在哪儿;
3、变量副本是如何从共享变量中复制出来的;
首先我们来看① initialValue( ) 方法:返回的是本地线程变量的初始值。返回值为空的原因很简单,这个方法就是用来重写
* @return the initial value for this thread-local
*/
protected T initialValue() {
return null;
}
②get()源码分析
2.1 get()源码入口
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//如果ThreadLocalMap已经被创建了,那么通过当前的threadLocal对象作为key,获取value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果ThreadLocalMap还没有被创建或者在ThreadLocalMap中查找不到此元素
return setInitialValue();
}
2.1.1 ThreadLocalMap没初始化,ThreadLocalMap为null时,会调用setInitialValue()方法:
private T setInitialValue() {
//initialValue方法一般会被重写,返回变量,不重写的话,直接返回null
T value = initialValue();
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//ThreadLocalMap已经被创建,那么直接设置初始值(即保存变量副本),初始值来自initialValue方法
map.set(this, value);
else
//创建ThreadLocalMap
createMap(t, value);
return value;
}
其中,initialValue()方法是由我们重写的,需要注意的是,返回值必须为new一个对象,而不是直接返回一个对象引用。因为如果多个线程都保存同一个引用的副本的话,那他们通过这个引用修改共享变量的值,是相互影响的。我们本来的目的便是为了获取共享变量的初始值副本,各个线程对副本的修改不影响变量本身。这就是能实现每个线程能有一个独立的变量副本原因。
2.1.2 看看createMap是如何创建threadLocalMap的:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//创建一个初始容量为16的Entry数组
table = new Entry[INITIAL_CAPACITY];
//通过threadLocal的threadLocalHashCode来定位在数组中的位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//保存在数组中
table[i] = new Entry(firstKey, firstValue);
//记录下已用的大小
size = 1;
//设置阈值为容量的2/3
setThreshold(INITIAL_CAPACITY);
}
2.2 初始化threadLocalMap之后,此线程再次调用get()方法,又做了哪些操作呢
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
//如果定位的元素的key与传入的key不相等,那么一直往后找
return getEntryAfterMiss(key, i, e);
}
可以看到是通过map.getEntry(this)去查找元素的,返回Entry。
2.3 如果map.getEntry(this)也找不到元素怎么办?回顾前面讲的get入口,先判断是否能根据当前线程获取threadLocalMap,第一种情况:threadLocalMap为空,那么直接新初始化创建一个。第二请情况:threadLocalMap有值,但是map.getEntry(this) 为空,这个时候就会在初始化方法里调用map.set(this, value)方法,将当前参数设置进Map。
private T setInitialValue() {
//initialValue方法一般会被重写,不重写的话,直接返回null
T value = initialValue();
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//ThreadLocalMap已经被创建,那么直接设置初始值(即保存变量副本),初始值来自initialValue方法
map.set(this, value);
else
//创建ThreadLocalMap
createMap(t, value);
return value;
}
private void set(ThreadLocal<?> key, Object value) {
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
//替代过期的元素,并清除后面的一些过期元素
replaceStaleEntry(key, value, i);
return;
}
}
//如果在table中确实找不到,那么新建一个
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
//如果没有元素被清除,且超过阈值,那么扩容并重新hash定位
rehash();
}
③set()源码分析
ThreadLocalMap的set方法和get方法很类似
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
底层源码工作原理总结
首先使用ThreadLocal<?>维护变量时,重写initialValue()方法,返回线程本地变量的初始值。然后每个线程去使用共享变量时,实际调用threadLocal的get()方法,获取当前线程对应的ThreadLocalMap。进入 get函数,先判断是否能根据当前线程获取threadLocalMap,第一种情况:threadLocalMap为空,那么直接新初始化创建一个。第二请情况:threadLocalMap有值,但是map.getEntry(this) 为空,这个时候就会在初始化方法里调用map.set(this, value)方法,将当前参数设置进Map。第三种情况:threadLocalMap有值,map.getEntry(this) 有值 根据key获取value直接返回,前两种情况返回初始化value,实现安全访问。
threadLocal的set()方法,作用:设置当前线程的线程局部变量的值,实现根据当前线程获取对应的threadLocalMap,获取到map直接将值set进去,获取map为空,直接创建threadLocalMap将值设置进去。
二、ThreadLocal的内存泄露分析
在分析ThreadLocal导致的内存泄露前,需要普及了解一下内存泄露、强引用与弱引用以及GC回收机制,这样才能更好的分析为什么ThreadLocal会导致内存泄露呢?更重要的是知道该如何避免这样情况发生,增强系统的健壮性。
内存泄露
内存泄露是程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光
通俗的讲:内存一直被对象或者变量占用,导致内存不能被回收
强引用,使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用,JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。
GC回收机制-如何找到需要回收的对象
JVM如何找到需要回收的对象,方式有两种:
-
引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收,
-
可达性分析法:以根集对象为起始点进行搜索,如果有对象不可达的话,即没有引用指向的对象,就是垃圾对象,jvm垃圾回收的时候将会对垃圾对象进行回收。(根集一般包括Java栈中引用的对象,方法区常量池中引用的对象,堆中引用的对象等)
不可达定义:在java中,对象是通过引用使用的,如果在没有引用指向该对象的情况下,那么将无从处理或调用该对象,这样的对象为不可达。
ThreadLocal的内存泄露分析
由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,线程一直没有完成,如果都没有手动删除对应key,都会导致内存泄漏。源码开发也考虑到了这一点
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
但这次发现:Entry是继承的WeakReference,并且只绑定了ThreadLocal(WeakReference表示弱引用对象)。
使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,但是value就不同了,它是强引用,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。
防止内存泄漏最直接的方法就是使用完变量后调用ThreadLocal的remove(),remove()实际是将对象的引用置为null,这样一来没有引用指向这个对象,该对象就会被JVM判定为垃圾并在GC时回收掉。
三、ThreadLocal在set()时发生哈希冲突怎么办吗
数据是以键值对方式存进Entry数组的,在存入时会根据键(ThreadLocal)的哈希值,找到它所存放的位置,但这样有时会出现哈希冲突,至于如何应对哈希冲突
-
如果该位置是空的,那么直接将键值对存储;
-
若不为空且两个键相同,那么新值换旧值;
-
若不为空且两键不相同,那只能找下个空位置了。
文章参考:
当面试官问到ThreadLocal时,我们应具备怎样的谈资?
【推荐】:并发容器之ThreadLocal详解_threadlocal容器大小_ThinkWon的博客-CSDN博客
【推荐】:ThreadLocal内存泄漏分析与解决方案_ThinkWon的博客-CSDN博客
比较好图文https://www.cnblogs.com/killbug/archive/2021/11/19/15575701.html