关于后端:Java面试必问ThreadLocal终极篇-淦

开场白

张三最近天气很热情绪不是很好,所以他决定进来面试跟面试官聊聊天排解一下,后果刚投递简历就有人约了面试。

我丢,什么状况怎么刚投递进来就有人约我面试了?诶。。。真烦啊,哥曾经不在江湖这么久了,江湖还是有哥的传说,我还是这么热门的么?太懊恼了,帅无罪。

暗自窃喜的张三来到了某东现场面试的办公室,我丢,这面试官?不是吧,这满是划痕的Mac,这发量,难道就是传说中的架构师?

张三的心态一下子就崩了,进去第一场面试就遇到一个顶级面试官,这谁顶得住啊。

你好,我是你的面试官Tony,看我的发型应该你能猜到我的身份了,我也话不说,咱们间接开始好不好?看你简历写了多线程,来你跟我聊一下ThreadLocal吧,我很久没写代码不太熟悉了,你帮我回顾一下。

我丢?这TM是人话?这是什么逻辑啊,说是问多线程而后一上来就来个这么冷门的ThreadLocal?心态崩了呀,再说你TM本人忘了不晓得上来看看书么,来我这里找答案是什么鬼啊…

只管非常不愿意,然而张三还是高速运转他的小脑袋,回忆起了ThreadLocal的种种细节…

面试官说实话我在理论开发过程中用到ThreadLocal的中央不是很多,我在写这个文章的时候还刻意去把我电脑上几十个我的项目关上之后去全局搜寻ThreadLocal发现除了零碎源码的应用,很少在我的项目中用到,不过也还是有的。

ThreadLocal的作用次要是做数据隔离,填充的数据只属于以后线程,变量的数据对别的线程而言是绝对隔离的,在多线程环境下,如何避免本人的变量被其它线程篡改。

你能跟我说说它隔离有什么用,会用在什么场景么?

这,我都说了我很少用了,还问我,好受了呀,哦哦哦,有了想起来了,事务隔离级别。

面试官你好,其实我第一工夫想到的就是Spring实现事务隔离级别的源码,这还是过后我大学被女朋友甩了,一个人在图书馆哭泣的时候无意间发现的。

Spring采纳Threadlocal的形式,来保障单个线程中的数据库操作应用的是同一个数据库连贯,同时,采纳这种形式能够使业务层应用事务时不须要感知并治理connection对象,通过流传级别,奇妙地治理多个事务配置之间的切换,挂起和复原。

Spring框架外面就是用的ThreadLocal来实现这种隔离,次要是在TransactionSynchronizationManager这个类外面,代码如下所示:

private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);

    private static final ThreadLocal<Map<Object, Object>> resources =
            new NamedThreadLocal<>("Transactional resources");

    private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
            new NamedThreadLocal<>("Transaction synchronizations");

    private static final ThreadLocal<String> currentTransactionName =
            new NamedThreadLocal<>("Current transaction name");

  ……

Spring的事务次要是ThreadLocal和AOP去做实现的,我这里提一下,大家晓得每个线程本人的链接是靠ThreadLocal保留的就好了,持续的细节我会在Spring章节细说的,暖么?

除了源码外面应用到ThreadLocal的场景,你本人有应用他的场景么?个别你会怎么用呢?

来了来了,加分项来了,这个我还真遇到过,装B的机会终于来了。

有的有的面试官,这个我会!!!

之前咱们上线后发现局部用户的日期竟然不对了,排查下来是SimpleDataFormat的锅,过后咱们应用SimpleDataFormat的parse()办法,外部有一个Calendar对象,调用SimpleDataFormat的parse()办法会先调用Calendar.clear(),而后调用Calendar.add(),如果一个线程先调用了add()而后另一个线程又调用了clear(),这时候parse()办法解析的工夫就不对了。

其实要解决这个问题很简略,让每个线程都new 一个本人的 SimpleDataFormat就好了,然而1000个线程难道new1000个SimpleDataFormat

所以过后咱们应用了线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的正本,从而解决了线程平安的问题,也进步了性能。

那……

还有还有,我还有,您别着急问下一个,让我再加点分,迁延一下面试工夫。

我在我的项目中存在一个线程常常遇到横跨若干办法调用,须要传递的对象,也就是上下文(Context),它是一种状态,常常就是是用户身份、工作信息等,就会存在过渡传参的问题。

应用到相似责任链模式,给每个办法减少一个context参数十分麻烦,而且有些时候,如果调用链有无奈批改源码的第三方库,对象参数就传不进去了,所以我应用到了ThreadLocal去做了一下革新,这样只须要在调用前在ThreadLocal中设置参数,其余中央get一下就好了。

