抛出这8个问题,检验一下你到底会不会ThreadLocal,来摸个底~(下)

简介: 抛出这8个问题,检验一下你到底会不会ThreadLocal,来摸个底~(下)

2、set()


/**
 * 设置当前线程的线程局部变量的值
 * 实际上ThreadLocal的值是放入了当前线程的一个ThreadLocalMap实例中,所以只能在本线程中访问。
 */
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程对应的ThreadLocalMap实例
    ThreadLocalMap map = getMap(t);
    // 若当前线程有对应的ThreadLocalMap实例,则将当前ThreadLocal对象作为key,value做为值存到ThreadLocalMap的entry里。
    if (map != null)
        map.set(this, value);
    else
        // 若当前线程没有对应的ThreadLocalMap实例,则创建ThreadLocalMap,并将此线程与之绑定
        createMap(t, value);
}


3、getMap()


// 在你调用ThreadLocal.get()方法的时候就会调用这个方法,它的返回是当前线程里的threadLocals的引用。
// 这个引用指向的是ThreadLocal里的ThreadLocalMap对象
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
public class Thread implements Runnable {
    // ThreadLocal.ThreadLocalMap
    ThreadLocal.ThreadLocalMap threadLocals = null;
}


4、map.set()


// 不多BB,就和HashMap的set一个道理,只是赋值key,value。
// 需要注意的是这里key是ThreadLocal对象,value是值
private void set(ThreadLocal<?> key, Object value) {}


5、createMap()


/**
 * 创建ThreadLocalMap对象。
 * t.threadLocals在上面的getMap中详细介绍了。此处不BB。
 * 实例化ThreadLocalMap并且传入两个值,一个是当前ThreadLocal对象一个是value。
 */
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// ThreadLocalMap构造器。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 重点看这里!!!!!!
    // new了一个ThreadLocalMap的内部类Entry,且将key和value传入。
    // key是ThreadLocal对象。
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}
/**
 * 到这里朋友们应该真相大白了,其实ThreadLocal就是内部维护一个ThreadLocalMap,
 * 而ThreadLocalMap内部又维护了一个Entry对象。Entry对象是key-value形式,
 * key是ThreadLocal对象,value是传入的value
 * 所以我们对ThreadLocal的操作其实都是对内部的ThreadLocalMap.Entry的操作
 * 所以保证了线程之前互不干扰。
 */


6、get()


/**
 * 获取当前线程下的entry里的value值。
 * 先获取当前线程下的ThreadLocalMap,
 * 然后以当前ThreadLocal为key取出map中的value
 */
public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程对应的ThreadLocalMap对象。
    ThreadLocalMap map = getMap(t);
    // 若获取到了。则获取此ThreadLocalMap下的entry对象,若entry也获取到了,那么直接获取entry对应的value返回即可。
    if (map != null) {
        // 获取此ThreadLocalMap下的entry对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 若entry也获取到了
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 直接获取entry对应的value返回。
            T result = (T)e.value;
            return result;
        }
    }
    // 若没获取到ThreadLocalMap或没获取到Entry,则设置初始值。
    // 知识点:我早就说了,初始值方法是延迟加载,只有在get才会用到,这下看到了吧,只有在这获取没获取到才会初始化,下次就肯定有值了,所以只会执行一次!!!
    return setInitialValue();
}


7、setInitialValue()


// 设置初始值
private T setInitialValue() {
    // 调用初始值方法,由子类提供。
    T value = initialValue();
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取map
    ThreadLocalMap map = getMap(t);
    // 获取到了
    if (map != null)
        // set
        map.set(this, value);
    else
        // 没获取到。创建map并赋值
        createMap(t, value);
    // 返回初始值。
    return value;
}


8、initialValue()


// 由子类提供实现。
// protected
protected T initialValue() {
    return null;
}


9、remove()


/**
 * 将当前线程局部变量的值删除,目的是为了减少内存占用。
 * 其实当线程结束后对应该线程的局部变量将自动被垃圾回收,所以无需我们调用remove,我们调用remove无非也就是加快内存回收速度。
 */
public void remove() {
    // 获取当前线程的ThreadLocalMap对象,并将其移除。
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}


10、小结


只要捋清楚如下几个类的关系,ThreadLocal将变得so easy!

ThreadThreadLocalThreadLocalMapEntry

一句话总结就是:Thread维护了ThreadLocalMap,而ThreadLocalMap里维护了Entry,而Entry里存的是以ThreadLocal为key,传入的值为value的键值对。


五、答疑(面试题)


1、和Synchronized的区别


问:他和线程同步机制(如:Synchronized)提供一样的功能,这个很吊啊。


