ThreadLocal源码分析以及可能引发的内存泄漏问题
ThreadLocal是什么
ThreadLocal是解决多线程并发访问的一种方案,官方的解释如下:
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
#### */
复制代码
大概意思是ThreadLocal可以在线程内存储变量,不同之处在于每个线程读取变量是相互独立的。
ThreadLocal和Synchonized相比,Synchonized是利用锁机制,使变量或者代码块在同一时间只能被一个线程访问,而ThreadLocal是为每个线程提供了自己的变量副本,这样每个线程在同一时间访问的不是同一个对象这样来实现多线程对数据的共享问题。
ThreadLocal的使用
- void set(Object value)
设置当前线程的线程局部变量的值。
- public Object get()
该方法返回当前线程所对应的线程局部变量。
- public void remove()
将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是 JDK 5.0 新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动 被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
- protected Object initialValue()
返回该线程局部变量的初始值,该方法是一个 protected 的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行 1 次。ThreadLocal 中的缺省实现直接返回一个null
public final static ThreadLocal RESOURCE = new ThreadLocal();RESOURCE代表一个能够存放String类型的ThreadLocal对象。 此时不论什么一个线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。
源码分析
当调用ThreadLocal的get方法时,会拿到当前线程的ThreadLocalMap,即每一个线程中都有一个ThreadLocalMap
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内部是一个Entry数组,Entr是一个以ThreadLocal对象为键,存储值为值,数组的使用是为了处理多个变量的情况
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
复制代码
这样getEntry时获取的是某个ThreadLocal对应的值,值得注意的是这里的key是弱引用
内存泄漏分析
通过源码分析,每个Thread维护了一个ThreadLocalMap,这个映射表的Key是ThreadLocal,Value是要存的值,同时Key是作为弱引用使用,弱引用对象会在GC时被回收。ThreadLocal对象作为key使用,而ThreadLocalMap和Thread生命周期一致,当ThreadLocal对象被回收时如果当前线程未结束,就会存在key已经为null,value访问不到的情况而导致内存泄漏。这里弱引用的使用会在ThreadLocal的get或者set方法在某些时候被调用时会调用expungeStaleEntry方法用来清除Entry中Key为null的Value,但是这是不及时的,也不一定每次都能执行,所以需要在使用之后调用remove()方法来显示调用expungeStaleEntry方法进行回收。其本质问题是ThreadLocalMap的生命周期和Thread一样长,没及时remove时线程没有结束就会导致内存泄漏。弱引用的使用增加了回收的机会,一点程度上避免了泄漏。当使用线程池和ThreadLocal时要注意线程是不断重复的,不手动删除会导致value的积累。
Android中ThreadLocal的应用
我们知道在 Handler 机制中 Handler 所在的线程中都有一个独立的 Looper 对象,其实就是用到了ThreadLocal。
// sThreadLocal.get() will return null unless you've called prepare().
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static Looper sMainLooper;
/**
* Initialize the current thread as a looper, marking it as an
* application's main looper. The main looper for your application
* is created by the Android environment, so you should never need
* to call this function yourself. See also: {@link #prepare()}
*/
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
/**
* Return the Looper object associated with the current thread. Returns
* null if the calling thread is not associated with a Looper.
*/
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
/** Initialize the current thread as a looper.
* ......
*/
public static void prepare() {
prepare(true);
}
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
/**
* Returns the application's main looper, which lives in the main thread of the application.
*/
public static Looper getMainLooper() {
synchronized (Looper.class) {
return sMainLooper;
}
}
复制代码
prepareMainLooper为App进程启动的时候在APP的入口方法中会启动一个Looper,子线程中实现Handler的时候也要显示调用Looper.prepare()来生成当前线程的Looper对象,Looper正是通过ThreadLocal机制来保证了每个线程都有一个自己的Looper,同时各个线程之间不互相干扰。