Android源码之ThreaLocal

ThreadLocal提供了一种线程操作数据的思想。以前我以为ThreadLocal的实现就是有一个以线程的name为关键字的HashMap,每次请求数据的时候在这个HashMap中查找数据。今天读完代码,我才明白原来是用一种更巧妙的方法实现的。
下面我们就分析一下ThreadLocal的源码实现,
首先我们先看一下ThreadLocal的相关成员变量以及构造函数:

/** Weak reference to this thread local instance. */
    private final Reference<ThreadLocal<T>> reference = new WeakReference<ThreadLocal<T>>(this);
在这里可以将reference理解为关键字,下面我们可以看到reference的用处

    private static AtomicInteger hashCounter = new AtomicInteger(0);
    private final int hash = hashCounter.getAndAdd(0x61c88647 * 2);
hash用于计算hash值,hash值就是为了计算索引地址

构造函数如下:
public ThreadLocal() {}
ThreadLocal的构造函数时一个空函数。

在ThreadLocal类中有一个static的内部类Values. Values类提供了存储和索引具体的值的逻辑
我们也来看一下Values的成员变量以及构造函数:
private static final int INITIAL_SIZE = 16;
初始的存放数据的数组的大小
private static final Object TOMBSTONE = new Object();
TOMBSTONE是一个占位符,表示一个墓碑
private Object[] table;
保存数据的数组
private int mask;
用于计算索引值的掩码
private int size;
size表示有效的数据的个数
private int tombstones;
tombstones译为墓碑,表示索引值的数据无效,但是曾经被使用过的数据的个数
private int maximumLoad;
maximumLoad表示最大的装载数,也就是size+tombstones的最大值
private int clean;
clean记录了下次执行cleanup操作的起始位置

Values只提供了默认构造函数,实现如下:
Values() {
initializeTable(INITIAL_SIZE);
this.size = 0;
this.tombstones = 0;
}
private void initializeTable(int capacity) {
this.table = new Object[capacity * 2];
this.mask = table.length - 1;
this.clean = 0;
this.maximumLoad = capacity * 2 / 3; // 2/3
}
构造函数只是提供了成员变量的初始化。在实现中我们看到,将mask掩码初始化为table的长度-1, 这样在计算索引值时,保证索引值落在table里面。将maximumLoad初始化为table的长度的2/3.

(1)set函数
接下来我们看一下ThreadLocal的set函数的实现,set函数将值设置到线程中去
public void set(T value) {
//得到当前的运行线程
        Thread currentThread = Thread.currentThread();
//获得运行线程的Values值
        Values values = values(currentThread);
//如果values值为空,则说明还没有为该线程设置过值,这时需要初始化一个空的values对象
        if (values == null) {
            values = initializeValues(currentThread);
        }
//设置值
        values.put(this, value);
    }

values函数的实现如下:
Values values(Thread current) {
        return current.localValues;
    }
我们可以看到,values函数就是返回了线程的localValues成员变量。
在Thread类中,存在ThreadLocal.Values localValues的成员变量,有着包访问权限。从这里我们可以看出,ThreadLocal的值是绑定到各个线程中去的,而不是在ThreadLocal里统一管理。

initializeValues函数的实现也很简单,如下:
Values initializeValues(Thread current) {
        return current.localValues = new Values();
    }
只是简单的初始化Thread的localValues成员变量new一个新Values对象。

