ThreadLocal是如何实现保存线程私有对象的

Looper中的ThreadLocal

最早知道ThreadLocal是在Looper的源码里,用一个ThreadLocal保存了当前的looper对象。

    //一个静态ThreadLocal对象,因此一个进程里的所有Looper都共用这个ThreadLocal
    // sThreadLocal.get() will return null unless you've called prepare().
    @UnsupportedAppUsage
    static final ThreadLocal sThreadLocal = new ThreadLocal();

    //所有Looper都需要调用的prepare方法,实例化新的Looper并保存到ThreadLocal
    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }
    
    //由于ThreadLocal保存的是线程内独立的对象,所以在哪个线程调用,取到的就是哪个线程的Looper
    /**
     * Return the Looper object associated with the current thread.  Returns
     * null if the calling thread is not associated with a Looper.
     */
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }        

当时就查了下ThreadLocal,发现是保存线程私有对象的容器,以为就是一个类似hashmap,用线程做key,存value。最近看了下并不是如此,实际上是以ThreadLocal自身为key来存储对象的。于是来学习下ThreadLocal源码是如何做到保存线程私有对象的。

ThreadLocal和ThreadLocalMap以及Thread的关系

ThreadLocal、ThreadLocalMap、Thread之间的关系和我们下意识中想的不太一样,不过一步步看下去之后就能明白为啥ThreadLocal能保存线程私有对象了。

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

这个是Thread源码,每一个Thread都有一个ThreadLocalMap对象,而ThreadLocalMap是ThreadLocal的内部类,是实际存储对象的容器。

    static class ThreadLocalMap {
        //可以看到这里持有的ThreadLocal对象是弱引用,
        //防止线程不死(比如android主线程)ThreadLocal无法被回收。
        //这里可以看到Entry的key是ThreadLocal,value是要保存的对象。
        static class Entry extends WeakReference> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }

        //线程初始化后,会实例化一个ThreadLocalMap,map里有table,table里存Entry。
        //因此,一个线程对应一个ThreadLocalMap,一个ThreadLocalMap对应多个Entry。
        //每个Entry对应一个ThreadLocal作为key,所以一个ThreadLocal只能存一个value。
        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);
        }

        //ThreadLocalMap保存方法
        private void set(ThreadLocal key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            //通过ThreadLocal的hashcode获得索引i
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal k = e.get();

                //验证k相等,replace value的值
                if (k == key) {
                    e.value = value;
                    return;
                }

                //有空的位置,存入
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //到这里还没return,说明table存满了,需要扩容
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

        //ThreadLocalMap获取方法,还是通过hashcode获取table中的索引
        private Entry getEntry(ThreadLocal key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
    } 

基本关系知道了,最后再来看看ThreadLocal的方法:

    //ThreadLocal的get方法
    public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //拿到当前线程的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //因为ThreadLocalMap存的时候就是拿ThreadLocal作key
            //因此这里传入this获取entry,再拿到value
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    //获取当前线程的ThreadLocalMap对象
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    //ThreadLocal的set方法
    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //拿到当前线程的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //this作key传入存储value
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

那么现在基本完全清楚ThreadLocal与Thread还有ThreadLocalMap之间的关系了。
每个Thread都有一个成员变量ThreadLocalMap。这个ThreadLocalMap对象不是public,所以外部调用不了,可以看做是线程私有对象。
ThreadLocalMap存了一个table,table里保存了一些entry,每个entry对应一个key和value,而这个key就是ThreadLocal对象。
因此一个ThreadLocal只能存一个value,但是可以通过new多个ThreadLocal来保存多个线程私有对象。

ThreadLocal内存泄露

在上面的源码中我们看到Entry里持有的ThreadLocal对象是弱引用持有,因此ThreadLocal不会因为线程持有而泄露,比如我们Android的主线程,正常使用过程中是不会挂掉的。
但是Enrty的value的是强引用的,因此ThreadLocal中的value还是会因为线程持有而无法回收。如果继续看源码的话,会发现在ThreadLocalMap的resize和expungeStaleEntry方法里会检查key为空的value值为空帮助GC。
因此为了避免value内存泄露,我们需要在ThreadLocal不需要的时候主动remove掉。

并发修改问题

    private final int threadLocalHashCode = nextHashCode();

    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

ThreadLocal通过自身的threadLocalHashCode来碰撞得到自己在ThreadLocalMap的table里的索引i。因此这个threadLocalHashCode就十分重要了。
这里需要保证threadLocalHashCode是唯一的,否则两个线程同时创建ThreadLocal得到相同的hashcode,那就破坏了ThreadLocalMap的线程私有特性了。
这里生成threadLocalHashCode是通过一个静态对象nextHashCode不断增加来获得的。那怎么保证多个进程并发实例化ThreadLocal对象,不会生成相同的hashcode呢?
答案是AtomicInteger,通过这个类来保证变量自增操作在多线程操作时候的原子性。
我们知道Synchronized也可以保证原子性,但相比于它,AtomicInteger类是通过非阻塞方法来实现原子性的,需要的性能开销更小。
这种非阻塞实现原子性的方法和处理器的CAS指令有关,感兴趣的小伙伴自行研究吧~

你可能感兴趣的:(ThreadLocal是如何实现保存线程私有对象的)