Android进阶知识:ThreadLocal

一、ThreadLocal是什么?

ThreadLocal是一个线程内部数据存储类,经过他能够在指定的线程中存储数据。存储后,只能在指定的线程中获取到存储的数据,对其余线程来讲没法获取到数据。java

二、ThreadLocal的使用场景

平常使用场景很少,当某些数据是以线程为做用域而且不一样线程具备不一样的数据副本的时候,能够考虑使用ThreadLocalAndroid源码的LopperActivityThread以及AMS中都用到了ThreadLocalide

三、ThreadLocal的使用示例

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();
}
}
复制代码

运行结果:this

D/ThreadLocalActivity: Thread:main name:小明  
D/ThreadLocalActivity: Thread:thread1 name:小红  
D/ThreadLocalActivity: Thread:thread2 name:null
复制代码

能够看到虽然访问的是同一个ThreadLocal对象,可是获取到的值倒是不同的。spa

四、ThreadLocal的源码阅读

那么为何会形成这样的结果呢?这就须要去看看ThreadLocal的源码实现,这里的源码版本为API28。主要看它的getset方法。
set方法:线程

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方法:设计

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
复制代码

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

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
复制代码

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

/* 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方法:生命周期

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方法:内存

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

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()方法来清除。

参考资料:

Android开发艺术探索

ThreadLocal内存泄漏真因探究

相关文章
相关标签/搜索