答:放屁!同步机制保证的是多线程同时操作共享变量并且能正确的输出结果。ThreadLocal不行啊,他把共享变量变成线程私有了,每个线程都有独立的一个变量。举个通俗易懂的案例:网站计数器,你给变量count++的时候带上synchronized即可解决。ThreadLocal的话做不到啊,他没发统计,他只能说能统计每个线程登录了多少次。


2、存储在jvm的哪个区域


问:线程私有,那么就是说ThreadLocal的实例和他的值是放到栈上咯?


答:不是。还是在堆的。ThreadLocal对象也是对象,对象就在堆。只是JVM通过一些技巧将其可见性变成了线程可见。


3、真的只是当前线程可见吗


问:真的只是当前线程可见吗?


答:貌似不是,貌似通过InheritableThreadLocal类可以实现多个线程访问ThreadLocal的值,但是我没研究过,知道这码事就行了。


4、会导致内存泄漏么


问:会导致内存泄漏么?


答:分析一下:


  • 1、ThreadLocalMap.Entry的key会内存泄漏吗?


  • 2、ThreadLocalMap.Entry的value会内存泄漏吗?


先看下key-value的核心源码


static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}


先看继承关系,发现是继承了弱引用,而且key直接是交给了父类处理super(key),父类是个弱引用,所以key完全不存在内存泄漏问题,因为他不是强引用,它可以被GC回收的。


弱引用的特点:如果这个对象只被弱引用关联,没有任何强引用关联,那么这个对象就可以被GC回收掉。弱引用不会阻止GC回收。这是jvm知识。


再看value,发现value是个强引用,但是想了下也没问题的呀,因为线程终止了,我管你强引用还是弱引用,都会被GC掉的,因为引用链断了(jvm用的可达性分析法,线程终止了,根节点就断了,下面的都会被回收)。

这么分析一点毛病都没有,但是忘了一个主要的角


色,那就是线程池,线程池的存在核心线程是不会销毁的,只要创建出来他会反复利用,生命周期不会结束掉,但是key是弱引用会被GC回收掉,value强引用不会回收,所以形成了如下场面:


Thread->ThreadLocalMap->Entry(key为null)->value


由于value和Thread还存在链路关系,还是可达的,所以不会被回收,这样越来越多的垃圾对象产生却无法回收,早晨内存泄漏,时间久了必定OOM。


解决方案ThreadLocal已经为我们想好了,提供了remove()方法,这个方法是将value移出去的。所以用完后记得remove()


5、为什么用Entry数组而不是Entry对象


这个其实主要想考ThreadLocalMap是在Thread里持有的引用。


问:ThreadLocalMap内部的table为什么是数组而不是单个对象呢?


答:因为你业务代码能new好多个ThreadLocal对象,各司其职。但是在一次请求里,也就是一个线程里,ThreadLocalMap是同一个,而不是多个,不管你new几次ThreadLocalThreadLocalMap在一个线程里就一个,因为ThreadLocalMap的引用是在Thread里的,所以它里面的Entry数组存放的是一个线程里你new出来的多个ThreadLocal对象。


6、你学习的开源框架哪些用到了ThreadLocal


Spring框架。


DateTimeContextHolder

RequestContextHolder


7、ThreadLocal里的对象一定是线程安全的吗


未必,如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()获取的还是这个共享对象本身,还是有并发访问线程不安全问题。


8、笔试题


问:下面这段程序会输出什么?为什么?


public class TestThreadLocalNpe {
    private static ThreadLocal<Long> threadLocal = new ThreadLocal();
    public static void set() {
        threadLocal.set(1L);
    }
    public static long get() {
        return threadLocal.get();
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            set();
            System.out.println(get());
        }).start();
        // 目的就是为了让子线程先运行完
        Thread.sleep(100);
        System.out.println(get());
    }
}


答:


1
Exception in thread "main" java.lang.NullPointerException
 at com.chentongwei.study.thread.TestThreadLocalNpe.get(TestThreadLocalNpe.java:16)
 at com.chentongwei.study.thread.TestThreadLocalNpe.main(TestThreadLocalNpe.java:26)


为什么?


为什么输出个1,然后空指针了?


首先输出1是没任何问题的,其次主线程空指针是为什么?


如果你这里回答


1
1


那我恭喜你,你连ThreadLocal都不知道是啥,这明显两个线程,子线程和主线程。子线程设置1,主线程肯定拿不到啊,ThreadLocal和线程是嘻嘻相关的。这个不多费口舌。


说说为什么是空指针?


因为你get方法用的long而不是Long,那也应该返回null啊,大哥,long是基本类型,默认值是0,没有null这一说法。ThreadLocal里的泛型是Long,get却是基本类型,这需要拆箱操作的,也就是会执行null.longValue()的操作,这绝逼空指针了。



看似一道Javase的基础题目,实则隐藏了很多知识。


六、ThreadLocal工具类


