记得看文章三部曲,点赞,评论,转发。
微信搜索【程序员小安】关注还在移动开发领域苟活的大龄程序员,“面试系列”文章将在公众号同步发布。1.前言
最近看到网络上都说现在内卷化严重,面试很难,作为颜值担当的天才少年_也开始了面试之路,既然说面试官各个都是精锐,很不巧,老子打的就是精锐。
2.正文
天才少年_信心满满的来到某东的会议室,等待面试,决定跟他们好好切磋一翻。
小伙子,我是今天的面试官,看我的发型你应该知道我的技术有多强了,闲话不多说了,Looper对象使用ThreadLocal来保证每个线程有唯一的Looper对象,并且线程之间互不影响,这个知道吧,那么我们来聊聊ThreadLocal吧。
果然是精锐,这么直接,毫无前戏,看来得拿出真本领了。
ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本,从而实现线程隔离。
那给我讲讲Looper中是如何使用ThreadLocal的?
说这么多原来还是聊Looper的源码,哈哈,这可是我的强项。
如下是Looper类关于ThreadLocal的主要代码行
1)初始化ThreadLocal:
// sThreadLocal.get() will return null unless you've called prepare().
static final ThreadLocal sThreadLocal = new ThreadLocal();
2)调用set方法可以存储当前线程的Looper对象,调用get方法获取当前线程的Looper对象:
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));
}
嗯,小伙子看来对Looper很熟悉,既然内卷,那我肯定不问Looper,我们来聊聊ThreadLocal的原理。
就知道会这么问,还好,那晚我跟小韩一起在办公室看源码,她偷偷告诉我她有了我的孩子。
不对,那晚好像是我一个人看源码的,不管了,我努力的回忆着ThreadLocal的源码。
1)我们先看看ThreadLocal的set方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
可以看到先获取到当前线程t,随后通过getMap方法获取ThreadLocalMap对象,把value塞到ThreadLocalMap对象中,继续跟到getMap方法:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
这边就是从Thread对象中获取到threadLocals变量,让我们来看看threadLocals是什么,直接定位到Thread类中:
class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
......
}
到这里是不是豁然开朗,原来每个Thread内部都有一个ThreadLocalMap对象,用来存储Looper。这样,每个线程在存储Looper对象到ThreadLocal中的时候,其实是存储在每个线程内部的ThreadLocalMap对象中,从而其他线程无法获取到Looper对象,实现线程隔离。
既然已经说到这里了,那给我讲讲ThreadLocalMap吧。
问吧,反正那晚很漫长,我们一起除了看源码,也没有做其他的事情,至于孩子怎么来的,我只能说我是个老实人,我什么都不知道。
1)先看下ThreadLocalMap的构造函数和关键成员变量:
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
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);
}
2)通过Entry[] table可以知道,虽然他叫做ThreadLocalMap,但是底层竟然不是基于hashmap存储的,而是以数组形式。呸,渣男,表里不一。
那我们就不看他的外表了,去看看他的内在,Entry的定义如下:
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
可以看到,虽然他不是以hashmap的形式存储,但是Entry对象里面也是设计成key/value的形式解决hash冲突的。所以你可以想象成ThreadLocalMap是个数组,而存储在数组里面的各个对象是以key/value形式的Entry对象。
不好意思,打断一下,这边我有几个问题想问下,第一个是为什么要设计成数组?
这种问题还问,我们中台返回数据给客户端的时候,不全是凭心情吗,明明就只返回一个对象,他非要返回一个数组,这tm我怎么知道为什么要这么设计,可能写ThreadLocalMap的工程师是我们中台的同学吧,哈哈。
抱怨归抱怨,我大脑开始疯狂运转,这得从ThreadLocal的set方法说起,那我们继续深入看set方法吧:
1)ThreadLocal的set方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
2)上面已经讲过,set方法是先获取到当前线程t,随后通过getMap方法获取ThreadLocalMap对象,然后把this作为key,Looper作为value塞到ThreadLocalMap对象中,this是什么,就是当前类对象呗,也就是ThreadLocal,到这里,我应该能够解答糟老头子,不对,是面试官的问题了,ThreadLocalMap设计成数组,肯定是有些线程里面不止一个ThreadLocal对象,可能会初始化多个,这样存储的时候就需要数组了。
为了弄清楚,ThreadLocalMap是如何存储的,我们继续看下ThreadLocalMap的set方法,谁让咱是个好奇心很重的人呢。
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;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
代码量不大,int i = key.threadLocalHashCode & (len-1);这段代码我相信经常面试头条的同学应该不陌生(面试必问题目,hashmap的源码)这段代码跟hashmap中key的hash值的计算规则一致,目的就是为了解决hash冲突,寻找数组插入下标的。
再往下是个for循环,里面是寻找可插入的位置,如果需要插入的key在数组中已存在,则直接把需要插入的value覆盖到数组中的vaule上:
if (k == key) {
e.value = value;
return;
}
如果key为空,则创建出Entry对象,放在该位置上:
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
如果上面两种情况都不满足,那就寻找下一个位置i,继续循环上面的两个判断,直到找到可以插入或者刷新的位置。
e = tab[i = nextIndex(i, len)]
那顺便把get方法也讲下吧。
服务肯定会全套,不用你问,我也会讲get方法的逻辑,这是咱技工(技术工种)的职业操守。
1)ThreadLocal的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();
}
跟set方法类似,先获取到当前线程t,随后通过getMap方法获取ThreadLocalMap对象,再通过getEntry获取到Enety对象:
2)getEntry方法如下所示:
private Entry getEntry(ThreadLocal> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
int i = key.threadLocalHashCode & (table.length - 1);又是非常熟悉的代码,通过该方法获取到数组下标i,如果该位置的Entry对象中的key跟当前的TreadLocal一致,则返回该Entry对象,否则继续执行getEntryAfterMiss方法:
private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
代码很容易理解,开启循环查找,如果当前ThreadLocal跟数组下标i对应的Entry对象的key相等,则返回当前Entry对象;
如果数组下标I对应的Entry对象的key为空,则执行expungeStaleEntry(i)方法,从方法命名就知道,删除废弃的Entry对应,其实就是做了次内存回收,expungeStaleEntry源码如下所示:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
我们主要看如下几行代码:
if (k == null) {
e.value = null;
tab[i] = null;
这个方法其实实现的功能就是,如果数组中,某个Entry对象的key为空,该方法会释放掉value对象和Entry对象。
再回到上面,如果ThreadLocal跟数组下标i对应的Entry对象的key既不相等,也不为空,则调用nextIndex方法,向下查找,跟set方法的nextIndex方法一致。
嗯,小伙可以啊,ThreadLocal理解算比较透彻了,但是既然你过来打精英,那咱们就再深入一点,聊聊为什么Entry对象要key设置成弱引用呢?还有ThreadLocal是否存在内存泄露呢?
传统面试其实讲究点到为止,点到为止我就通过了,如果我使劲吹牛逼,一下就能把他忽悠懵逼。这个年轻人不讲面德,来!骗!来!内卷我一个老客户端,这好吗?这不好,我劝,这位面试官,耗子尾汁,好好反思,以后不要再出这种面试题,IT人应该以和为贵,谢谢!
既然来面试,我肯定是跟小韩单独相处了好几个夜晚,不对,是看了好几个夜晚的源码。
让我们再回顾下Entry的构造函数:
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
从构造函数可以看到,Entry对象中的key,即ThreadLocal对象为弱引用,为了再秀一把技术,我先普及下弱引用的定义吧:
弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
接下来的这段话要仔细读几遍哦,画重点啦。
key如果不是WeakReference弱引用,则如果某个线程死循环,则ThreadLocalMap一直存在,引用住了ThreadLocal,导致ThreadLocal无法释放,同时导致value无法释放;当是WeakReference弱引用时,即使线程死循环,当创建ThreadLocal的地方释放了,ThreadLocalMap的key会同样被被释放,在调用getEntry时,会判断如果key为null,则会释放value,内存泄露则不存在。当然ThreadLocalMap类也提供remove方法,该方法会帮我们把当前ThreadLocal对应的Entry对象清除,从而不会内存泄露,所以如果我个人觉得如果每次在不需要使用ThreadLocal的时候,手动调用remove方法,也不存在内存泄露。
嗯,不错不错,深度挖得差不多了,我们再回到表明来,说说为什么Looper对象要存在ThreadLocal中,为什么不能公用一个呢,或者每个线程持有一个呢?
果然是资深面试官,问题由浅入深,再回到问题本质中来,这技术能力,对得起他那脱落的毛发。
首先,个人觉得,技术上,Looper对象可以公用一个全局的,即每个线程公用同一个Looper对象,但是为了线程安全,我们就要进行线程同步处理,比如加同步锁,这样运行效率会降低,另外一方面Andriod系统如果5秒内没有处理Looper消息,则会造成ANR,加同步锁会增加ANR的几率。
至于为什么不每个线程都持有一个Looper对象呢,这个也很好理解:为了节约内存。
如果你就只有2个线程,其实用不用ThreadLocal感觉不到优势,如果要初始化1000个线程,每个线程都初始化Looper对象的话,那么就会存在1000个Looper对象,造成很大的内存开销,而且我们知道,多线程时,往往会把线程加入线程池,比如:
ExecutorService threadPool = Executors.newFixedThreadPool(8);
用线程池的好处就是线程复用,如上的代码,只会实例出8个线程,而ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每个线程持有一个Looper对象,也就会初始化出8个Looper对象,很明显,用ThreadLocal节省了内存。
可以了,你对ThreadLocal的了解比较全面了,把我打动了,回去等offer吧。
微信搜索【程序员小安】“面试系列(java&andriod)”文章将在公众号同步发布。