通俗易懂弄清ThreadLocal内部原理

前言

ThreadLocal是JDK中的一个类,不少基础框架和平时开发中都会使用到,所以有必要弄清其内部原理,才能更好地使用它。java

使用方法

要弄清原理,仍是要先知道如何使用,ThreadLocal用起来是很简单的,通常都是把ThreadLocal定义为static变量,也就是只有一个实例对象,以下:git

private static ThreadLocal<Integer> sThreadLocal = new ThreadLocal<Integer>();
复制代码

而后就能够在各个线程中使用这个sThreadLocal了:github

for (int i = 0; i < 5; i++) {
	final int index = i;
	new Thread() {
		
		public void run() {
			sThreadLocal.set(index);
			
			try {
				Thread.sleep(1000);
			} catch (Throwable t) {
				t.printStackTrace();
			}
			
			System.out.println(sThreadLocal.get());
		}
		
	}.start();
}
复制代码

能够看到,一个ThreadLocal只能用来保存一个对象,若是须要保存多个对象,就须要定义多个ThreadLocal。数组

使用场景

ThreadLocal的做用就是用来保存线程相关的局部对象,当某个对象跟线程有一对一的关系,就能够使用ThreadLocal进行保存。并发

在Android中,咱们知道一个线程对应一个Looper,所以Looper内部就使用到了ThreadLocal,把线程的Looper对象保存在ThreadLocal中。又好比AnimationHandler类,它内部也定义了一个ThreadLocal对象,用于保存线程的AnimationHandler对象,也就是说,线程的动画执行最终都是在被同一个AnimationHandler对象处理的,有兴趣能够本身看源码。框架

咱们还注意到,定义的ThreadLocal虽然在多个线程中被使用,但不会有线程问题,由于每一个线程访问的都是本身的局部对象。oop

原理分析

  • 总体原理图性能

    这个Thread就是线程对象,每一个线程只有一个,能够经过Thread.currentThread()方法拿到,也能够在new Thread()时拿到,无论什么方式拿到的都是同个对象,这个对象里面有一个哈希表,每个Entry的key就是ThreadLocal的弱引用,而value就是set进来的局部对象。优化

    能够看到这个哈希表是线程惟一的,而里面的每个Entry,对应一个ThreadLocal对象,好比说我在线程A中用到了3个ThreadLocal,都设置了不一样的局部对象,那么这个哈希表就有3个Entry对象。动画

  • ThreadLocalMap

    上面所说的线程Thread对象中的哈希表,其真实类型就是ThreadLocalMap,它实际上是一个简化版哈希表,初始容量为16,固然,只有真的放东西Entry数组才会初始化,threshold为容量的2/3,超过了就触发扩容,扩容也是比较简单的,就是直接扩大为原容量的2倍,至于哈希冲突问题,若是冲突就采用线性探测的方法解决。

    这里还有个问题,就是如何根据key肯定放到哪一个桶里,能够看下代码:

    int i = key.threadLocalHashCode & (len-1);
    复制代码

    threadLocalHashCode在初始化时被赋值:

    public class ThreadLocal<T> {
      	......
      	private final int threadLocalHashCode = nextHashCode();
    
      	private static final int HASH_INCREMENT = 0x61c88647;
    
      	private static int nextHashCode() {
          	return nextHashCode.getAndAdd(HASH_INCREMENT);
      	}
      	......
      }
    复制代码

    也就是说,这个threadLocalHashCode,就是0x61c88647的整数倍,而后跟len-1进行与操做,就获得了桶的下标,至于为何是0x61c88647这个数值,网上有分析文章,有兴趣能够本身查看。

  • 内存泄漏

    根据上面的总体原理图,能够获得一条引用链:

    Thread对象 --> threadLocals --> Entry[] --> WeakReference<ThreadLocal<?>>和value
    复制代码

    由于Thread对象的生命周期是比较长的,只有当线程退出后,Thread对象才会被回收,那么在线程退出前,咱们的ThreadLocal被WeakReference包着,若是外部没有强引用,在内存不足时gc会自动回收了,但那个value就不会了,须要咱们本身手动去清空引用。

    所以这里存在一个内存泄漏的问题,若是某个局部对象使用ThreadLocal保存了,而后用完以后没有清除掉,线程又还没退出,就可能致使内存泄漏,固然解决的方法也很简单,调用remove方法就能够:

    sThreadLocal.remove();
    复制代码

    另外,并非说这种场景下咱们不调用remove方法,value引用就一直不会被置空,ThreadLocal内部作了优化,在get和set时会主动置空那些key被gc自动回收的value引用,不过通常咱们的ThreadLocal都是定义为static,这种状况就不可能会被gc自动回收了。

  • Netty的FastThreadLocal

    若是同个线程用到了多个ThreadLocal,也就是Entry[]会有多个元素,那么每次在搜索某个ThreadLocal对应在哪一个桶这个过程就会比较耗时,特别是对于服务端并发量很大的状况下,性能损耗会很明显,所以FastThreadLocal就作了优化,直接把ThreadLocal的哈希表去掉了,改成用变量index记录当前ThreadLocal对应的数组下标,也就是空间换时间,实现代码:FastThreadLocal.java,固然,在Android中对同个线程的ThreadLocal哈希表频繁操做这种场景可能并很少见。

相关文章
相关标签/搜索