接下来我们分析一下Values类的put函数的实现:
void put(ThreadLocal<?> key, Object value){
//一上来就执行cleanUp操作
            cleanUp();

//记录第一个墓碑的位置
            int firstTombstone = -1;

//遍历表
            for (int index = key.hash & mask;; index = next(index)) {
//得到关键字
                Object k = table[index];

//查找到关键字,则替换值
                if (k == key.reference) {
                    // Replace existing entry.
                    table[index + 1] = value;
                    return;
                }

//如果当前位置的关键字是空,说明查找到一个没有使用过的位置,这时候就必须将该值保存
                if (k == null) {
//firstTombstone == -1, 说明在查找过程中没有遇到墓碑,则将该值保存在这个null位置
                    if (firstTombstone == -1) {
                        // Fill in null slot.
//保存key和value
                        table[index] = key.reference;
                        table[index + 1] = value;
//有效的数据个数加1
                        size++;
                        return;
                    }

                    //firstTombstone != -1,说明查找过程中遇到过墓碑,则要将该值替换遇到的第一个墓碑
//保存key和value
                    table[firstTombstone] = key.reference;
                    table[firstTombstone + 1] = value;
//墓碑的个数减1
                    tombstones--;
//有效的个数加1
                    size++;
                    return;
                }

                //保存第一个墓碑的位置
                if (firstTombstone == -1 && k == TOMBSTONE) {
                    firstTombstone = index;
                }
            }
}
从put的实现逻辑中,我们看到Values是想一个位置保存key,在这个位置的下一个位置处保存value。这就要求table的长度必须为2的整数倍,并且计算索引的步长为2。
我们从next函数的视线中验证了这一点:
private int next(int index) {
return (index + 2) & mask;
}

我们接下来看一下cleanUp函数的实现逻辑:
private void cleanUp() {
if (rehash()) {
//如果重新hash了table,则没有必要在进行cleanUp操作
                return;
            }

//size = 0, 表示还没有保存有效的数据
            if (size == 0) {
                // No live entries == nothing to clean.
                return;
            }

            // 从上次执行cleanUp结束时的索引值开始
            int index = clean;
            Object[] table = this.table;

//遍历table
            for (int counter = table.length; counter > 0; counter >>= 1, index = next(index)) {
//得到key
                Object k = table[index];

                if (k == TOMBSTONE || k == null) {
                    continue; // on to next entry
                }


                @SuppressWarnings("unchecked")
                Reference<ThreadLocal<?>> reference = (Reference<ThreadLocal<?>>) k;
//如果为null,表示此对象已经被回收
                if (reference.get() == null) {
                    //将该位置设置成墓碑,并将值设置为null
                    table[index] = TOMBSTONE;
                    table[index + 1] = null;
//增加墓碑的数目和减少有效的对象数目
                    tombstones++;
                    size--;
                }
            }

            //保存下次cleanUp的其实索引值
            clean = index;
    }
从源码中可以看出,cleanUp主要清理了被垃圾回收器回收的对象。这样能保证每次取数据和设置数据时索引位置的有效性。
我们看一下rehash的函数实现:
private boolean rehash() {
//如果没有达到可装载的最大值,则不用重新hash,直接返回false
            if (tombstones + size < maximumLoad) {
                return false;
            }

            int capacity = table.length >> 1;

            int newCapacity = capacity;

//如果一半的容量都被有效的值所占据,则将容量扩大一倍。因为table是将关键字和值保存在相邻的位置上,所以这里其实比较的是table数组的长度的1/4
            if (size > (capacity >> 1)) {
                newCapacity = capacity * 2;
            }

            Object[] oldTable = this.table;

            //重新分配一个新的table数组
            initializeTable(newCapacity);

       
            this.tombstones = 0;

            //如果有效的值的个数为0, 则没必要进行新旧table的赋值
            if (size == 0) {
                return true;
            }

            //进行新旧table的赋值
            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) {
                    //该值是有效的,则加入到新的table中
                    add(key, oldTable[i + 1]);
                } else {
                    //该关键字对象已经被回收
                    size--;
                }
            }

            return true;
}
add函数是将值赋值到table数组中。在遍历table的过程中,忽略掉当前索引位置的关键字是墓碑的情况,保证插入到null的位置
void add(ThreadLocal<?> key, Object value) {
for (int index = key.hash & mask;; index = next(index)) {
Object k = table[index];
            if (k == null) {
table[index] = key.reference;
table[index + 1] = value;
return;
}
}
}

(2)get()函数
get()函数负责取出已经保存的值。实现如下:
public T get() {
        //得到当前线程的Values对象
        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成员
            values = initializeValues(currentThread);
        }

