Hi,大家好,我是抢老婆酸奶的小肥仔。
在很多的地方,我们都能看到ThreadLocal的身影,也会用到它,但是我们真的就了解它吗?
今天我们来叨叨这个我们既熟悉又陌生的小伙伴,废话不多说开整。
1、啥是ThreadLocal
一言以蔽之:线程各行其是,即线程间的隔离性。
在多线程时,访问同一个共享变量,可能会存在线程安全问题,为了线程安全,我们会为这个变量加锁,以达到同一时间只能有一个线程进行访问,其他线程只能等待。这样就会导致程序复杂性,开发人员也必须对锁的使用特别熟练,否则可能产生死锁。
而ThreadLocal可以将创建的变量作为当前线程私有变量。
我们通过代码来看看ThreadLocal是否是只操作线程本身的私有变量的。
java
/**
* @author: jiangjs
* @description: 使用ThreadLocal
* @date: 2023/7/28 14:55
**/
public class UseThreadLocal {
private static final ThreadLocal<String> tl = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
tl.set("线程A名称【" + Thread.currentThread().getName() + "】");
System.out.println("获取当前线程A的名称:" + tl.get());
tl.remove();
System.out.println("验证是否删除当前线程A的名称:" + tl.get());
}).start();
new Thread(() -> {
tl.set("线程B名称【" + Thread.currentThread().getName()+"】");
System.out.println("获取当前线程B的名称:" + tl.get());
System.out.println("验证是否删除当前线程B的名称:" + tl.get());
}).start();
}
}
执行结果:
上述代码中,我们通过创建两个线程,分别在定义的ThreadLocal中添加了各自的线程名称,但是在线程A中调用了ThreadLocal提供的remove方法进行了删除。我们发现线程A的线程名称被删除了,而线程B并未受影响,因此,ThreadLocal做到了隔离性,线程间的变量互不干扰。
2、ThreadLoal原理
我们翻开ThreadLocal的源码,会发现其内部有一个ThreadLocalMap的内部静态类。根据这个静态内部类上的注释我们可以了解,这是一个定制的散列映射,只适合于维护线程本地值。
在ThreadLocal简介里面我们用到了三个方法:set(T value) ,get(),remove()三个方法,我们来看看他们的源码,通过他们的源码来了解ThreadLocal的原理。
2.1 set(T value)
set(T value)方法源码:
java
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//根据当前线程,获取ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null)
//ThreadLocalMap对象不为空,则直接赋值
map.set(this, value);
else
//ThreadLocalMap对象为空,则创建对象,赋值
createMap(t, value);
}
上述源码中我们知道,ThreadLocalMap则是以当前线程作为key,而传递的value则是ThreadLocalMap保存的值。
在上述源码中,我们用到两个内部方法:getMap(t), createMap(t, value),我们也顺便看看这两个方法的源码。
getMap(t)源码:
java
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
源码返回的是当前线程中的threadLocals
变量:ThreadLocal.ThreadLocalMap threadLocals = null;
因此,当我们第一次调用时,返回的就是null。
createMap(t, value)源码:
java
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
在createMap()中,则是直接调用ThreadLocalMap的构造方法创建对象,并赋值给线程的threadLocals
变量。
上述的源码比较好理解,也就是获取当前线程,通过当前线程获取自身的成员变量threadLocals
,而threadLocals
其实就是TheadLocal的ThreadLocalMap对象,如果对象不为null,则直接调用set方法赋值,否则创建ThreadLocalMap对象后并实例化当前线程的threadLocals
成员变量。
2.2 get()
get()方法源码:
java
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
上述源码,获取当前线程的ThreadLocalMap对象即threadLocals成员变量,如果为空,则调用setInitialValue()方法初始化threadLocals成员变量的值,否则直接返回绑定的本地变量。
java
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
initialValue()
:返回的是一个null值。因此如果get()获取不到值时,则直接返回的就是null,setInitialValue()的方法也是调用createMap(t, value)创建ThreadLocalMap,只不过传递的值为null。
2.3 remove()
remove()方法源码:
java
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
上述源码就比较简单,获取当前线程的ThreadLocalMap对象,如果不为空,则删除value值。
2.4 关于ThreadLocalMap
在上述的ThreadLocal的三个方法中,其本质都是操作ThreadLocalMap,我们通过createMap中初始化ThreadLocalMap时调用的构造方法来看看其本质:
java
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
INITIAL_CAPACITY
:定义Entry数组的长度,值:16。
threadLocalHashCode
:获取当前线程的hash值。
setThreshold(INITIAL_CAPACITY)
:计算扩容因子。
Entry源码:
java
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作为key是一个弱引用,而弱引用在JVM的每一次GC时都会被回收。
3、ThreadLocal一些特性
3.1 变量不具有传递性
简单来说:同一个ThreadLocal在父线程中设置值后,子线程也是无法获取的。
其实不难理解,毕竟ThreadLocal主打的就是一个变量的隔离性。
我们也用代码来验证一下。
java
public class UseThreadLocal {
private static final ThreadLocal<String> tl = new ThreadLocal<>();
public static void main(String[] args) {
tl.set("获取的值");
new Thread(() -> {
System.out.println("获取主线线程的值:" + tl.get());
}).start();
}
}
执行结果:
如果想要子线程获取父线程的值则可以使用:InheritableThreadLocal。
3.2 关于OOM
3.2.1 原因
在ThreadLocalMap中,其定义的Entity是继承了WeakReference,并指定ThreadLocal<?> k做为key,且是一个弱引用,当ThreadLocal作为key在没有外部强引用时,就会被GC回收,而value作为强引用不会被回收,就会造成存在key为null,value不为null的Entity,此时若线程一直不结束,或线程作为线程池中的一员,即使结束也不会被销毁,这样久而久之就可能造成OOM。
3.2.2 如何避免OOM呢?
1、使用完ThreadLocal后,调用remove()方法清除数据
2、将ThreadLocal变量使用private static,使其一直存在强引用,同时能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
4、总结
ThreadLocal采用了多线程隔离机制,在多线程下线程将共享变量复制一个副本,线程各自只能使用线程本身的副本变量,为提供了访问变量的安全性。同时采用了空间换时间的思想,通过ThreadLocalMap来管理线程成员变量信息。
好了,今天就跟大家叨叨到这,谢谢大家。