前语

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);
}

初始化中总共做了五步操作:

  1. 先将table目标赋值完成,它是一个Entry数组目标,这是接下来的要点;
  2. 经过key也便是ThreadLocal获取HashCode值,然后和数组的初始长度-1(15)进行与运转,这样能够确保此刻获取到的i值在0-15之间;
  3. 然后创立新的Entry目标存入到table数组中,数组的下标便是上一步获取到的i值;
  4. 第四步记录此刻的巨细size为1;
  5. 最终设置阈值巨细,初始和数组的长度保持一致。

其实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();
}

这儿咱们持续分过程来看下详细操作:

  1. 首先根据ThreadLocal计算出HashCode,然后计算出对应的下标值,注意看一点,这儿的与操作是和len – 1 来操作的;
  2. 根据下标去Entry[]寻觅对应的弱引证值;
  3. 假如Entry[]中已经存在此Key,那么直接修正value
  4. 假如Entry[]中不存在此Key,那么添加到Entry[]中;
  5. 最终还需要查看下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);
}

老样子分过程来剖析下详细操作:

  1. 先经过ThreadLocal的HashCode值确定数组中对应的下标值;
  2. 然后经过下标值i从table数组中获取对应的数据,假如获取到的值e不为空,直接回来;
  3. 这儿第三步的getEntryAfterMiss()办法其实便是e.refersTo()办法检测到了弱引证目标被铲除时,需要重新进行对数组进行散列操作,将数据存入到Entry数组中。

到这咱们就将ThreadLocal存数据和取数据的操作都介绍完了,一个ThreadLocal涉及到的目标仍是挺多了,ThreadLocal经过当时线程Thread进行ThreadLocalMap目标的绑定,确保了当时ThreadLocal在当时线程中只要一个ThreadLocalMap目标,最终仍是ThreadLocalMap目标承受了存数据和取数据的关键操作。

最终是ThreadLocalThreadThreadLocalMapThreadLocalMap.Entry四个类的联系图:

面试必问之 - ThreadLocal

写在最终

我是Taonce,假如觉得本文对你有所协助,帮助关注、赞或者收藏三连一下,谢谢~