面试必问之 - ThreadLocal
前言
ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到数据。
ThreadLocal可以在各个线程同时使用,但是每个线程互不干扰,每个线程的数据都是独立的。
ThreadLocal简单使用
为了清楚的了解ThreadLocal在多线程中互不干扰的作用,我们定义一个ThreadLocal对象,分别在主线程和两个子线程中进行赋值和取值操作,然后看看取到的值是不是各自独立的。
class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
private val threadLocal = ThreadLocal<String>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
threadLocal.set("main thread")
thread(name = "child1 thread") {
threadLocal.set("child1 thread")
Thread.sleep(1000L)
Log.d(TAG, "Thread name: ${Thread.currentThread().name}, value: ${threadLocal.get()}")
}
thread(name = "child2 thread") {
threadLocal.set("child2 thread")
Thread.sleep(500L)
Log.d(TAG, "Thread name: ${Thread.currentThread().name}, value: ${threadLocal.get()}")
}
Log.d(TAG, "Thread name: ${Thread.currentThread().name}, value: ${threadLocal.get()}")
}
}
上面代码中,我们分别在主线程和两个子线程中队同一个ThreadLocal进行set()
操作,并且在child1线程中故意延时了1s,在child2线程中延时500ms,这样就可以更清晰的看出各个线程赋值和取值操作是互不干扰的,接下来我们运行程序看看结果:
Thread name: main, value: main thread
Thread name: child2 thread, value: child2 thread
Thread name: child1 thread, value: child1 thread
通过日志就可以看出每个线程取到的值都依赖自己的set()
操作,线程之间对于ThreadLocal的赋值是相互独立的,这也是ThreadLocal的魅力所在,接下来我们逐一分析下ThreadLocal的set()
和get()
原理。
ThreadLocal如何存储数据
// ThreadLocal.set(value)方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);// 获取当前线程中的ThreadLocalMap对象
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocal.set()
方法中先通过Thread.currentThread()
获取了当前的线程对象,然后调用getMap(thread)
方法获取了当前线程的ThreadLocalMap对象,如果没有获取到ThreadLocalMap对象那么就通过createMap()
创建一个ThreadLocalMap,如果获取到了ThreadLocalMap对象,直接调用map.set(ThreadLocal, value)
将数据存入到Map中。
这里我们先留意下一个点那就是Thread,这里的ThreadLocalMap对象是何Thread进行绑定的,也是为什么线程之间互不干扰的重要点。
我们先看下createMap(thread, value)
这块逻辑处理:
// ThreadLocal.createMap()
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocal.createMap()
方法中只有初始化ThreadLocalMap这一步操作,具体的逻辑还是在ThreadLocalMap的初始化中。
// ThreadLocalMap构造方法
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);
}
初始化中一共做了五步操作:
- 先将table对象赋值完成,它是一个Entry数组对象,这是接下来的重点;
- 通过key也就是ThreadLocal获取HashCode值,然后和数组的初始长度-1(15)进行与运行,这样可以保证此时获取到的i值在0-15之间;
- 然后创建新的Entry对象存入到table数组中,数组的下标就是上一步获取到的i值;
- 第四步记录此时的大小size为1;
- 最后设置阈值大小,初始和数组的长度保持一致。
其实ThreadLocalMap的构造方法逻辑还是比较清楚的,就是将table创建好,然后将对应的key和value存入进去。再进入ThreadLocalMap.set(ThreadLocal, value)
方法具体逻辑之前,我们先看下这个Entry对象是什么?
// ThreadLocalMap.Entry
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
ThreadLocalMap是ThreadLocal内部的一个静态类,而Entry则是ThreadLocalMap内部的一个静态类,并且它是继承自WeakReference的,它是一个弱引用对象,可在内存不够时自动回收,它内部只有一个value变量,用于存储我们在set()
操作时传入的数据。
下面我们接着来看下ThreadLocalMap.set(ThreadLocal, value)
这个方法是如何存储数据的。
// 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();
// 如果存在ThreadLocal为key的Entry,直接覆盖值
if (k == key) {
e.value = value;
return;
}
// 如果不存在,将键值对插入到Entry[]数组中
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
这里我们继续分步骤来看下具体操作:
- 首先根据ThreadLocal计算出HashCode,然后计算出对应的下标值,注意看一点,这里的与操作是和len - 1 来操作的;
- 根据下标去
Entry[]
寻找对应的弱引用值; - 如果
Entry[]
中已经存在此Key
,那么直接修改value
; - 如果
Entry[]
中不存在此Key
,那么添加到Entry[]
中; - 最后还需要检查下table是否需要进行扩容操作。
看到这里对于整个ThreadLocal存储数据的操作是否有个大概的了解了,我们先简单的总结下整个过程,ThreadLocal其实是将数据存储在ThreadLocalMap对象中,并且这个Map对象是和当前的线程所绑定的,每个线程都有自己独立的ThreadLocalMap对象,这样我们在各个线程存入的数据都是独立开来的,这也就是为什么我们在简单使用环节中两个子线程存入数据后再取出之后都是不会影响的原理。
那么接下来我们趁热打铁继续看下ThreadLocal是如何获取数据的。
ThreadLocal如何获取数据
// ThreadLocal.get()
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);// 获取当前线程的ThreadLocalMap对象
if (map != null) { // map不为空,去map中寻找
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// map为空,直接初始化值
return setInitialValue();
}
在get()
方法中,我们依旧可以看到熟悉的方法getMap(thread)
,还是通过当前线程对象获取对应的ThreadLocalMap对象,如何找到了对应的Map对象,那么就调用ThreadLocalMap.getEntry(ThreadLocal)
方法进行值的获取,如果没有寻找到Map对象,直接调用setInitialValue()
方法将初始值存入到ThreadLocalMap中,这里的initialValue我们可以自己设置也可以不设置默认为null。
这里我们主要是看ThreadLocalMap.getEntry(ThreadLocal)
这个方法是如何进行值的获取:
// ThreadLocalMap.getEntry(ThreadLocal)
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// Android-changed: Use refersTo()
if (e != null && e.refersTo(key))
return e;
else
return getEntryAfterMiss(key, i, e);
}
老样子分步骤来分析下具体操作:
- 先通过ThreadLocal的HashCode值确定数组中对应的下标值;
- 然后通过下标值i从table数组中获取对应的数据,如果获取到的值e不为空,直接返回;
- 这里第三步的
getEntryAfterMiss()
方法其实就是e.refersTo()
方法检测到了弱引用对象被清除时,需要重新进行对数组进行散列操作,将数据存入到Entry数组中。
到这我们就将ThreadLocal存数据和取数据的操作都介绍完了,一个ThreadLocal涉及到的对象还是挺多了,ThreadLocal通过当前线程Thread进行ThreadLocalMap对象的绑定,保证了当前ThreadLocal在当前线程中只有一个ThreadLocalMap对象,最后还是ThreadLocalMap对象承受了存数据和取数据的关键操作。
最后是ThreadLocal
、Thread
、ThreadLocalMap
和ThreadLocalMap.Entry
四个类的关系图:
写在最后
我是Taonce,如果觉得本文对你有所帮助,帮忙关注、赞或者收藏三连一下,谢谢😆😆~
转载自:https://juejin.cn/post/7352245958593724454