您的位置:首页 > 移动开发 > Android开发

Android ThreadLocal理解

2017-03-22 20:05 489 查看
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函数,将不需要的数组中数据释放掉。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息