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

62次阅读

共计 8456 个字符,预计需要花费 22 分钟才能阅读完成。

开场白

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

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

暗自窃喜的张三来到了某东现场面试的办公室,我丢,这面试官?不是吧,这满是划痕的 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 是多线程外面比拟冷门的一个类,应用频率比不上别的办法和类,然而通过我这篇文章,不晓得你是否有新的认知呢?

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

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

正文完
 0