背景
突然被人问到这个问题,心想不是很简单吗!但是他又问你提到ThreadMap,哪ThreadMap怎么解决hash冲突的!这还真把我问住了。
原理
抱着虚心学习,不断遗忘不断再学习的态度,咱们又来看一次源码。
1、Looper是怎么创建的
在Android里面最常见的就是我们主线程的Looper了,主线程的Looper是怎么创建?
我们在从桌面点击一个图标,实际上是通过launchActivity通过startActivity启动Activity的,
会一直走到instrumentation.execStartactivity,然后在里面会跨进程调用ActivityTaskManager的startActivity,然后通过AMS去和Zygote进程进行socket通信,然后会foker一个进程,在这个进程里面反射执行ActivityThrea的mian方法。我们来看看这里有啥
ActiivityThread.java
我们可以看到很明显的调用了
//初始化Lopper
Looper.prepareMainLooper()
//继续调用了Looper的prepare。
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
// 我们看到了代码进入ThreadLocal
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));
}
在上面的代码中我们都是通过sThreadLocal 来获取Looper的,如果已经存在了Looper就会抛出异常。
//一个静态变量
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
我们继续查看它的get()方法
//get 方法也很简单,没有特别复杂的操作
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
这里由于我们是第一次进来map肯定是为空的,所以我们肯定会调用setInitialValue方法
我们来看看里面有啥
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
这里还是会去判断这个map,我们去看看这个map到底是啥。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
很明显这个map就是一个线程对象的成员变量,一个简单的变量没啥大不了的,不过它的类型为ThreadLocalMap,我们看看这个TreaLocalMap和普通map区别。
static class ThreadLocalMap {
//我们可以看到他的Key是一个弱引用!
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two
* 这里也很好理解,必须为2的倍数
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* 这里是容器
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
//一个构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
和普通map区别也不大,里面是一个tab[]用来保存键值对,不过这个Entry的键值对他是一个弱引用,需要值得关注,我们知道GC回收的时候,会对软应用持有的对象进行一次回收。所以这里如果key被回收了,value就是无效的,但是这个map又被线程一直持有,所以会造成内存泄露。
了解这个Map的结构之后我们再来看这个
/////////////////////------ThreadLocal.java-----------------
private T setInitialValue() {
T value = initialValue(); // 这里就是一个null值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
//调用构造函数
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
这时候就直接调用构造函数,我们知道了会构造一个map,并且执行一次hash,把value设置为null。
到这里我们就知道为什么只能保证一个了,因为Looper的创建只能是通过Looper.prepare()或者时Looper.prepareMain(),就会给调用这个方法的线程创建一个Looper,当你想在创建的时候会判断当前线程对象的变量ThreadLocalMap,是否包含了Looper的Value,如果存在会抛出异常。这里的key就是sTrheadLocal。
这时候我们再回答ThreadLocalMap怎么解决hash冲突的?
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
可以清楚的看到源码里面是通过线性探测来解决的。不是很麻烦。
一般解决hash冲突有三种方法:线性探测,再hash,以及链表法。
小贴士
线性探测是解决hash冲突的一种方式,比如一个key 的hash值为21,一个key的hash值为11,他们在对数组长度11 (key.threadLocalHashCode & (len-1);)取模的时候,都会定位到1这个位置。
比如先put ke为21这个发现1这个位置没有值,直接就放入了,这时候我们再放入11这个key,他会发现1这个位置已经有值了,他会将下标+1,变为2这个位置,没有值,直接放入。
我们从里面取出元素的时候,比如我们先get 11这个key,直接定位到1这个位置,发现这时候我们不能直接获取value,先要比对equal方法,相等直接返回value,否则需要将下标加1比对下一个位置。