Android ThreadLocal理解

Android ThreadLocal与Java ThreadLocal实现并不相同。

在Android消息循环一文中http://blog.csdn.net/zyfzhangyafei/article/details/62882117,提到了ThreadLocal,这个叫做 线程局部变量 的东西。

看一个实例:

package test;

import test.*;

public class Test {
static final    ThreadLocal<ThreadValue> mThreadLocal = new ThreadLocal<ThreadValue>();
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        ThreadValue threadValue = new ThreadValue("主线程");
         mThreadLocal.set(threadValue);
         System.out.print("in main thread : mThreadLocal:" + mThreadLocal +"\n");
         System.out.print("in main thread : 名字:" + mThreadLocal.get().name +"\n");
         mThreadLocal.get().print();

         new Thread(new Runnable() {
                @Override
                public void run() {

                    ThreadValue childThreadValue = new ThreadValue("子线程");
                     mThreadLocal.set(childThreadValue);
                     System.out.print("in child thread : mThreadLocal:" + mThreadLocal +"\n");
                     System.out.print("in child thread : 名字:" + mThreadLocal.get().name +"\n");
                     mThreadLocal.get().print();
                }
              }).start();
    }

}

package test;

public class ThreadValue  {
      String name;
      public ThreadValue() {

      }

      public ThreadValue(String name) {
          this.name=name;
      }
      public void print()
      {
          System.out.print("this = " + this+" \n"); 
      }
    }

