ThreadLocal 解析 [转载]

本文转载自:


ThreadLocal是什么?

ThreadLocal 是一个线程内部数据存储类,通过它可以在指定的线程中存储数据。存储后,只能在指定的线程中获取到存储的数据,对其他线程来说无法获取到数据。

ThreadLocal的使用场景

日常使用场景不多,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,可以考虑使用ThreadLocalAndroid源码的LopperActivityThread以及AMS中都用到了ThreadLocal

ThreadLocal的使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ThreadLocalActivity extends AppCompatActivity {
private ThreadLocal<String> name = new ThreadLocal<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_thread_local);
name.set("小明");
Log.d("ThreadLocalActivity", "Thread:" + Thread.currentThread().getName() + " name:" + name.get());
new Thread("thread1") {
@Override
public void run() {
name.set("小红");
Log.d("ThreadLocalActivity", "Thread:" + Thread.currentThread().getName() + " name:" + name.get());
}
}.start();
new Thread("thread2") {
@Override
public void run() {
Log.d("ThreadLocalActivity", "Thread:" + Thread.currentThread().getName() + " name:" + name.get());
}
}.start();
}
}

运行结果:

1
2
3
D/ThreadLocalActivity: Thread:main name:小明  
D/ThreadLocalActivity: Thread:thread1 name:小红
D/ThreadLocalActivity: Thread:thread2 name:null

可以看到虽然访问的是同一个ThreadLocal对象,但是获取到的值却是不一样的。

ThreadLocal的源码阅读

那么为什么会造成这样的结果呢?这就需要去看看ThreadLocal的源码实现,这里的源码版本为API28。主要看它的getset方法。

1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

set方法中首先获取了当前线程对象,然后通过getMap方法传入当前线程t获取到一个ThreadLocalMap,接下来判断这个map是否为空,不为空就直接将当前ThreadLocal作为keyset方法中传入要保存的值最为value,存放到map中;如果map为空就调用createMap方法创建一个map并同样将当前ThreadLocal和要保存的值作为keyvalue加入到map中。
接下先看getMap方法:

1
2
3
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

getMap方法比较简单,就是返回从传入的当前线程对象的成员变量threadLocals。 接着是createMap方法:

1
2
3
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

createMap方法也很简单就是new了一个ThreadLocalMap并赋给当前线程对象t中的threadLocals。 原来这个Map是存放在Thread类中的。于是进入Thread类中查看。
Thread.java第188-190行:

1
2
3
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

根据这里的注释可以得知,每个线程Thread中都有一个ThreadLocalMap类型的threadLocals成员变量来保存数据,通过ThreadLocal类来进行维护。这样看来我们每次在不同线程调用ThreadLocalset方法set的数据是存在不同线程的ThreadLocalMap中的,就像注释说的ThreadLocal只是起了个维护ThreadLocalMap的功能。想到是get方法同样也是到不同线程的ThreadLocalMap去取数据。
get方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
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();
}

果然,get方法中同样是先获取当前线程对象,然后在拿着这个对象t去获取到t中的ThreadLocalMap,只要map不等于null就调用map.getEntry(this)方法来获取数据,因为ThreadLocalMap里使用一个内部类Entry来存储数据的,所以调用getEntry(this)方法,传入的key是当前的ThreadLocal。这样获取到Entry类型数据e,只要e不为null,返回e.value即先前存储的数据。如果获取到的mapnull又或者根据key获取Entrynull,就调用setInitialValue方法初始化一个value返回。
setInitialValueinitialValue方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

protected T initialValue() {
return null;
}

setInitialValue方法中首先调用initialValue方法初始化了一个空value,之后的操作和set方法相同,将这个空的value加入到当前线程的ThreadLocalMap中去,ThreadLocalMap为空就创建个Map,最后返回这个空值。
至此,ThreadLocalgetset方法就都看过了,也理解了ThreadLocal可以在多个线程中操作而互不干扰的原因。但是ThreadLocal还有一个要注意的地方就是ThreadLocal使用不当会造成内存泄漏。

ThreadLocal内存泄漏的原因

内存泄漏的根本原因是当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,导致本该被回收的对象不能被回收而停留在堆内存中。那么ThreadLocal中是在哪里发生的呢?这就要看到ThreadLocalMap中存储数据的内部类Entry

1
2
3
4
5
6
7
8
9
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

可以看到这个Entry类,这里的key是使用了个弱引用,所以因为使用弱引用这里的keyThreadLocal会在JVM下次GC回收时候被回收,而造成了个keynull的情况,而外部ThreadLocalMap是没办法通过null key来找到对应value的。如果当前线程一直在运行,那么线程中的ThreadLocalMap也就一直存在,而map中却存在key已经被回收为null对应的Entryvalue却一直存在不会被回收,造成内存的泄漏。
不过,这一点设计者也考虑到了,在get()set()remove()方法调用的时候会清除掉线程ThreadLocalMap中所有EntryKeynullValue,并将整个Entry设置为null,这样在下次回收时就能将Entryvalue回收。
这样看上去好像是因为key使用了弱引用才导致的内存泄漏,为了解决还特意添加了清除null key的功能,那么是不是不用弱引用就可以了呢?
很显然不是这样的。设计者使用弱引用是由原因的。

  • 如果使用强引用,那么如果在运行的线程中ThreadLocal对象已经被回收了但是ThreadLocalMap还持有ThreadLocal的强引用,若是没有手动删除,ThreadLocal不会被回收,同样导致内存泄漏。
  • 如果使用弱引用ThreadLocal的对象被回收了,因为ThreadLocalMap持有的是ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。nullkeyvalue在下一次ThreadLocalMap调用setgetremove的时候会被清除。

所以,由于ThreadLocalMap和线程Thread的生命周期一样长,如果没有手动删除Map的中的key,无论使用强引用还是弱引用实际上都会出现内存泄漏,但是使用弱引用可以多一层保护,null key在下一次ThreadLocalMap调用setgetremove的时候就会被清除。 因此,ThreadLocal的内存内泄漏的真正原因并不能说是因为ThreadLocalMap的key使用了弱引用,而是因为ThreadLocalMap和线程Thread的生命周期一样长,没有手动删除Map的中的key才会导致内存泄漏。所以解决ThreadLocal的内存泄漏问题就要每次使用完ThreadLocal,都要记得调用它的remove()方法来清除。

总结一波:

(1)每个Thread维护着一个ThreadLocalMap的引用

(2)ThreadLocalMapThreadLocal的内部类,用Entry来进行存储

(3)ThreadLocal创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap

(4)ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在map

(5)在进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法。

(6)ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value

OK,现在从源码的角度上不知道你能理解不,对于ThreadLocal来说关键就是内部的ThreadLocalMap


ThreadLocal 解析 [转载]
https://luoyuy.top/posts/a6b3bf9fb608/
作者
LuoYu-Ying
发布于
2022年4月16日
许可协议