在《JUC入门的知识体系梳理》中聊过volatile保证了变量的可见性与有序性,但是不保证变量的原子性,所以在并发下,volatile并不能保证变量的安全性,今天我们来聊下ThreadLocal,从源码的角度分析,看一看它是如何保证线程之间的变量安全的。
ThreadLocal意为线程本地变量,用于解决多线程并发时访问共享变量的问题
在JDK1.2中引入了ThreadLocal类,来修饰共享变量,使每个线程都单独拥有一份共享变量,这样就可以做到线程之间对于共享变量的隔离问题
此时回想一下JMM内存模型,再想一下volatile的实现原理,是不是存在一定的相似性呢?
volatile 它的实现原理是将变量的值刷新到主内存中,每次读取都从主内存中读取,而不是从线程的工作内存中读取。而ThreadLocal是将主内存的变量copy一份到线程的工作内存中,每次操作的都是线程内工作内存中的变量,天然的隔离性,不需要加锁,也不需要刷新到主内存中,保证了变量的安全性,那么它究竟是如何实现的呢?待会源码分析中揭晓。
ThreadLocal的使用非常简单,只需要创建一个ThreadLocal对象,调用set()、get()、remove()方法即可。
javaThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("hello");
threadLocal.get();
threadLocal.remove();
ThreadLocal创建对象的方式有2种:
javaThreadLocal<String> threadLocal = new ThreadLocal<>();
javaThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "init");
ThreadLocal在我们平时的开发中使用的场景并不是太多,大多常用在组件开发上,许多源码中也包含了ThreadLocal,比如Mybatis中的SqlSession,Spring中的TransactionSynchronizationManager等等。
举个实际使用的栗子:
javapublic class Demo {
/**
* ThreadLocal 线程变量,每个线程都有一个副本,互不干扰
*/
public final static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
/**
* 设置值
* @param value 值
*/
public static void set(String value) {
THREAD_LOCAL.set(value);
}
/**
* 测试new Thread()与main线程的ThreadLocal变量值
*/
public static void main(String[] args) {
String main = "Hello World!";
new Thread(() -> {
THREAD_LOCAL.set(main);
System.out.println(Thread.currentThread().getName() + ":" + THREAD_LOCAL.get());
THREAD_LOCAL.remove();
}).start();
System.out.println("main:" + THREAD_LOCAL.get());
}
}
// 结果
main:null
Thread-0:Hello World!
ThreadLocal 是一个泛型类,泛型的类型即为存储的数据类型,从它的三个方法get()、set()、remove()开始分析
javapublic class ThreadLocal<T> {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
public T get() {
// 获取当前线程 - 联想到个线程都单独拥有一份共享变量,是不是通过当前线程来处理的呢?继续往下分析
Thread t = Thread.currentThread();
// 通过getMap()方法可以看出从当前线程中获取的ThreadLocalMap,此时猜测肯定这个对象存储了当前线程的共享变量
ThreadLocalMap map = getMap(t);
// map不为null,说明当前线程的ThreadLocalMap不为null
if (map != null) {
// 通过ThreadLocalMap的getEntry()方法获取当前ThreadLocal的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// Entry 存储了变量的值,此处返回
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 设置初始化的值,猜测与ThreadLocal.withInitial()方法有关
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果ThreadLocalMap不为null,说明当前线程的ThreadLocalMap不为null
if (map != null)
// 将当前ThreadLocal作为key,value作为value存储到ThreadLocalMap中
map.set(this, value);
else
// 如果ThreadLocalMap为null,说明当前线程的ThreadLocalMap为null,继续往下分析
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
// 创建一个ThreadLocalMap,绑定到当前线程
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
...
}
从上面的get/set源码中可以看出,当前的Thread中有一个ThreadLocalMap类型的成员变量threadLocals,在set的时候,如果有数据,用当前线程的ThreadLocalMap存储,如果没有数据,会指向一个新的new ThreadLocalMap()对象。
猜测此当前线程中的ThreadLocalMap变量就是实现线程隔离的关键,set的时候,实际的变量数据是添加到了当前的线程类中的ThreadLocalMap中存储,以下是Thread中的源码
javapublic class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
咦,在Thread源码中发现ThreadLocalMap居然是ThreadLocal的内部类,从之前的分析上,看名知意,这应该是个Map结构,那么ThreadLocalMap中存储结构是怎样呢?继续看源码
javastatic class ThreadLocalMap {
...
// Entry 继承了弱引用 WeakReference,key 即为 ThreadLocal,value 为存储的数据
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
// key 即为 ThreadLocal,value 为存储的数据
Entry(ThreadLocal<?> k, Object v) {
// 指定k变量也就是ThreadLocal为弱引用
super(k);
value = v;
}
...
}
// Entry数组,用于存储数据
private Entry[] table;
private Entry getEntry(ThreadLocal<?> key) {
// 计算key的hash值
int i = key.threadLocalHashCode & (table.length - 1);
// 通过hash计算的值定位取出数组中的Entry
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
...
}
以上源码可以观察到ThreadLocalMap类中存储的是一个Entry数组,Entry是ThreadLocal的内部类,Entry继承了弱引用WeakReference,key即为ThreadLocal,value为存储的数据
此时就联想到了一个问题,为什么一个线程可以绑定多个ThreadLocal呢?
Thread中存在一个ThreadLocalMap属性,ThreadLocalMap 本身就是基于 Entry[] 实现存储数据的,其中 ThreadLocal 作为key,ThreadLocal的值作为value,这种数据结构就是一个线程可以绑定多个ThreadLocal的原因
回想到set方法,直接是将值set到了当前Thread中的ThreadLocalMap中,线程与线程之间的变量都存储在自己当前线程中,保证了并发下变量的安全性,这也是隔离性实现的原理
此时回想一下,弱引用的特点是什么?如果ThreadLocal对象被回收了,会怎么样?
此处就会引发了一个致命问题,如果ThreadLocal被回收了,那么ThreadLocalMap中的Entry的key就为null,那么value就无法获取了,但此时Entry对象还是存在的,这样就导致了内存泄漏
所以ThreadLocalMap中的Entry需要被回收,那么ThreadLocalMap中的Entry是如何被回收的呢?继续看源码
java// ThreadLocal #remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
// ThreadLocalMap #remove()
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
// 调用 Reference #clear() 方法,清除弱引用
e.clear();
// 清除数组中的Entry
expungeStaleEntry(i);
return;
}
}
}
// ThreadLocalMap #expungeStaleEntry()
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 将Entry中的value值设置为null,将Entry对象设为null,方便GC回收
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
此时分析remove方法,从源码中可以看出,remove方法是通过当前线程获取ThreadLocalMap,然后调用ThreadLocalMap的remove方法
ThreadLocalMap #remove方法是通过key的hash值定位到数组中的Entry,调用 Reference #clear 清除引用
然后调用expungeStaleEntry方法,将Entry中的value值设置为null,将Entry对象设为null,方便GC回收
此时就完美解决了ThreadLocal内存泄漏的问题,这也是为什么我们在使用了ThreadLocal之后,必须调用remove方法的原因,属于强制性的操作,可千万别埋坑啊。
对于其他源码中的方法,就不一一分析了,有兴趣的小伙伴可以自行研究,比如hash计算的方法等,这里只是分析了ThreadLocal实现原理,以及存在的内存泄漏问题、解决方案的核心源码。
总结一下,开篇聊了 ThreadLocal 是什么以及使用,通过源码分析了它是怎么做到线程的隔离,本质就是每个Thread中都有一份 ThreadLocal.ThreadLocalMap 的属性,key为ThreadLocal,value是变量值,通过ThreadLocal #get() 方法,拿 ThreadLocal 当前的this对象从Thread中获取值。
接着从源码观察到 ThreadLocal 的内部类 ThreadLocalMap 中,它的实现子类Entry继承了弱引用WeakReference,根据弱引用的特点,分析其存在的内存泄漏的风险。又通过源码中remove方法了解到它是怎么解决内存泄漏的问题得。
在我们平时 ThreadLocal 的使用中,做到了知其然,知其所以然,才能更好的使用它。
本文作者:柳始恭
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!