然后编译:javac test/*.java
运行:java test.Test
输出:
in main thread : mThreadLocal:java.lang.ThreadLocal@788bf135
in main thread : 名字:主线程
this = test.ThreadValue@2b890c67
in child thread : mThreadLocal:java.lang.ThreadLocal@788bf135
in child thread : 名字:子线程
this = test.ThreadValue@4f93b604

可以看出由于mThreadLocal定义为静态最终变量,所以在主线程和子线程中,mThreadLocal都是同一个实例。
但是在两个线程中调用mThreadLocal.get(),得到的ThreadValue对象却并不相同。
这是因为mThreadLocal.get(),取到的对象是线程内的局部变量,相互之间并不干扰。

在android中Handler.java中,通过Looper.myLooper()获得了当前线程绑定的消息泵Looper:

    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

就是通过种方式来实现的。

我们就从这个使用过程来跟踪它的实现和原理。
在Looper.java中:
static final ThreadLocal sThreadLocal = new ThreadLocal();
定义了一个静态的最终的变量sThreadLocal
然后在Loop.prepare中,new了一个Looper,并设置进sThreadLocal中:

   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.set函数:

    public void set(T value) {
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values == null) {
            values = initializeValues(currentThread);
        }
        values.put(this, value);
    }

这里的values 是返回了current.localValues:

Values values(Thread current) {
        return current.localValues;
    }

这个current.localValues定义在Thread.java 中

public class Thread implements Runnable {
...
    ThreadLocal.Values localValues;
...
}

在最初的时候values是null,所以调用initializeValues函数:

    Values initializeValues(Thread current) {
        return current.localValues = new Values();
    }

这里new 的Values对象,被赋给了current.localValues,结合前面values(Thread current)函数,可知会为每个线程创建属于自己的Values对象。再看与set对应的get函数:

    public T get() {
        // Optimized for the fast path.
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values != null) {
            Object[] table = values.table;
            int index = hash & values.mask;
            if (this.reference == table[index]) {
                return (T) table[index + 1];
            }
        } else {
            values = initializeValues(currentThread);
        }

        return (T) values.getAfterMiss(this);
    }

get函数是通过获得当前线程的ThreadLocal.Values,找到对应的存储在values.table里的数据的。所以这就达到了各个线程的数据相互独立的目的,这就是所谓的线程局部变量。
接着来看ThreadLocal.Values是个什么东西:

        Values() {
            initializeTable(INITIAL_SIZE);//INITIAL_SIZE的值是16
            this.size = 0;//有效元素个数
            this.tombstones = 0;//“废弃”元素个数
        }

        private void initializeTable(int capacity) {
            this.table = new Object[capacity * 2];//capacity 默认为16
            this.mask = table.length - 1;//mask 默认是31,就是2^n-1,这里的n指位数。转换成二进制就明白了11111,也就是这个掩码用来取最后的五个位
            this.clean = 0;//这个是用来保存下一个清除的位置
            this.maximumLoad = capacity * 2 / 3; // 2/3 //maximumLoad 最大元素保存数(包括“废弃”加上有效元素) 默认是10。由于要取偶数,所以总容量需要除以2再减1,也就是默认15,但为什么是除以3,也就是10,这个没有理解。
        }

new 了一个Values对象,并且初始化了一个16*2的object数组table,然后设置了相关的变量。size指的是当前table中有效元素的个数,tombstones是墓碑的意思,在这里代表已经被移除掉的元素。size+tombstones的个数要小于maximumLoad。table是一个object数组,这里是当做map来用。mask是用来计算元素保存的下标的,是一个掩码。clean是用来记录下一个清除的位置的。maximumLoad就是总的存储元素的阀值。

最后调用了Values.set函数values.put(this, value);:

        void put(ThreadLocal<?> key, Object value) {
            cleanUp();

            // Keep track of first tombstone. That's where we want to go back
            // and add an entry if necessary.
            int firstTombstone = -1;//用来记录第一个“墓碑”的位置,即第一个被“废弃”的位置

            for (int index = key.hash & mask;; index = next(index)) {
                Object k = table[index];

                if (k == key.reference) { // ..........<1>
                    // Replace existing entry.
                    table[index + 1] = value;//覆盖原来的value
                    return;
                }

                if (k == null) {
                    if (firstTombstone == -1) {// ..........<2>
                        // Fill in null slot.
                        table[index] = key.reference;
                        table[index + 1] = value;
                        size++;
                        return;
                    }
                        //...........<3>
                    // Go back and replace first tombstone
                    table[firstTombstone] = key.reference;
                    table[firstTombstone + 1] = value;
                    tombstones--;
                    size++;
                    return;
                }

                // Remember first tombstone.
                if (firstTombstone == -1 && k == TOMBSTONE) {//...........<4>
                    firstTombstone = index;//记住第一个“墓碑”的位置
                }
            }
        }

1、第一次调用这个put函数时,Object k = table[index];这里的k取出来的是null,且firstTombstone的值为-1,所以走的是<2>。table的长度一定是2的倍数,原因在这里可以看出:数组的前一个单元存储的是key,后一个单元存储的是value。由此可见,存储一组数据需要两个单元。

另外这里的key存储的是一个弱引用key.reference,这样GC的时候可以直接回收该对象。

2、当再次调用put函数时,如果key中指向的弱引用指向的对象并没有被回收,就会走<1>,这时候就是覆盖原来的value.

3、当调用了Values.remove函数后:

        void remove(ThreadLocal<?> key) {
            cleanUp();

            for (int index = key.hash & mask;; index = next(index)) {//key.hash & mask: mask是31即2^n - 1也即11111,也就是即key.hash的后n位,这里默认n=5,这里的目的是为了将数据尽量分散的存储在数组当中
                Object reference = table[index];

                if (reference == key.reference) {
                    // Success!
                    table[index] = TOMBSTONE;//将key置为了TOMBSTONE
                    table[index + 1] = null;//将value置为null
                    tombstones++;//“墓碑”增加
                    size--;//有效元素减少
                    return;
                }

                if (reference == null) {
                    // No entry found.
                    return;
                }
            }
        }

key.hash & mask: mask是31即2^n - 1也即11111,也就是即key.hash的后n位,这里默认n=5,这里的目的是为了将数据尽量分散的存储在数组当中。0x61c88647据说是个很神奇的数,产生的数字分布很均匀。这里用来构造hash表。

可以看出将key置为了TOMBSTONE,value置为null,所以k == TOMBSTONE成立,如果是第一个元素的话,firstTombstone == -1成立,所以在调用remove后,再调用put时就走<4>。
并接着循环遍历,直到k == null,即一个空闲的单元。然后走<3>,将数据放在第一个被“废弃”的位置,并结束遍历。(为什么?难道说这个table里一定要空一个位置出来?没理解!)

接着往下看。在remove的第一行代码,调用了cleanUp()函数。
cleanUp()函数在最开始调用了rehash()函数,我们先看rehash()函数:

        private boolean rehash() {
            if (tombstones + size < maximumLoad) {//如果被“废弃”的加上有效元素小于阀值(默认是10),返回false
                return false;
            }

            int capacity = table.length >> 1;

            int newCapacity = capacity;

            if (size > (capacity >> 1)) {//如果有效元素的个数超过一半
                newCapacity = capacity * 2;//容量增加一倍
            }

            Object[] oldTable = this.table;

            // Allocate new table.
            initializeTable(newCapacity);//重新开壁了一个原来容量两倍的数组

            // We won't have any tombstones after this.
            this.tombstones = 0;// 将“废弃”数清0

            // If we have no live entries, we can quit here.
            if (size == 0) {
                return true;
            }

            // Move over entries.
            for (int i = oldTable.length - 2; i >= 0; i -= 2) {
                Object k = oldTable[i];
                if (k == null || k == TOMBSTONE) {//空单元和“废弃”的单元丢弃
                    // Skip this entry.
                    continue;
                }

                // The table can only contain null, tombstones and references.
                @SuppressWarnings("unchecked")
                Reference<ThreadLocal<?>> reference
                        = (Reference<ThreadLocal<?>>) k;
                ThreadLocal<?> key = reference.get();
                if (key != null) {//如果对象没有被回收
                    // Entry is still live. Move it over.
                    add(key, oldTable[i + 1]);//添加到新的数组当中
                } else {
                    // The key was reclaimed.
                    size--;//有效元素减少
                }
            }

            return true;
        }

可以看出rehash()是用来重新调整数组中的元素的,有必要的时候将容量扩大一倍(重新构建一个新的hash表).

我们来看看cleanUp()这个函数:

        private void cleanUp() {
            if (rehash()) {
                // If we rehashed, we needn't clean up (clean up happens as
                // a side effect).
                return;
            }

            if (size == 0) {
                // No live entries == nothing to clean.
                return;
            }

            // Clean log(table.length) entries picking up where we left off
            // last time.
            int index = clean;//clean默认为0
            Object[] table = this.table;
            for (int counter = table.length; counter > 0; counter >>= 1,
                    index = next(index)) {
                Object k = table[index];

                if (k == TOMBSTONE || k == null) {//空单元和“废弃”的单元跳过
                    continue; // on to next entry
                }

                // The table can only contain null, tombstones and references.
                @SuppressWarnings("unchecked")
                Reference<ThreadLocal<?>> reference
                        = (Reference<ThreadLocal<?>>) k;
                if (reference.get() == null) {//已经被回收的对象
                    // This thread local was reclaimed by the garbage collector.
                    table[index] = TOMBSTONE;//置为“废弃”单元
                    table[index + 1] = null;
                    tombstones++;
                    size--;
                }
            }

            // Point cursor to next index.
            clean = index;//指向下一个要清理的数据
        }

cleanUp函数的目的是对table做一个整理,交将已经回收的单元做一个标识,置为“废弃”单元,将将对应的值释放掉。

总结一下:
1、ThreadLocal之所以能达到“线程局部变量”的目的,是因为每个线程都有一个Thread.localValues变量,如果使用了ThreadLocal,这个变量会指向一个Values对象,这个Values对象就是线程独有的。

2、Values中有一个table的成员变量,table是一个Object数组,但是是以map的方式来存储的。偶数单元存储的是key,key的下一个单元存储的是对应的value,所以每存储一个元素,需要两个单元,这就是为什么容量一定是2的倍数。这里的key存储的是ThreadLocal实例的弱引用。

3、get 的时候是用的斐波拉契散列寻址的方式。(寻址的问题,后面再单独写一章)

最后记录一个点:
由于Values.table.key虽然是弱引用能够被GC所回收,但是Values本身被当前线程current thread所引用,于是Values.table.value所保存的对象并不能被GC所回收。只有当前thread结束以后, current thread就不会存在栈中,这个引用才会中断,才能被GC所回收。

一般的情况下,这种现象的存在,并不能叫做内存泄露,只能说内存的回收被delay了。
但是如果是在线程池的情况下,这种情况就比较严重了。因为线程池的情况下线程本身不会被销毁,而是返回到线程池中等待再次被启用,所以这个内存一直被占用,我们假设这个线程池有100个线程,而且保存在这个Values里的是图片类的大内存占用的数据的话,那这个内存就很可观了。

所以我们在使用完线程后,在交还回线程池之前,应该要调用threadlocal的remove函数,将不需要的数组中数据释放掉。

发布了112 篇原创文章 · 获赞 11 · 访问量 5万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览