ThreadLocal的原理,以及在Looper是如何应用的?(字节跳动、小米)

这道题想考察什么?

ThreadLocal 是必须要掌握的,这是因为 Looper 的工作原理,就跟 ThreadLocal 有很大的关系,理解 ThreadLocal 的实现方式有助于我们理解 Looper 的工作原理,这篇文章就从 ThreadLocal 的用法讲起,一步一步带大家理解 ThrealLocal。

考察的知识点
  1. ThreadLocal的内部运行原理
  2. Looper相关知识
考生应该如何回答

ThreadLocal 可以把一个对象保存在指定的线程中,对象保存后,只能在指定线程中获取保存的数据,对于其他线程来说则无法获取到数据。日常开发中 ThreadLocal 使用的地方比较少,但是系统在 Handler 机制中使用了它来保证每一个 Handler 所在的线程中都有一个独立的 Looper 对象,为了更好的理解 Handler 机制。

ThreadLocal 是什么

ThreadLocal 是一个关于创建线程局部变量的类。

其实就是这个变量的作用域是线程,其他线程访问不了。通常我们创建的变量是可以被任何一个线程访问的,而使用 ThreadLocal 创建的变量只能被当前线程访问,其他线程无法访问。

那么ThreadLocal是如何确保只有当前线程可以访问呢?我们先来分析一下ThreadLocal里面最重要的两个函数,get(),set()两个函数。

首先看下get()方法中的源码

public T get() {
	Thread t = Thread.currentThread();  //code 1
	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();
}

public void set(T value) {
	Thread t = Thread.currentThread();  //code 2
	ThreadLocalMap map = getMap(t);
	if (map != null)
		map.set(this, value);
	else
		createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
	return t.threadLocals;
}
    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.

从code1 和code2 大家应该不难发现,在给ThreadLocal去set值或者get值的时候都会先获取当前线程,然后基于线程去调用getMap(thread),getMap返回的就是线程thread的成员变量threadLocals。所以通过get 和set都执行对于的函数,这样就保证了threadLocal的访问,一定是只能访问或许修改当前线程的值,这就保障了这个变量是线程的局部变量。

那么接下来ThreadLocalMap又是什么呢?

ThreadLocalMap是什么
static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    private Entry[] table;
    //省略部分代码
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

从源码可以看出来ThreadLocalMap是一个数组,数组里面是继承自弱引用的Entry。弱引用的使用也是为了当出现异常情况,比如死循环的时候内存能得到回收。

再回过头来看一下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);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

这里面的代码就很清晰,根据当前线程取出ThreadLocalMap,然后进行存储数据的操作,如果Map为空的话就先创建,再赋值。

探秘一下map.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();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

代码中int i = key.threadLocalHashCode & (len-1)与HashMap中key的hash值方法一样,主要是避免hash值的冲突。再往下走是遍历Map有三种情况:1. 找存在的key有效,然后赋值;2. 找存在的key但是无效,替换掉过期的Entry;3. 没找到相同key值,新建一个Entry然后赋值。

那么ThreadLocal在Looper中又是如何应用的呢?

ThreadLocal在Looper中的应用

找到Looper的源码

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

 private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {//code 1
            throw new RuntimeException("Only one Looper may be create per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));// code 2
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

Looper与线程的关系是一对一的关系,不同线程之间Looper对象是隔离的,那么Looper是怎么保障这一点的呢?通过上面的代码大家应该不难发现Looper初始化是必须调用prepare函数进行,在调用prepare函数的时候代码会执行到code 1,在code1会先去判断当前线程对于的ThreadLocal中是否存在looper的value,如果存在,那么就抛出异常,这样的执行就保证了一个线程只会设置一次Looper。这个代码的执行流程,就是确保了一个线程只有一个ThreadLocal,一个ThreadLocal就只有一个looper。

总结

ThreadLocal是一个创建线程局部变量的类,它的实现机制决定了这个变量的作用域是线程,其他线程访问不了,利用这个机制,可以保障一个线程只有唯一的一个ThreadLocal变量。然后,在looper中通过prepare函数的设计,确保了一个ThreadLocal 只会和一个Looper进行绑定。通过这两个方式确保了一个线程只有一个ThreadLocal变量,一个ThreadLocal变量只有一个Looper,从而形成了一一对应的关系。


最后

我整理了一套Android面试题合集,除了以上面试题,还包含【Java 基础、集合、多线程、虚拟机、反射、泛型、并发编程、Android四大组件、异步任务和消息机制、UI绘制、性能调优、SDN、第三方框架、设计模式、Kotlin、计算机网络、系统启动流程、Dart、Flutter、算法和数据结构、NDK、H.264、H.265.音频编解码、FFmpeg、OpenMax、OpenCV、OpenGL ES