博客为 有时个哥 原创,如需转载请标明出处:https://mp.youkuaiyun.com/mdeditor/83142537
说明: 参考源码为Android7.0,请读者注意
如果大家仔细看过Handler 的源码之后,就会发现其主要角色Looper类中有个ThreadLocal对象。
// sThreadLocal.get() will return null unless you've called prepare().
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
其实Handler 能实现线程之间通信主要的是因为ThreadLocal的功劳。
ThreadLocal是做什么的
ThreadLocal 是为线程提供的局部变量的类。它为线程提供一个独立的初始化副本变量。
我们先来大体看一下ThreadLocal 的工作原理结构。
下面我们通过分析代码一起来看一下ThreadLocal的原理。
ThreadLocalMap
首先看一下ThreadLocalMap。
ThreadLocalMap是ThreadLocal的静态内部类。
/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*/
static class ThreadLocalMap {
通过ThreadLocal 的类的介绍说明我们知道这么几点:
- ThreadLocalMap 是一个定制的Map, 只适合维护线程本地值。
- 除了ThreaLocal类外,其他类不对其进行操作。
- 为了处理大的和周期长的变量,内部的Hash表的Key采用了弱引用。
- 只有在耗尽空间时才会主动删除陈旧无用的条目。
ThreadLocalMap中也有一个静态内部类Entry
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
通过代码我们可以知道几点:
- Entry 集成了 WeakReference<ThreadLocal<?>>,所以其Key值为弱引用(这一点要注意)。
- Entry 可以接收的key永远是ThreadLocal 。
- Entry 对象是真正存储数据的单元。
- 当key为null 时,此条目就可以从表中移除
Key为什么设计成弱引用
Map类的介绍中提到了,是针对大的周期长的数据对象。 因为线程一般执行的任务的时间比较长, 有很多要存储的变量对象的周期可能存在于整个线程任务周期中, 数据量比较大的对象也是很常见的,但是当你存储的时候使用的是强引用, 则当线程执行完时,这些变量对象不会立马回收掉(具体的可以去了解一下GC的回收机制)。这样就造成了内存没有及时的被释放回收,从而可能造成其他的问题(比如由于内存占用达到极值造成oom异常等)。如果当对象没有强引用指向时,只有弱引执向时,gc 执行时则会直接回收此对象。所以设计出弱引用,能更好的及时的回收对象,释放内存空间。
但是要注意的是,虽然Entry 类的key 是弱引用,但是Entry是强引用,当key被释放可,则Entry的key就为null,变成了无用数据, 但是由于Entry强引用,所以要记得要调用Map 的remove方法把其移除,否则就造成了内存泄漏。
ThreaLocal类外,其他类不对ThreaLocalMap进行操作
通过一系列的设计,也会看到一直遵循着这个设计理念,比如ThreaLocalMap 是ThreadLocal内部类,并且Map的方法都是私有的,这就导致了Map的接口调用只能在ThreadLocal内部;Map只接受ThreadLocal 类型的key(实际上真正的是Entry),ThreadLocal 中的代码编写也有体现(下面会逐步的讲到ThreadLocal代码). 遵循这一设计理念,是为了保证只有ThreadLocal 一个入口操作,保证数据操作安全。
定制的Map, 只适合维护线程本地值
假如你看过Thread 的代码,你会发现在Thread 会持有一个ThreadLocal.ThreadLocalMap 的变量threadLocals。就是这个变量来进行维护数据的。
ThreaddLocalMap的实现
Map的内部是有一个Entry[] 维护着数据, 大小默认是16;
/**
* 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;
通过hash值得计算来得到Entry[] 数组的下标,即数据的存储位置。
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
setThreshold()方法是设置调制Map大小的阈值。
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
threshold 就是调整Map大小的阈值了。
然后又几个主要方法: set getEntry remove等
Set
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
set方法中首先用HashCode计算出了数组的下标,当对应下标的位置有数据存在时,则进入for循环。 for 循环做了两个事(检查两种特殊的情况),1 是判断当前循环取得的数据是否和要存储的数据一样(包括键值对); 2 当前循环获得的数据是否key为null,为null则用当前的新值替换上。无论满足哪种情况,都会直接return出方法。否则就按照新的方法安排新值。
get 方法
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
get 方法也是先计算下标,然后查找数据, 如果数据不为空,并且key相等,则直接返回,否则就按规则计算出下标,找到存放的位置。具体的规则实现有兴趣的可以查看源码。
由代码可以看出ThreadLocalMap和HashMap的结构还是有不一样的地方; HashMap是数组加链表实现的,而ThreadLocalMap 而完全是用数组存储。
ThreadLocal
ThreadLocal 其实就是对ThreadLocalMap的操作封装。由于ThreadLocalMap 的方法基本是私有的,所以除ThreadLocal 以外的类,无法对ThreadLocalMap进行直接操作。
ThreadLocal 也有set, get 方法。 这两个也是其主要的方法。先看set方法。
set
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
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是没有key的参数,只有value。所以外部无法决定其value是谁的数据变量, 只是负责把value传入进来。然后得到当前线程, 接着调用了getMap()。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
getMap() 的代码非常简单,只是把当前线程的threadLocals返回,这是一个ThreadLocalMap类型。就是这个类型真正的存储维护线程的数据变量。
首先查看线程的 threadLocals 是否为空,不为空则直接把数据存储进去了。
若为空,则调用createMap() 创建ThreadLocalMap();
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
通过代码可以看出,new 了一个ThreadLocalMap对象,赋值给了线程的threadLocals字段。
虽然代码不多,但是需要我们好好深思理解。
- 外部无法传入key, 所以存储是有内部决定。 通过Thread.currentThread()得到当前线程,从而与当前线程做了操作绑定。这就是为什么ThreadLocal 在多线程的情况下还能做到数据存储不错乱。因为set的数据只能和当前的线程有关系。
- Thread 的map为null时,ThreadLocal要为其初始化Map。
- Thread map存在时, 则直接对把数据添加到Map中。这句代码意味着,真正维持数据的是线程自己。 ThreadLocal只是其他一个获取Map操作的入口。
我们结合一下上面的工作原理图, 你会发现Thread1中的Map即有ThreadLocal1 key的数据,又有ThreadLocal2 作为key的数据, Thread2也是一样。这就是说明上面的第三点。 ThreadLocal是入口,只负责对Thread的Map的数据操作。只要是在当前Thread中调用的ThreadLocal 的set方法都是操作Map的入口, 无论他是ThreadLocal1还是ThreadLocal2.
结合一下代码分析可能更容易明白:
if (map != null)
map.set(this, value);
else
createMap(t, value);
看set方法中的这段代码,假如map不为null, 是ThreadLocal1 给线程初始化的。当前是ThreadLocal2. 按照代码逻辑,则会直接存储数据。所以Thread 中的Map的key既有ThreadLocal1又有ThreadLocal2.
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方法也很简单,也是没有传入key。保证了只能取得当前线程的Map。
总结
ThreadLocal 就是一个仅能够对当前线程变量副本(ThreadLocalMap)操作的类。由于其set,get的设计,能做到各个线程存储数据操作的隔离。