重走JAVA之路(四):ThreadLocal源码解析

527 阅读6分钟

前言

说起ThreadLocal大家应该有种很熟悉的感觉,但是又好像不知道是干啥用的,第一次接触它还是在Looper的源码中,每次获取Looper对象是,通过ThreadLocal的get方法获取到当前线程的Looper对象,有兴趣的可以看看之前的文章Android源码学习之handler,为什么要通过ThreadLocal来获取Looper对象呢,亦或者说这样做有什么好处?今天就带大家一起深入了解这个神秘的ThreadLocal。

源码

话不多说,直接开撸:

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 */
public class ThreadLocal<T> {
    private final int threadLocalHashCode = nextHashCode();
    private static AtomicInteger nextHashCode =
        new AtomicInteger();
    private static final int HASH_INCREMENT = 0x61c88647;
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    public ThreadLocal() {
    }

    public T get() {
        Thread t = Thread.currentThread();
        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();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
}

从类上面的注释可以看到,大概翻译下也就是:该类提供线程局部变量,这些变量与正常的变量不同,而是每个访问一个的线程都有自己独立初始化的变量副本,ThreadLocal实例通常是类中的私有静态字段,希望将状态与线程关联

不要羡慕鄙人的英语,因为。。我是google翻译的...(咳咳)

这里只是摘了一段代码,从上面暴露的方法可以看到,提供了set,get方法,很明显就能看出来,set方法时,key是this,也就是当前的ThreadLocal对象,value就是传递进来的值,而最终是存储到哪呢,一个叫ThreadLocalMap的对象,追踪一下,发现它其实是ThreadLocal的静态内部类:

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

Set方法

可以看到内部再维护了一个静态Entry类继承弱引用,所以上面所说的key,ThreadLocal对象其实是咦弱引用的形式存储的,这样也有益于GC回收,防止内存泄漏,我们先来看set方法:

  • 通过key的哈希码和数组长度,计算出存储元素的下标,这点应该很类似于HashMap中的找数组下标的方式。
  • 找到下标之后,一个循环,从i开始往后一直遍历到数组最后一个Entry,如果key相等,覆盖value,如果key为null,用新key、value覆盖,同时清理历史key=null的陈旧数据
  • 如果找到下标为空的元素,跳出循环,将key和value,设置进去,填满该下标元素位置,同时size++,如果超过阈值,重新hash
	   private void rehash() {
           //清理一次旧的数据
            expungeStaleEntries();
           //如果当前size大于3/4的阈值,就进行扩容
            if (size >= threshold - threshold / 4)
                resize();
        }
        private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            //将长度扩容到之前的2倍
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;
            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    //取出ThreadLocal对象
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        //如果不为空,类似上面的循环,一直找到一个没有使用的位置,在						空节点上塞入Entry
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }
            setThreshold(newLen);
            size = count;
            table = newTab;
        }

大部分注释,其实都是根据里面的英文注释翻译过来的,所以想了解的可以静下心来好好的翻一翻源码,相信我,你会有意外的收获。

Get方法

   public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
       	//通过当前线程,获取ThreadLocalMap,如果不为空,返回value,否则走初始化流程
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    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;
    }
	void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
	ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        	//初始化,设置阈值位int值16
            table = new Entry[INITIAL_CAPACITY];
        	//计算数组下标
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
 	//阈值设置为容量的*2/3,即负载因子为2/3,超过就进行再哈希
     private void setThreshold(int len) {
        threshold = len * 2 / 3;
      }
  • 从当前线程中获取ThreadLocalMap,查询当前ThreadLocal变量实例对应的Entry,如果不为null,获取value,否则进入初始化
  • 初始化,设置数组初始长度,阈值等等参数

总结

  • ThreadLocal内部维护ThreadLocalMap,功能大致类似于HashMap,内部静态Entry类,key是ThreadLocal对象本身,value是传递来的对象,真正实现结构是数组,不断的扩容插入数据。
  • ThreadLocal解决线程局部变量统一定义问题,并不是用来用来解决线程安全问题的,因为本身就是多线程不共享的,是不存在同步竞争的关系的,保证线程本地变量且只能单个线程内维护使用
  • 本文只给出了大概代码,主要看ThreadLocalMap类代码,内部如何实现了一套定制的线性探测hash表以及高效的垃圾清理机制
  • 对于Hash冲突,也就是当经历过hash计算出下标,发现位置上是有人的,ThreadLocalMap和HashMap的处理方式有所不同:
    • ThreadLocalMap:比较直接简单,如果发生冲突,将下标i加1,不断的进行遍历整个数组,找到空的位置放置数据,同时计算当前size是否超过阈值,如果超过,就扩容,每次扩容成之前的2倍。
    • HashMap:JDK1.8之前内部是数组+链表实现的,1.8是数组+红黑树,这里说链表的实现方式,JDK1.8源码还没有详细读过,链表的话,遍历链表,看是否有key相同的节点,有则更新value值,没有则新建节点,此时若链表数量大于阀值8,就进行扩容,由于hash的平均性,这样的效率明显会比ThreadLocalMap高不少,有兴趣可以看下这篇文章,你想要的HashMap都在这里

相关文章阅读:

3分钟带你看懂android的Binder机制

Activity不用注册?手把手教你Hook

Android源码学习之handler

求求你们不要再问HashMap原理了....

零基础带你吃掉JNI全家桶

请帮顶 / 评论点赞!因为你的鼓励是我写作的最大动力!