//在table里没有查找到
        return (T) values.getAfterMiss(this);
    }

我们看一下getAfterMiss的实现:
Object getAfterMiss(ThreadLocal<?> key) {
//保存当前的数据table
            Object[] table = this.table;
//计算索引值
            int index = key.hash & mask;

            //如果当前关键字为null, 则说明没有保存过该值,则没有继续查找下去的必要
            if (table[index] == null) {
//初始化一个值
                Object value = key.initialValue();

                //如果在initialValue过程中,没有改变table
                if (this.table == table && table[index] == null) {
//将关键字和值 保存
                    table[index] = key.reference;
                    table[index + 1] = value;
                    size++;
//执行一次cleanUp操作
                    cleanUp();
                    return value;
                }

                //在initialValue的过程中,改变了table
//put到table中去
                put(key, value);
                return value;
            }

            int firstTombstone = -1;

            //继续查找
            for (index = next(index);; index = next(index)) {
                Object reference = table[index];
//如果关键字相等
                if (reference == key.reference) {
                    return table[index + 1];
                }

//如果当前索引的关键字为null,则表明当前位置还没有被使用过,则没有必要在查找下去
                if (reference == null) {
//初始化一个值
                    Object value = key.initialValue();

                    //如果在initialValue过程中,没有改变table
                    if (this.table == table) {
//如果我们曾经遇到过墓碑,并且现在那个位置还是个墓碑,则将值赋值到第一个墓碑的位置
                        if (firstTombstone > -1 && table[firstTombstone] == TOMBSTONE) {
                            table[firstTombstone] = key.reference;
                            table[firstTombstone + 1] = value;
                            tombstones--;
                            size++;

                            // No need to clean up here. We aren't filling
                            // in a null slot.
                            return value;
                        }

//当前索引的位置还是null,则赋值到当前的位置
                        if (table[index] == null) {
                            table[index] = key.reference;
                            table[index + 1] = value;
                            size++;

                            cleanUp();
                            return value;
                        }
                    }

                    //在initialValue的过程中,改变了table
                    put(key, value);
                    return value;
                }

//记录第一个墓碑的位置
                if (firstTombstone == -1 && reference == TOMBSTONE) {
                    firstTombstone = index;
                }
            }
}
在ThreadLocal的实现中,initialValue只是简单的返回一个null,等待子类去实现。
protected T initialValue() {
        return null;
    }
至此,我们分析了set和get函数的实现。
ThreadLocal还实现了remove的逻辑,我们接下来来分析一下remove的实现
(3)remove()函数
remove函数的源码如下:
public void remove() {
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values != null) {
            values.remove(this);
        }
    }
得到当前线程对象的values成员之后,调用Values的remove函数即可。Values的remove函数实现如下:
void remove(ThreadLocal<?> key) {
cleanUp();

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

                if (reference == key.reference) {
//如果关键字相等,则将该位置标记为墓碑。
                    table[index] = TOMBSTONE;
                    table[index + 1] = null;
                    tombstones++;
                    size--;
                    return;
                }

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

至此,我们完整的分析了ThreadLocal的实现。我们思考一下问题:
为什么要使用一个叫墓碑的占位符呢?
我们知道在table数组中,每一个位置可以存放的对象要么等于null,要么是一个要保存的值。我们只要在按顺序遍历table,总会找到一个位置可以保存数据。如果要删除一个数据,只要将该位置设置为null。这样当查找到一个位置为null的时候,就可以知道这个位置肯定是没有被使用的位置,因此也就没有必要再查找下去了。那为什么还有多引入一个墓碑的对象呢?看似这个墓碑是多余的,其实不然!因为计算每个线程的索引值的时候,每个线程的索引值其实是固定的,每次都会映射到相同的位置。这样有了墓碑这个占位符对象之后,能保证别的对象不会使用这个位置,相当于将这个位置保护起来,以便留给特定的线程去使用。

从ThreadLocal的实现中,我们总结到,通过ThreadLocal.set()到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。






猜你喜欢

转载自zzu-007.iteye.com/blog/2261553