package com.duoku.base.util;
import com.google.common.collect.Maps;
import org.springframework.core.NamedThreadLocal;
import java.util.Map;
/**
 * Description:
 *
 * @author TongWei.Chen 2019-09-09 18:35:30
 */
public class ThreadLocalUtil {
    private static final ThreadLocal<Map<String, Object>> threadLocal = new NamedThreadLocal("xxx-threadlocal") {
        @Override
        protected Map<String, Object> initialValue() {
            return Maps.newHashMap();
        }
    };
    public static Map<String, Object> getThreadLocal(){
        return threadLocal.get();
    }
    public static <T> T get(String key) {
        Map map = threadLocal.get();
        // todo:copy a new one
        return (T)map.get(key);
    }
    public static <T> T get(String key,T defaultValue) {
        Map map = threadLocal.get();
        return (T)map.get(key) == null ? defaultValue : (T)map.get(key);
    }
    public static void set(String key, Object value) {
        Map map = threadLocal.get();
        map.put(key, value);
    }
    public static void set(Map<String, Object> keyValueMap) {
        Map map = threadLocal.get();
        map.putAll(keyValueMap);
    }
    public static void remove() {
        threadLocal.remove();
    }
}



琐碎时间想看一些技术文章,可以去公众号菜单栏翻一翻我分类好的内容,应该对部分童鞋有帮助。同时看的过程中发现问题欢迎留言指出,不胜感谢~。另外,有想多了解哪些方面内容的可以留言(什么时候,哪篇文章下留言都行),附菜单栏截图(PS:很多人不知道公众号菜单栏是什么)


image.png

相关文章
|
7月前
|
算法 Java
垃圾收集-判断对象的生死
垃圾收集-判断对象的生死
37 0
|
存储
最粗暴的方法实现一个栈
最粗暴的方法实现一个栈
45 1
|
4月前
|
存储 算法 Java
惊!Java程序员必看:JVM调优揭秘,堆溢出、栈溢出如何巧妙化解?
【8月更文挑战第29天】在Java领域,JVM是代码运行的基础,但需适当调优以发挥最佳性能。本文探讨了JVM中常见的堆溢出和栈溢出问题及其解决方法。堆溢出发生在堆空间不足时,可通过增加堆空间、优化代码及释放对象解决;栈溢出则因递归调用过深或线程过多引起,调整栈大小、优化算法和使用线程池可有效应对。通过合理配置和调优JVM,可确保Java应用稳定高效运行。
143 4
|
2月前
|
Java 测试技术 Android开发
让星星⭐月亮告诉你,强软弱虚引用类型对象在内存足够和内存不足的情况下,面对System.gc()时,被回收情况如何?
本文介绍了Java中四种引用类型(强引用、软引用、弱引用、虚引用)的特点及行为,并通过示例代码展示了在内存充足和不足情况下这些引用类型的不同表现。文中提供了详细的测试方法和步骤,帮助理解不同引用类型在垃圾回收机制中的作用。测试环境为Eclipse + JDK1.8,需配置JVM运行参数以限制内存使用。
32 2
|
7月前
|
存储 编译器 C语言
C陷阱:数组越界遍历,不报错却出现死循环?从内存解析角度看数组与局部变量之“爱恨纠葛”
在代码练习中,通常会避免数组越界访问,但如果运行了这样的代码,可能会导致未定义行为,例如死循环。当循环遍历数组时,如果下标超出数组长度,程序可能会持续停留在循环体内。这种情况的发生与数组和局部变量(如循环变量)在内存中的布局有关。在某些编译器和环境下,数组和局部变量可能在栈上相邻存储,数组越界访问可能会修改到循环变量的值,导致循环条件始终满足,从而形成死循环。理解这种情况有助于我们更好地理解和预防这类编程错误。
148 0
|
7月前
【错题集-编程题】点击消除(栈)
【错题集-编程题】点击消除(栈)
|
缓存 算法 Oracle
面试官:JVM是如何判定对象已死的?学JVM必会的知识!
作为一名Java程序员,我们每天都在程序里不停地去new对象,但是你知道这些被new出来的对象,最后是怎么被回收的吗?
22245 4
面试官:JVM是如何判定对象已死的?学JVM必会的知识!
|
缓存 算法 Java
面试官:JVM是如何判定对象已死的?
在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(“死去”即不可能再被任何途径使用的对象)了。
191 0
|
安全 Java 索引
Java集合类不安全分析
我们平时编码时使用集合类,都是new 一个 ArrayList 或者 HashSet 或者 HashMap就直接开用,好像也没遇到啥问题。那这里为什么说集合不安全呢?下面一 一道来。
Java集合类不安全分析
|
Java C++
关于 Synchronized 的一个点,网上99%的文章都错了(中)
关于 Synchronized 的一个点,网上99%的文章都错了(中)
关于 Synchronized 的一个点,网上99%的文章都错了(中)