你生来一无所有,何惧从头再来 ---勉励自己
- ThreadLocal是如何实现线程隔离的?
- 为什么ThreadLocal会造成内存泄露? 如何解决?
本篇文章主要是针对这两个问题进行剖析,确保每个小伙伴都能读懂,深刻理解,篇幅较长,请耐心阅读。大家如果还有什么难点,欢迎在评论区留言,小编将和大家一起学习。
- 定义:ThreadLocal提供线程局部变量,通过为每个线程提供不同的局部变量副本,实现线程之间的数据隔离。每个线程都可以访问自己的变量副本。用于解决多线程并发问题。
- 原理图
- 基本使用
/**
* ThreadLocal基本使用
*/
//定义一个ThreadLocal局部变量
val threadLocal = ThreadLocal<Int>()
for (i in 0 until 3){
thread {
threadLocal.set(i + 1)
Log.i(TAG, "输出: "+ (Thread.currentThread().name+":"+threadLocal.get()))
}
}
//输出
2022-09-05 10:47:30.137 9709-14942/? I/FLOW_DEMO: 输出: Thread-5:1
2022-09-05 10:47:30.137 9709-14944/? I/FLOW_DEMO: 输出: Thread-7:3
2022-09-05 10:47:30.138 9709-14943/? I/FLOW_DEMO: 输出: Thread-6:2
我们通过使用三个线程来给ThreadLocal赋值,进行累加操作,不同的线程输出的值都是累加后的。从输出结果可以看出来,不同线程之间的操作不会彼此影响结果值。从而保证了线程并发访问的正确性。
- TheadLocal原理
我们从源码进行分析,看一下ThreadLocal是如何进行线程之间的数据隔离的?
首先我们来看一下set方法:
/**
* 将此线程局部变量的当前线程副本设置为指定值。大多数子类不需要重写此方法,
* 仅依靠initialValue方法来设置线程局部变量的值。
* 参数:
* value – 要存储在此线程本地的当前线程副本中的值
*/
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//通过当前线程获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
//判断当前ThreadLocal是否为null
if (map != null)
map.set(this, value); //将当前ThreadLocal作为key,局部变量值value,set给map
else
createMap(t, value); //若map为null,则创建新的ThreadLocalMap并赋值
}
我们来看一下getMap这个方法。
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// Thread中的ThreadLocal.ThreadLocalMap threadLocals = null;
从上述源码中,可以看出ThreadLocal先获取当前线程,通过当前线程的getMap拿到线程的成员变量threadLocals,threadLocals的类型是ThreadLocalMap,然后以当前ThreadLocal作为key,局部变量value将数据保存到ThreadLocalMap中。那么ThreadLocalMap是如果保存数据的呢,接下来我们一起往下看。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
........
}
从上述源码中可以看出,ThreaLocalMap类中有个静态内部类Entry,Entry继承WeakReference弱引用,并且将ThreadLocal作为key,Object作为value。,除此之外,ThreadLocalMap中还有一个成员变量table,它的类似想Entry数组,那这个table的作用是干啥的呢,我们接着往下看。
我们看过ThreadLocal的set方法,知道了线程是如何将数据存储到ThreadLocal中的,接下来我们来看一下线程是如何从ThreadLocal中取出数据的。
/**
*返回此线程局部变量的当前线程副本中的值。
*如果变量没有当前线程的值,则首先将其初始化为调用initialValue方法返回的值。
*返回:此线程本地的当前线程的值
*/
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取线程的局部变量ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//取出entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果map为null,则初始化结果
return setInitialValue();
}
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;
}
从上述源码中我们可以看出,调用了ThreadLocalMap的getEntry方法,将ThreadLocal作为参数传进去,获取Entry,然后返回Entry的value。我们继续往下看。
private Entry getEntry(ThreadLocal<?> key) {
// 位运算获取下标
int i = key.threadLocalHashCode & (table.length - 1);
//通过下标获取对应的Entry
Entry e = table[i];
// Android-changed: Use refersTo()
if (e != null && e.refersTo(key)) //Entry不为bull且key未被回收
return e; //返回Entry
else
return getEntryAfterMiss(key, i, e); //返回null
}
我们再介绍Entry这个静态内部类的时候提到过Entry[ ] table这个成员变量。用来存放在ThreadLocal插入的变量。我们来看一下ThreadLocal与Entry的关系。
目前为止我们可以得出以下几点。
- 每个线程中都有一个ThreadLocal.ThreadLocalMap 成员变量。
- ThreadLocalMap中通过数组保存Entry,每一个Entry中都有一个ThreadLocal作为Key,Object为Value用来保存数据变量。
- 不同线程之间获取的ThreadLocalMap不同,从而访问的数据变量副本不同。实现了线程之间的数据隔离。
我们接着往下看ThreadLocal是如果解决内存泄漏的。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table; //保存entry的数组
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); //hash算法
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get(); //遍历数组
if (k == key) { //如果key相同 直接覆盖value
e.value = value;
return;
}
if (k == null) { //如果key为null ,释放value
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 如果key为bull 则将value赋值为null
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
当我们调用threadLocal的set,get方法时,会判断当前的key是否为null,将Entry中的value赋值为null,但是这个释放value还有其他条件限制,并不是一定会发生,当系统内存不足时,由于Entry中的key继承软引用,回被垃圾回收器回收调,这时,Entry中的key为null,无法被线程访问,但是value仍然占用一定的内存空间,虽然在调用set,get方法时有可能进行系统回收,仍然无法回收无用所有内存。无法被访问的vlaue就会导致内存泄漏,怎么解决内存泄漏呢,最好的方法就是当我们使用完变量副本后及时调用remove方法,手动进行垃圾回收。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* Remove the entry for key.
*/
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) {
e.clear();//清除value
expungeStaleEntry(i);
return;
}
}
}
当线程发生内存泄漏时,线程与内部的ThreadLocalMap之间存在着强引用,导致ThreadLocalMap无法被释放,这时由于ThreadLocalMap中的Entry的key为弱引用,ThreadLocal容易被回收,导致key为null,当调用remove方法时,会清除key为null对应的value。所以为了避免内存泄漏的出现,我们在使用完ThreadLocal的set方法后,及时调用remove方法进行内存释放。避免出现内存泄漏。