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<ThreadLocal>> { /** 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<ThreadLocal>> { /** 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吧。