问题


  1. 和Synchronized的区别
  2. 存储在jvm的哪个区域
  3. 真的只是以后线程可见吗
  4. 会导致内存透露么
  5. 为什么用Entry数组而不是Entry对象
  6. 你学习的开源框架哪些用到了ThreadLocal
  7. ThreadLocal里的对象肯定是线程平安的吗
  8. 口试题

一、概述


1、官网术语

ThreadLocal类是用来提供线程外部的局部变量。让这些变量在多线程环境下拜访(get/set)时能保障各个线程里的变量绝对独立于其余线程内的变量。

2、大白话

ThreadLocal是一个对于创立线程局部变量的类。

通常状况下,咱们创立的成员变量都是线程不平安的。因为他可能被多个线程同时批改,此变量对于多个线程之间彼此并不独立,是共享变量。而应用ThreadLocal创立的变量只能被以后线程拜访,其余线程无法访问和批改。也就是说:将线程公有化变成线程私有化。

二、利用场景


  • 每个线程都须要一个独享的对象(比方工具类,典型的就是SimpleDateFormat,每次应用都new一个多节约性能呀,间接放到成员变量里又是线程不平安,所以把他用ThreadLocal治理起来就完满了。)
比方:
/** * Description: SimpleDateFormat就一份,不浪费资源。 * * @author TongWei.Chen 2020-07-10 14:00:29 */public class ThreadLocalTest05 {    public static String dateToStr(int millisSeconds) {        Date date = new Date(millisSeconds);        SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();        return simpleDateFormat.format(date);    }    private static final ExecutorService executorService = Executors.newFixedThreadPool(100);    public static void main(String[] args) {        for (int i = 0; i < 3000; i++) {            int j = i;            executorService.execute(() -> {                String date = dateToStr(j * 1000);                // 从后果中能够看出是线程平安的,工夫没有反复的。                System.out.println(date);            });        }        executorService.shutdown();    }}class ThreadSafeFormatter {    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal() {        @Override        protected SimpleDateFormat initialValue() {            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");        }    };    // java8的写法,装逼神器//    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =//            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));}
仔细的敌人曾经发现了,这TM也是每个线程都创立一个SimpleDateFormat啊,跟间接在办法外部new没区别,错了,大错特错!1个申请进来是一个线程,他可能贯通了N个办法,你这N个办法假如有3个都在应用dateToStr(),你间接new的话会产生三个SimpleDateFormat对象,而用ThreadLocal的话只会产生一个对象,一个线程一个。
  • 每个线程内须要保留全局变量(比方在登录胜利后将用户信息存到ThreadLocal里,而后以后线程操作的业务逻辑间接get取就完事了,无效的防止的参数来回传递的麻烦之处),肯定层级上缩小代码耦合度。

再细化一点就是:

  • 比方存储 交易id等信息。每个线程公有。
  • 比方aop里记录日志须要before记录申请id,end拿出申请id,这也能够。
  • 比方jdbc连接池(很典型的一个ThreadLocal用法)
  • ....等等....

三、外围常识


1、类关系

每个Thread对象中都持有一个ThreadLocalMap的成员变量。每个ThreadLocalMap外部又保护了N个Entry节点,也就是Entry数组,每个Entry代表一个残缺的对象,key是ThreadLocal自身,value是ThreadLocal的泛型值。

外围源码如下

// java.lang.Thread类里持有ThreadLocalMap的援用public class Thread implements Runnable {    ThreadLocal.ThreadLocalMap threadLocals = null;}// java.lang.ThreadLocal有外部动态类ThreadLocalMappublic class ThreadLocal<T> {    static class ThreadLocalMap {        private Entry[] table;                // ThreadLocalMap外部有Entry类,Entry的key是ThreadLocal自身,value是泛型值        static class Entry extends WeakReference<ThreadLocal<?>> {            Object value;            Entry(ThreadLocal<?> k, Object v) {                super(k);                value = v;            }        }    }}

2、类关系图

ThreadLocal内存结构图。

3、次要办法

  • initialValue:初始化。在get办法里懒加载的。
  • get:失去这个线程对应的value。如果调用get之前没set过,则get外部会执行initialValue办法进行初始化。
  • set:为这个线程设置一个新值。
  • remove:删除这个线程对应的值,避免内存泄露的最佳伎俩。

3.1、initialValue

3.1.1、什么意思

见名知意,初始化一些value(泛型值)。懒加载的。

3.1.2、触发机会

调用get办法之前没有调用set办法,则get办法外部会触发initialValue,也就是说get的时候如果没拿到货色,则会触发initialValue

3.1.3、补充阐明

  • 通常,每个线程最多调用一次此办法。然而如果曾经调用了remove(),而后再次调用get()的话,则能够再次触发initialValue
  • 如果要重写的话个别倡议采取匿名外部类的形式重写此办法,否则默认返回的是null。
比方:
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal() {    @Override    protected SimpleDateFormat initialValue() {        return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");    }};// Java8的高逼格写法public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));

3.1.4、源码

// 由子类提供实现。// protected的含意就是交给子类干的。protected T initialValue() {    return null;}

3.2、get

3.2.1、什么意思

获取以后线程下的ThreadLocal中的值。

3.2.2、源码

/** * 获取以后线程下的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();}

3.3、set

3.3.1、什么意思

其实干的事和initialValue是一样的,都是set值,只是调用机会不同。set是想用就用,api摆在这里,你想用就调一下set办法。很自在。

3.3.2、源码

/** * 设置以后线程的线程局部变量的值 * 实际上ThreadLocal的值是放入了以后线程的一个ThreadLocalMap实例中,所以只能在本线程中拜访。 */public void set(T value) {    // 获取以后线程    Thread t = Thread.currentThread();    // 获取以后线程对应的ThreadLocalMap实例,留神这里是将t传进去了,t是以后线程,就是说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.4、remove

3.4.1、什么意思

将以后线程下的ThreadLocal的值删除,目标是为了缩小内存占用。次要目标是避免内存透露。内存透露问题上面会说。

3.4.2、源码

/** * 将以后线程局部变量的值删除,目标是为了缩小内存占用。次要目标是避免内存透露。内存透露问题上面会说。 */public void remove() {    // 获取以后线程的ThreadLocalMap对象,并将其移除。    ThreadLocalMap m = getMap(Thread.currentThread());    if (m != null)        // 间接移除以以后ThreadLocal为key的value        m.remove(this);}

4、ThreadLocalMap

为啥独自拿出来说下,我就是想强调一点:这个货色是归Thread类所有的。它的援用在Thread类里,这也证实了一个问题:ThreadLocalMap类外部为什么有Entry数组,而不是Entry对象?

因为你业务代码能new好多个ThreadLocal对象,各司其职。然而在一次申请里,也就是一个线程里,ThreadLocalMap是同一个,而不是多个,不论你new几次ThreadLocalThreadLocalMap在一个线程里就一个,因为再说一次,ThreadLocalMap的援用是在Thread里的,所以它外面的Entry数组寄存的是一个线程里你new进去的多个ThreadLocal对象。

外围源码如下:

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

四、残缺源码


1、外围源码

// 本地线程。Thread:线程。Local:本地public class ThreadLocal<T> {    // 结构器 public ThreadLocal() {}    // 初始值,用来初始化值用的,比方:ThreadLocal<Integer> count = new ThreadLocal<>();    // 你想Integer value = count.get(); value++;这样是报错的,因为count当初还没值,取出来的是个null,所以你须要先重写此办法为value赋上初始值,自身办法是protected也代表就是为了子类重写的。    // 此办法是一个提早调用办法,在线程第一次调用get的时候才执行,上面具体分析源码就晓得了。 protected T initialValue() {}    // 创立ThreadLocalMap,ThreadLocal底层其实就是一个map来保护的。 void createMap(Thread t, T firstValue) {}    // 返回应当火线程对应的线程局部变量值。 public T get() {}    // 获取ThreadLocalMap ThreadLocalMap getMap(Thread t) {}    // 设置以后线程的线程局部变量的值 public void set(T value) {}    // 将以后线程局部变量的值删除,目标是为了缩小内存占用。其实当线程完结后对应该线程的局部变量将主动被垃圾回收,所以无需咱们调用remove,咱们调用remove无非也就是放慢内存回收速度。 public void remove() {}    // 设置初始值,调用initialValue private T setInitialValue() {}    // 动态外部类,一个map来保护的!!! static class ThreadLocalMap {          // ThreadLocalMap的动态外部类,继承了弱援用,这正是不会造成内存透露根本原因        // Entry的key为ThreadLocal并且是弱援用。value是值  static class Entry extends WeakReference<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()

// 由子类提供实现。// protectedprotected 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());    }}

答:

1Exception 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是没任何问题的,其次主线程空指针是为什么?

如果你这里答复

11

那我祝贺你,你连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();    }}

写在最初

欢送大家关注我的公众号【惊涛骇浪如码】,海量Java相干文章,学习材料都会在外面更新,整顿的材料也会放在外面。

感觉写的还不错的就点个赞,加个关注呗!点关注,不迷路,继续更新!!!