ThreadLocal 可以把一个对象保存在指定的线程中,对象保存后,只能在指定线程中获取保存的数据,对于其他线程来说则无法获取到数据。日常开发中 ThreadLocal 使用的地方比较少,但是系统在 Handler 机制中使用了它来保证每一个 Handler 所在的线程中都有一个独立的 Looper 对象,为了更好的理解 Handler 机制,这篇文章来说说 ThreadLocal 的原理,一窥究竟。
ThreadLocal 是什么
ThreadLocal 位于 java.lang 包下。
ThreadLocal 是一个关于创建线程局部变量的类。什么是线程的局部变量呢?其实就是这个变量的作用域是线程,其他线程访问不了。通常我们创建的变量是可以被任何一个线程访问的,而使用 ThreadLocal 创建的变量只能被当前线程访问,其他线程无法访问。
使用示例
先来看看一个使用 ThreadLocal 的示例,对 ThreadLocal 有一个基本、直观的认识。
public class ThreadLocalTest extends AppCompatActivity {
private static final String TAG = "ThreadLocalTest";
private ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
booleanThreadLocal.set("MainThread");
Log.d(TAG, "MainThread's stringThreadLocal=" + stringThreadLocal.get());
new Thread("Thread#1") {
@Override
public void run() {
Log.d(TAG, "Thread#1's stringThreadLocal=" + stringThreadLocal.get());
}
}.start();
}
}
首先创建了一个 泛型为 String 的 ThreadLocal 对象,并初始化。这样就有了一个可以保存 String 类型的 ThreadLocal 对象。接着在主线程和子线程中分别操作该对象,使用 set 方法赋值,get 方法取值,注意看每个线程中的打印结果。
D/ThreadLocalTest: MainThread's stringThreadLocal = MainThread
D/ThreadLocalTest: Thread#1's stringThreadLocal = null
可以看到,MainThread 对 stringThreadLocal 的修改并没有影响到 Thread#1 中的值。说明了使用 ThreadLocal 保存的对象的作用域是当前线程。
Looper 中的使用
再来看看 Android 源码中 Looper.java 是怎样使用 ThreadLocal 的。
// sThreadLocal.get() will return null unless you've called prepare().
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
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));
}
这里使用 ThreadLocal 保存 Looper,确保每个线程中只有一个 Looper 对象。
修改默认值
从上面的示例中,我们看到 ThreadLocal 保存的对象默认值是 null。如果我们需要给定一个默认的值,就需要重写 initialValue 方法,该方法默认返回 null,我们可以根据具体要求返回需要的值,如下所示。
private ThreadLocal<Boolean> booleanThreadLocal = new ThreadLocal<Boolean>(){
@Override
protected Boolean initialValue() {
return false;
}
};
ThreadLocal 还有一个对外提供的方法 remove,看名字就知道这是删除已经保存的数据的。
原理
set 方法
ThreadLocal 的 public 方法,只有三个:set、get、remove。我们先从 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 方法进行了如下几部操作:
1.获取当前线程
2.使用当前线程获取一个 ThreadLocalMap 对象
3.如果获取到的 map 对象不为空,则设置值,否则创建 map 设置值
下面是 getMap 源码:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
上面代码获取到的是 Thread 对象的 threadLocals 变量,类型为 ThreadLocal.ThreadLocalMap。
而如果 map 对象为空,则新建 ThreadLocalMap 对象。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
结论:原来每个线程都有一个保存值的 ThreadLocalMap 对象,ThreadLocal 的值就存放在了当前线程的 ThreadLocalMap 成员变量中,所以只能在本线程访问,其他线程不能访问。
我们在看看具体的保存方法:ThreadLocalMap#set
private void set(ThreadLocal key, Object value) {
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();
}
上面的代码实现了数据的存储,其中 table 是一个 Entry[] 数组对象,而 Entry 是用来存储 ThreadLocal key, Object value 的,逻辑是根据 key 找出 Entry 对象,如果找出的这个 Entry 的 k 等于 key,直接设置 Entry 的 value,如果 k 为空,则通过 replaceStaleEntry 保存数据,最后构建出 Entry 保存进 table 数组中。
Entry 对象是怎样保存 key 和 value 的呢?
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>,那么通过 Entry 对象的 get 方法就可以获取到一个弱引用的 ThreadLocal 对象。扩展了一个 Object 类型的 value 对象,并且在构造方法中进行了初始化赋值。Entry 保存了 ThreadLocal(key) 和 对应的值(value),其中 ThreadLoacl 是通过弱引用的形式,避免了线程池线程复用带来的内存泄露。
get 方法
看完 set 方法,再来看看 get 方法的源码:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
get 方法首先取出当前线程的 ThreadLocalMap 对象,如果这个对象为空,则返回默认值;如果不为空,使用当前 ThreadLoacl 对象(this)获取 ThreadLocalMap 的 Entry 对象,返回 Entry 保存的 value 值。
从 ThreadLoacl 的 set 和 get 方法来看,它们操作的对象都是当前线程对象中的 ThreadLocalMap 对象的 Entry[] 数组,因此在不同的线程中访问同一个 ThreadLoacl 的 set 和 get 方法,操作的对应线程中的数据,所以不会影响到其他线程。
参考: