Android ThreadLocal理解

来源:互联网 发布:windows开发是什么工作 编辑:程序博客网 时间:2024/05/16 18:02

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函数,将不需要的数组中数据释放掉。

0 0
原创粉丝点击