热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

面试官问我:ThreadLocal的原理是什么,Looper对象为什么要存在ThreadLocal中?

面试官问我:ThreadLocal的原理是什么,Looper对象为什么要存在ThreadLocal中?-记得看文章三部曲,点赞,评论,转发。微信搜索【程序员小安】关注还在移动开

记得看文章三部曲,点赞,评论,转发。
微信搜索【程序员小安】关注还在移动开发领域苟活的大龄程序员,“面试系列”文章将在公众号同步发布。

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)”文章将在公众号同步发布。


推荐阅读
author-avatar
go800li
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有