before
  
void work(User user) {
    getInfo(user);
    checkInfo(user);
    setSomeThing(user);
    log(user);
}

then
  
void work(User user) {
try{
      threadLocalUser.set(user);
      // 他们外部  User u = threadLocalUser.get(); 就好了
    getInfo();
    checkInfo();
    setSomeThing();
    log();
    } finally {
     threadLocalUser.remove();
    }
}

我看了一下很多场景的cookie,session等数据隔离都是通过ThreadLocal去做实现的。

对了我面试官容许我再秀一下常识广度,在Android中,Looper类就是利用了ThreadLocal的个性,保障每个线程只存在一个Looper对象。

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<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));
}

面试官:我丢,这货怎么晓得这么多场景?还把Android都扯了进去,不是吧阿sir,上面我要考考他原理了。

嗯嗯,你答复得很好,那你能跟我说说他底层实现的原理么?

好的面试官,我先说一下他的应用:

ThreadLocal<String> localName = new ThreadLocal();
localName.set("张三");
String name = localName.get();
localName.remove();

其实应用真的很简略,线程进来之后初始化一个能够泛型的ThreadLocal对象,之后这个线程只有在remove之前去get,都能拿到之前set的值,留神这里我说的是remove之前。

他是能做到线程间数据隔离的,所以别的线程应用get()办法是没方法拿到其余线程的值的,然而有方法能够做到,我前面会说。

咱们先看看他set的源码:

public void set(T value) {
    Thread t = Thread.currentThread();// 获取以后线程
    ThreadLocalMap map = getMap(t);// 获取ThreadLocalMap对象
    if (map != null) // 校验对象是否为空
        map.set(this, value); // 不为空set
    else
        createMap(t, value); // 为空创立一个map对象
}

大家能够发现set的源码很简略,次要就是ThreadLocalMap咱们须要关注一下,而ThreadLocalMap呢是以后线程Thread一个叫threadLocals的变量中获取的。

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
public class Thread implements Runnable {
      ……

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  
     ……

这里咱们基本上能够找到ThreadLocal数据隔离的假相了,每个线程Thread都保护了本人的threadLocals变量,所以在每个线程创立ThreadLocal的时候,实际上数据是存在本人线程Thread的threadLocals变量外面的,他人没方法拿到,从而实现了隔离。

ThreadLocalMap底层构造是怎么样子的呢?

面试官这个问题问得好啊,心田暗骂,让我歇一会不行么?

张三笑着答复道,既然有个Map那他的数据结构其实是很像HashMap的,然而看源码能够发现,它并未实现Map接口,而且他的Entry是继承WeakReference(弱援用)的,也没有看到HashMap中的next,所以不存在链表了。

static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        ……
    }    

构造大略长这样:

稍等,我有两个疑难你能够解答一下么?

好呀,面试官你说。

为什么须要数组呢?没有了链表怎么解决Hash抵触呢?

用数组是因为,咱们开发过程中能够一个线程能够有多个TreadLocal来寄存不同类型的对象的,然而他们都将放到你以后线程的ThreadLocalMap里,所以必定要数组来存。

至于Hash抵触,咱们先看一下源码:

private void set(ThreadLocal<?> key, Object value) {
           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();
        }

我从源码外面看到ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,依据ThreadLocal对象的hash值,定位到table中的地位i,int i = key.threadLocalHashCode & (len-1)

而后会判断一下:如果以后地位是空的,就初始化一个Entry对象放在地位i上;

if (k == null) {
    replaceStaleEntry(key, value, i);
    return;
}

如果地位i不为空,如果这个Entry对象的key正好是行将设置的key,那么就刷新Entry中的value;

if (k == key) {
    e.value = value;
    return;
}

如果地位i的不为空,而且key不等于entry,那就找下一个空地位,直到为空为止。

这样的话,在get的时候,也会依据ThreadLocal对象的hash值,定位到table中的地位,而后判断该地位Entry对象中的key是否和get的key统一,如果不统一,就判断下一个地位,set和get如果抵触重大的话,效率还是很低的。

以下是get的源码,是不是就感觉很好懂了:

 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);
        }

 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
// get的时候一样是依据ThreadLocal获取到table的i值,而后查找数据拿到后会比照key是否相等  if (e != null && e.get() == key)。
            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;
        }

能跟我说一下对象寄存在哪里么?

在Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存能够了解成线程的公有内存,而堆内存中的对象对所有线程可见,堆内存中的对象能够被所有线程拜访。

