Android ThreadLocal理解
2017-03-22 20:05
489 查看
Android ThreadLocal与Java ThreadLocal实现并不相同。
在Android消息循环一文中http://blog.csdn.net/zyfzhangyafei/article/details/62882117,提到了ThreadLocal,这个叫做 线程局部变量 的东西。
看一个实例:
然后编译: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:
就是通过种方式来实现的。
我们就从这个使用过程来跟踪它的实现和原理。
在Looper.java中:
static final ThreadLocal sThreadLocal = new ThreadLocal();
定义了一个静态的最终的变量sThreadLocal
然后在Loop.prepare中,new了一个Looper,并设置进sThreadLocal中:
ThreadLocal.set函数:
这里的values 是返回了current.localValues:
这个current.localValues定义在Thread.java 中
在最初的时候values是null,所以调用initializeValues函数:
这里new 的Values对象,被赋给了current.localValues,结合前面values(Thread current)函数,可知会为每个线程创建属于自己的Values对象。再看与set对应的get函数:
get函数是通过获得当前线程的ThreadLocal.Values,找到对应的存储在values.table里的数据的。所以这就达到了各个线程的数据相互独立的目的,这就是所谓的线程局部变量。
接着来看ThreadLocal.Values是个什么东西:
new 了一个Values对象,并且初始化了一个16*2的object数组table,然后设置了相关的变量。size指的是当前table中有效元素的个数,tombstones是墓碑的意思,在这里代表已经被移除掉的元素。size+tombstones的个数要小于maximumLoad。table是一个object数组,这里是当做map来用。mask是用来计算元素保存的下标的,是一个掩码。clean是用来记录下一个清除的位置的。maximumLoad就是总的存储元素的阀值。
最后调用了Values.set函数values.put(this, value);:
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函数后:
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()函数:
可以看出rehash()是用来重新调整数组中的元素的,有必要的时候将容量扩大一倍(重新构建一个新的hash表).
我们来看看cleanUp()这个函数:
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函数,将不需要的数组中数据释放掉。
在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函数,将不需要的数组中数据释放掉。
相关文章推荐
- 理解Android中ThreadLocal的工作原理
- Android -理解ThreadLocal、Looper
- 【Android消息处理机制】正确理解ThreadLocal(一)
- Android ThreadLocal理解--续篇
- 理解Android之ThreadLocal
- 理解ThreadLocal
- android的文件系统结构及其引导过程的初步理解
- 理解ThreadLocal
- android的文件系统结构及其引导过程的初步理解
- Android面试之---谈谈你对Android NDK的理解.
- 理解ThreadLocal
- 通通透透理解ThreadLocal(转)
- 理解Threadlocal
- Android中级教程之--------Android应用程序的生命周期(一定要理解,面试会问的哦!)
- 对AndroidManifest.xml的一点理解
- android的文件系统结构及其引导过程的初步理解
- 正确理解ThreadLocal
- 理解ThreadLocal
- 理解ThreadLocal
- 对ThreadLocal的理解