那么是不是说ThreadLocal的实例以及其值寄存在栈上呢?

其实不是的,因为ThreadLocal实例实际上也是被其创立的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性批改成了线程可见。

如果我想共享线程的ThreadLocal数据怎么办?

应用InheritableThreadLocal能够实现多个线程拜访ThreadLocal的值,咱们在主线程中创立一个InheritableThreadLocal的实例,而后在子线程中失去这个InheritableThreadLocal实例设置的值。

private void test() {    
final ThreadLocal threadLocal = new InheritableThreadLocal();       
threadLocal.set("帅得一匹");    
Thread t = new Thread() {        
    @Override        
    public void run() {            
      super.run();            
      Log.i( "张三帅么 =" + threadLocal.get());        
    }    
  };          
  t.start(); 
} 

在子线程中我是可能失常输入那一行日志的,这也是我之前面试视频提到过的父子线程数据传递的问题。

怎么传递的呀?

传递的逻辑很简略,我在结尾Thread代码提到threadLocals的时候,你们再往下看看我刻意放了另外一个变量:

Thread源码中,咱们看看Thread.init初始化创立的时候做了什么:

public class Thread implements Runnable {
  ……
   if (inheritThreadLocals && parent.inheritableThreadLocals != null)
      this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  ……
}

我就截取了局部代码,如果线程的inheritThreadLocals变量不为空,比方咱们下面的例子,而且父线程的inheritThreadLocals也存在,那么我就把父线程的inheritThreadLocals给以后线程的inheritThreadLocals

是不是很有意思?

小伙子你懂的的确很多,那你算是一个深度的ThreadLocal用户了,你发现ThreadLocal的问题了么?

你是说内存泄露么?

我丢,这小子为啥晓得我要问什么?嗯嗯对的,你说一下。

这个问题的确会存在的,我跟大家说一下为什么,还记得我下面的代码么?

ThreadLocal在保留的时候会把本人当做Key存在ThreadLocalMap中,失常状况应该是key和value都应该被外界强援用才对,然而当初key被设计成WeakReference弱援用了。

我先给大家介绍一下弱援用:

只具备弱援用的对象领有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具备弱援用的对象,不论以后内存空间足够与否,都会回收它的内存。

不过,因为垃圾回收器是一个优先级很低的线程,因而不肯定会很快发现那些只具备弱援用的对象。

这就导致了一个问题,ThreadLocal在没有内部强援用时,产生GC时会被回收,如果创立ThreadLocal的线程始终继续运行,那么这个Entry对象中的value就有可能始终得不到回收,产生内存泄露。

就比方线程池外面的线程,线程都是复用的,那么之前的线程实例解决完之后,出于复用的目标线程仍然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。

依照情理一个线程应用完,ThreadLocalMap是应该要被清空的,然而当初线程被复用了。

那怎么解决?

在代码的最初应用remove就好了,咱们只有记得在应用的最初用remove把值清空就好了。

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("张三");
    ……
} finally {
    localName.remove();
}

remove的源码很简略,找到对应的值全副置空,这样在垃圾回收器回收的时候,会主动把他们回收掉。

那为什么ThreadLocalMap的key要设计成弱援用?

key不设置成弱援用的话就会造成和entry中value一样内存透露的场景。

补充一点:ThreadLocal的有余,我感觉能够通过看看netty的fastThreadLocal来补救,大家有趣味能够康康。

好了,你不仅把我问的都答复了,我不晓得的你甚至都说了,ThreadLocal你过关了,不过JUC的面试才刚刚开始,心愿你当前越战越勇,最初拿个好offer哟。

什么鬼,忽然这么煽情,不是很尴尬我的么?难道是为了锤炼我?难为巨匠这样为我着想,我还始终心里暗骂他,不说了回去好好学了。

总结

其实ThreadLocal用法很简略,外面的办法就那几个,算上正文源码都没多少行,我用了十多分钟就过了一遍了,然而在我深挖每一个办法背地逻辑的时候,也让我不得不感叹Josh Bloch 和 Doug Lea的厉害之处。

在细节设计的解决其实往往就是咱们和大神的区别,我认为很多不合理的点,在Google和本人不断深入理解之后才发现这才是正当,真的不服不行。

ThreadLocal是多线程外面比拟冷门的一个类,应用频率比不上别的办法和类,然而通过我这篇文章,不晓得你是否有新的认知呢?

我是敖丙,你晓得的越多,你不晓得的越多,咱们下期见!

人才们的 【三连】 就是敖丙创作的最大能源,如果本篇博客有任何谬误和倡议,欢送人才们留言!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理