1. 为什么要用ThreadLocal?
    并发编程是一项十分重要的技术,它让咱们的程序变得更加高效。
    但在并发的场景中,如果有多个线程同时批改公共变量,可能会呈现线程平安问题,即该变量最终后果可能出现异常。
    为了解决线程平安问题,JDK呈现了很多技术手段,比方:应用synchronized或Lock,给拜访公共资源的代码上锁,保障了代码的原子性。
    但在高并发的场景中,如果多个线程同时竞争一把锁,这时会存在大量的锁期待,可能会节约很多工夫,让零碎的响应工夫一下子变慢。
    因而,JDK还提供了另外一种用空间换工夫的新思路:ThreadLocal。
    它的核心思想是:共享变量在每个线程都有一个正本,每个线程操作的都是本人的正本,对另外的线程没有影响。
    例如:
@Servicepublic class ThreadLocalService {    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();    public void add() {        threadLocal.set(1);        doSamething();        Integer integer = threadLocal.get();    }}
  1. ThreadLocal的原理是什么?
    为了搞清楚ThreadLocal的底层实现原理,咱们不得不扒一下源码。
    ThreadLocal的外部有一个动态的外部类叫:ThreadLocalMap。
public class ThreadLocal<T> {     ...     public T get() {        //获取以后线程        Thread t = Thread.currentThread();        //获取以后线程的成员变量ThreadLocalMap对象        ThreadLocalMap map = getMap(t);        if (map != null) {            //依据threadLocal对象从map中获取Entry对象            ThreadLocalMap.Entry e = map.getEntry(this);            if (e != null) {                @SuppressWarnings("unchecked")                //获取保留的数据                T result = (T)e.value;                return result;            }        }        //初始化数据        return setInitialValue();    }        private T setInitialValue() {        //获取要初始化的数据        T value = initialValue();        //获取以后线程        Thread t = Thread.currentThread();        //获取以后线程的成员变量ThreadLocalMap对象        ThreadLocalMap map = getMap(t);        //如果map不为空        if (map != null)            //将初始值设置到map中,key是this,即threadLocal对象,value是初始值            map.set(this, value);        else           //如果map为空,则须要创立新的map对象            createMap(t, value);        return value;    }        public void set(T value) {        //获取以后线程        Thread t = Thread.currentThread();        //获取以后线程的成员变量ThreadLocalMap对象        ThreadLocalMap map = getMap(t);        //如果map不为空        if (map != null)            //将值设置到map中,key是this,即threadLocal对象,value是传入的value值            map.set(this, value);        else           //如果map为空,则须要创立新的map对象            createMap(t, value);    }         static class ThreadLocalMap {        ...     }     ...}

ThreadLocal的get办法、set办法和setInitialValue办法,其实最终操作的都是ThreadLocalMap类中的数据。
其中ThreadLocalMap类的外部如下:

static class ThreadLocalMap {    static class Entry extends WeakReference<ThreadLocal<?>> {        Object value;        Entry(ThreadLocal<?> k, Object v) {            super(k);            value = v;        }   }   ...   private Entry[] table;   ...}

ThreadLocalMap外面蕴含一个动态的外部类Entry,该类继承于WeakReference类,阐明Entry是一个弱援用。
ThreadLocalMap外部还蕴含了一个Entry数组,其中:Entry = ThreadLocal + value。而ThreadLocalMap被定义成了Thread类的成员变量

public class Thread implements Runnable {    ...    ThreadLocal.ThreadLocalMap threadLocals = null;}

上面用一张图从宏观上,认识一下ThreadLocal的整体构造:

从上图中看出,在每个Thread类中,都有一个ThreadLocalMap的成员变量,该变量蕴含了一个Entry数组,该数组真正保留了ThreadLocal类set的数据。
Entry是由threadLocal和value组成,其中threadLocal对象是弱援用,在GC的时候,会被主动回收。而value就是ThreadLocal类set的数据。
上面用一张图总结一下援用关系:

上图中除了Entry的key对ThreadLocal对象是弱援用,其余的援用都是强援用。
须要特地阐明的是,上图中ThreadLocal对象我画到了堆上,其实在理论的业务场景中不肯定在堆上。因为如果ThreadLocal被定义成了static的,ThreadLocal的对象是类共用的,可能呈现在办法区。

  1. 为什么用ThreadLocal做key?
    不晓得你有没有思考过这样一个问题:ThreadLocalMap为什么要用ThreadLocal做key,而不是用Thread做key?
    如果在你的利用中,一个线程中只应用了一个ThreadLocal对象,那么应用Thread做key也未尝不可。
@Servicepublic class ThreadLocalService {    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();}  

但理论状况中,你的利用,一个线程中很有可能不只应用了一个ThreadLocal对象。这时应用Thread做key不就出有问题?

@Servicepublic class ThreadLocalService {    private static final ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();    private static final ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();    private static final ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();}  

如果应用Thread做key时,你的代码中定义了3个ThreadLocal对象,那么,通过Thread对象,它怎么晓得要获取哪个ThreadLocal对象呢?
如下图所示:

因而,不能应用Thread做key,而应该改成用ThreadLocal对象做key,这样能力通过具体ThreadLocal对象的get办法,轻松获取到你想要的ThreadLocal对象。
如下图所示:

  1. Entry的key为什么设计成弱援用?
    后面说过,Entry的key,传入的是ThreadLocal对象,应用了WeakReference对象,即被设计成了弱援用。
    那么,为什么要这样设计呢?
    如果key对ThreadLocal对象的弱援用,改为强援用。

    咱们都晓得ThreadLocal变量对ThreadLocal对象是有强援用存在的。
    即便ThreadLocal变量生命周期完了,设置成null了,但因为key对ThreadLocal还是强援用。
    此时,如果执行该代码的线程应用了线程池,始终长期存在,不会被销毁。
    就会存在这样的强援用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。
    那么,ThreadLocal对象和ThreadLocalMap都将不会被GC回收,于是产生了内存泄露问题。
    为了解决这个问题,JDK的开发者们把Entry的key设计成了弱援用。
    弱援用的对象,在GC做垃圾清理的时候,就会被主动回收了。
    如果key是弱援用,当ThreadLocal变量指向null之后,在GC做垃圾清理的时候,key会被主动回收,其值也被设置成null。
    如下图所示:

    接下来,最要害的中央来了。
    因为以后的ThreadLocal变量曾经被指向null了,但如果间接调用它的get、set或remove办法,很显然会呈现空指针异样。因为它的生命曾经完结了,再调用它的办法也没啥意义。
    此时,如果零碎中还定义了另外一个ThreadLocal变量b,调用了它的get、set或remove,三个办法中的任何一个办法,都会主动触发清理机制,将key为null的value值清空。
    如果key和value都是null,那么Entry对象会被GC回收。如果所有的Entry对象都被回收了,ThreadLocalMap也会被回收了。
    这样就能最大水平的解决内存泄露问题。
    须要特地留神的中央是:
    1.key为null的条件是,ThreadLocal变量指向null,并且key是弱援用。如果ThreadLocal变量没有断开对ThreadLocal的强援用,即ThreadLocal变量没有指向null,GC就贸然的把弱援用的key回收了,不就会影响失常用户的应用?
    2.如果以后ThreadLocal变量指向null了,并且key也为null了,但如果没有其余ThreadLocal变量触发get、set或remove办法,也会造成内存泄露。
    上面看看弱援用的例子:
public static void main(String[] args) {    WeakReference<Object> weakReference0 = new WeakReference<>(new Object());    System.out.println(weakReference0.get());    System.gc();    System.out.println(weakReference0.get());}

打印后果:

java.lang.Object@1ef7fe8enull

传入WeakReference构造方法的是间接new解决的对象,没有其余援用,在调用gc办法后,弱援用对象会被主动回收。
但如果呈现上面这种状况:

public static void main(String[] args) {    Object object = new Object();    WeakReference<Object> weakReference1 = new WeakReference<>(object);    System.out.println(weakReference1.get());    System.gc();    System.out.println(weakReference1.get());}

执行后果:

java.lang.Object@1ef7fe8ejava.lang.Object@1ef7fe8e

先定义了一个强援用object对象,在WeakReference构造方法中将object对象的援用作为参数传入。这时,调用gc后,弱援用对象不会被主动回收。
咱们的Entry对象中的key不就是第二种状况吗?在Entry构造方法中传入的是ThreadLocal对象的援用。
如果将object强援用设置为null:

public static void main(String[] args) {    Object object = new Object();    WeakReference<Object> weakReference1 = new WeakReference<>(object);    System.out.println(weakReference1.get());    System.gc();    System.out.println(weakReference1.get());    object=null;    System.gc();    System.out.println(weakReference1.get());}

执行后果:

java.lang.Object@6f496d9fjava.lang.Object@6f496d9fnull

第二次gc之后,弱援用可能被失常回收。
由此可见,如果强援用和弱援用同时关联一个对象,那么这个对象是不会被GC回收。也就是说这种状况下Entry的key,始终都不会为null,除非强援用被动断开关联。
此外,你可能还会问这样一个问题:Entry的value为什么不设计成弱援用?
答:Entry的value如果只是被Entry援用,有可能没被业务零碎中的其余中央援用。如果将value改成了弱援用,被GC贸然回收了(数据忽然没了),可能会导致业务零碎出现异常。
而相比之下,Entry的key,治理的中央就十分明确了。这就是Entry的key被设计成弱援用,而value没被设计成弱援用的起因。

  1. ThreadLocal真的会导致内存泄露?
    通过下面的Entry对象中的key设置成弱援用,并且应用get、set或remove办法清理key为null的value值,就能彻底解决内存泄露问题?
    答案是否定的。
    如下图所示:

    如果ThreadLocalMap中存在很多key为null的Entry,但前面的程序,始终都没有调用过无效的ThreadLocal的get、set或remove办法。
    那么,Entry的value值始终都没被清空。
    所以会存在这样一条强援用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> value -> Object。
    其后果就是:Entry和ThreadLocalMap将会长期存在上来,会导致内存泄露。
  2. 如何解决内存泄露问题?
    后面说过的ThreadLocal还是会导致内存泄露的问题,咱们有没有解决办法呢?
    答:有方法,调用ThreadLocal对象的remove办法。
    不是在一开始就调用remove办法,而是在应用完ThreadLocal对象之后。列如:

先创立一个CurrentUser类,其中蕴含了ThreadLocal的逻辑。

public class CurrentUser {    private static final ThreadLocal<UserInfo> THREA_LOCAL = new ThreadLocal();        public static void set(UserInfo userInfo) {        THREA_LOCAL.set(userInfo);    }        public static UserInfo get() {       THREA_LOCAL.get();    }        public static void remove() {       THREA_LOCAL.remove();    }}

而后在业务代码中调用相干办法:

public void doSamething(UserDto userDto) {   UserInfo userInfo = convert(userDto);      try{     CurrentUser.set(userInfo);     ...          //业务代码     UserInfo userInfo = CurrentUser.get();     ...   } finally {      CurrentUser.remove();   }}

须要咱们特地留神的中央是:肯定要在finally代码块中,调用remove办法清理没用的数据。如果业务代码出现异常,也能及时清理没用的数据。
remove办法中会把Entry中的key和value都设置成null,这样就能被GC及时回收,无需触发额定的清理机制,所以它能解决内存泄露问题。

  1. ThreadLocal是如何定位数据的?
    后面说过ThreadLocalMap对象底层是用Entry数组保留数据的。
    那么问题来了,ThreadLocal是如何定位Entry数组数据的?
    在ThreadLocal的get、set、remove办法中都有这样一行代码:
int i = key.threadLocalHashCode & (len-1);

通过key的hashCode值,与数组的长度减1。其中key就是ThreadLocal对象,与数组的长度减1,相当于除以数组的长度减1,而后取模。
这是一种hash算法。

接下来给大家举个例子:假如len=16,key.threadLocalHashCode=31,

于是:int i = 31 & 15 = 15

相当于:int i = 31 % 16 = 15

计算的后果是一样的,然而应用与运算效率跟高一些。

为什么与运算效率更高?
答:因为ThreadLocal的初始大小是16,每次都是按2倍扩容,数组的大小其实始终都是2的n次方。这种数据有个法则就是高位是0,低位都是1。在做与运算时,能够不必思考高位,因为与运算的后果必然是0。只需思考低位的与运算,所以效率更高。
如果应用hash算法定位具体位置的话,就可能会呈现hash抵触的状况,即两个不同的hashCode取模后的值雷同。
ThreadLocal是如何解决hash抵触的呢?
咱们看看getEntry是怎么做的:

private Entry getEntry(ThreadLocal<?> key) {    //通过hash算法获取下标值    int i = key.threadLocalHashCode & (table.length - 1);    Entry e = table[i];    //如果下标地位上的key正好是咱们所须要寻找的key    if (e != null && e.get() == key)        //阐明找到数据了,间接返回        return e;    else        //阐明呈现hash抵触了,持续往后找        return getEntryAfterMiss(key, i, e);}

再看看getEntryAfterMiss办法:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {    Entry[] tab = table;    int len = tab.length;    //判断Entry对象如果不为空,则始终循环    while (e != null) {        ThreadLocal<?> k = e.get();        //如果以后Entry的key正好是咱们所须要寻找的key        if (k == key)            //阐明这次真的找到数据了            return e;        if (k == null)            //如果key为空,则清理脏数据            expungeStaleEntry(i);        else            //如果还是没找到数据,则持续往后找           i = nextIndex(i, len);        e = tab[i];    }    return null;

要害看看nextIndex办法:

private static int nextIndex(int i, int len) {    return ((i + 1 < len) ? i + 1 : 0);}

当通过hash算法计算出的下标小于数组大小,则将下标值加1。否则,即下标大于等于数组大小,下标变成0了。下标变成0之后,则循环一次,下标又变成1。。。
寻找的大抵过程如下图所示:

如果找到最初一个,还是没有找到,则再从头开始找。

不晓得你有没有发现,它形成了一个:环形。
ThreadLocal从数组中找数据的过程大抵是这样的:

1通过key的hashCode取余计算出一个下标。
2.通过下标,在数组中定位具体Entry,如果key正好是咱们所须要的key,阐明找到了,则间接返回数据。
3.如果第2步没有找到咱们想要的数据,则从数组的下标地位,持续往后面找。
4.如果第3步中找key的正好是咱们所须要的key,阐明找到了,则间接返回数据。
5.如果还是没有找到数据,再持续往后面找。如果找到最初一个地位,还是没有找到数据,则再从头,即下标为0的地位,持续从前往后找数据。
6.直到找到第一个Entry为空为止。

  1. ThreadLocal是如何扩容的?
    从下面得悉,ThreadLocal的初始大小是16。那么问题来了,ThreadLocal是如何扩容的?
    在set办法中会调用rehash办法:
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();}

留神一下,其中有个判断条件是:sz(之前的size+1)如果大于或等于threshold的话,则调用rehash办法。

threshold默认是0,在创立ThreadLocalMap时,调用它的构造方法

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

调用setThreshold办法给threshold设置一个值,而这个值INITIAL_CAPACITY是默认的大小16。

private void setThreshold(int len) {    threshold = len * 2 / 3;}

也就是第一次设置的threshold = 16 * 2 / 3, 取整后的值是:10。
换句话说当sz大于等于10时,就能够思考扩容了。
rehash代码如下:

private void rehash() {    //先尝试回收一次key为null的值,腾出一些空间    expungeStaleEntries();    if (size >= threshold - threshold / 4)        resize();}

在真正扩容之前,先尝试回收一次key为null的值,腾出一些空间。

如果回收之后的size大于等于threshold的3/4时,才须要真正的扩容。

计算公式如下:
16 * 2 * 4 / 3 * 4 - 16 * 2 / 3 * 4 = 8
也就是说增加数据后,新的size大于等于老size的1/2时,才须要扩容。

private void resize() {    Entry[] oldTab = table;    int oldLen = oldTab.length;    //按2倍的大小扩容    int newLen = oldLen * 2;    Entry[] newTab = new Entry[newLen];    int count = 0;    for (int j = 0; j < oldLen; ++j) {        Entry e = oldTab[j];        if (e != null) {            ThreadLocal<?> k = e.get();            if (k == null) {                e.value = null; // Help the GC            } else {                int h = k.threadLocalHashCode & (newLen - 1);                while (newTab[h] != null)                    h = nextIndex(h, newLen);                newTab[h] = e;                count++;            }        }    }    setThreshold(newLen);    size = count;    table = newTab;}

resize中每次都是按2倍的大小扩容。

扩容的过程如下图所示:

扩容的关键步骤如下:

1.老size + 1 = 新size
2.如果新size大于等于老size的2/3时,须要思考扩容。
3.扩容前先尝试回收一次key为null的值,腾出一些空间。
4.如果回收之后发现size还是大于等于老size的1/2时,才须要真正的扩容。
5.每次都是按2倍的大小扩容。

  1. 父子线程如何共享数据?
    后面介绍的ThreadLocal都是在一个线程中保留和获取数据的。
    但在理论工作中,有可能是在父子线程中共享数据的。即在父线程中往ThreadLocal设置了值,在子线程中可能获取到。
    例如:
public class ThreadLocalTest {    public static void main(String[] args) {        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();        threadLocal.set(6);        System.out.println("父线程获取数据:" + threadLocal.get());        new Thread(() -> {            System.out.println("子线程获取数据:" + threadLocal.get());        }).start();    }}

执行后果:

父线程获取数据:6子线程获取数据:null

你会发现,在这种状况下应用ThreadLocal是行不通的。main办法是在主线程中执行的,相当于父线程。在main办法中开启了另外一个线程,相当于子线程。

显然通过ThreadLocal,无奈在父子线程中共享数据。

那么,该怎么办呢?

答:应用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类。
批改代码之后:

public class ThreadLocalTest {    public static void main(String[] args) {        InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();        threadLocal.set(6);        System.out.println("父线程获取数据:" + threadLocal.get());        new Thread(() -> {            System.out.println("子线程获取数据:" + threadLocal.get());        }).start();    }}

执行后果:

父线程获取数据:6子线程获取数据:6

果然,在换成InheritableThreadLocal之后,在子线程中可能失常获取父线程中设置的值。

其实,在Thread类中除了成员变量threadLocals之外,还有另一个成员变量:inheritableThreadLocals。
Thread类的局部代码如下:

ThreadLocal.ThreadLocalMap threadLocals = null;ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

最要害的一点是,在它的init办法中会将父线程中往ThreadLocal设置的值,拷贝一份到子线程中。

  1. 线程池中如何共享数据?
    在实在的业务场景中,个别很少用独自的线程,绝大多数,都是用的线程池。
    那么,在线程池中如何共享ThreadLocal对象生成的数据呢?
    因为波及到不同的线程,如果间接应用ThreadLocal,显然是不适合的。
    咱们应该应用InheritableThreadLocal,具体代码如下:
private static void fun1() {    InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();    threadLocal.set(6);    System.out.println("父线程获取数据:" + threadLocal.get());    ExecutorService executorService = Executors.newSingleThreadExecutor();    threadLocal.set(6);    executorService.submit(() -> {        System.out.println("第一次从线程池中获取数据:" + threadLocal.get());    });    threadLocal.set(7);    executorService.submit(() -> {        System.out.println("第二次从线程池中获取数据:" + threadLocal.get());    });}

执行后果:

父线程获取数据:6第一次从线程池中获取数据:6第二次从线程池中获取数据:6

因为这个例子中应用了单例线程池,固定线程数是1。

第一次submit工作的时候,该线程池会主动创立一个线程。因为应用了InheritableThreadLocal,所以创立线程时,会调用它的init办法,将父线程中的inheritableThreadLocals数据复制到子线程中。所以咱们看到,在主线程中将数据设置成6,第一次从线程池中获取了正确的数据6。

之后,在主线程中又将数据改成7,但在第二次从线程池中获取数据却仍然是6。

因为第二次submit工作的时候,线程池中曾经有一个线程了,就间接拿过去复用,不会再从新创立线程了。所以不会再调用线程的init办法,所以第二次其实没有获取到最新的数据7,还是获取的老数据6。

那么,这该怎么办呢?

答:应用TransmittableThreadLocal,它并非JDK自带的类,而是阿里巴巴开源jar包中的类。
能够通过如下pom文件引入该jar包:

<dependency>   <groupId>com.alibaba</groupId>   <artifactId>transmittable-thread-local</artifactId>   <version>2.11.0</version>   <scope>compile</scope></dependency>

代码调整如下:

private static void fun2() throws Exception {    TransmittableThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();    threadLocal.set(6);    System.out.println("父线程获取数据:" + threadLocal.get());    ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));    threadLocal.set(6);    ttlExecutorService.submit(() -> {        System.out.println("第一次从线程池中获取数据:" + threadLocal.get());    });    threadLocal.set(7);    ttlExecutorService.submit(() -> {        System.out.println("第二次从线程池中获取数据:" + threadLocal.get());    });}

执行后果:

父线程获取数据:6第一次从线程池中获取数据:6第二次从线程池中获取数据:7

咱们看到,应用了TransmittableThreadLocal之后,第二次从线程中也能正确获取最新的数据7了。
如果你仔细观察这个例子,你可能会发现,代码中除了应用TransmittableThreadLocal类之外,还应用了TtlExecutors.getTtlExecutorService办法,去创立ExecutorService对象。

这是十分重要的中央,如果没有这一步,TransmittableThreadLocal在线程池中共享数据将不会起作用。

创立ExecutorService对象,底层的submit办法会TtlRunnable或TtlCallable对象。

以TtlRunnable类为例,它实现了Runnable接口,同时还实现了它的run办法:

public void run() {    Map<TransmittableThreadLocal<?>, Object> copied = (Map)this.copiedRef.get();    if (copied != null && (!this.releaseTtlValueReferenceAfterRun || this.copiedRef.compareAndSet(copied, (Object)null))) {        Map backup = TransmittableThreadLocal.backupAndSetToCopied(copied);        try {            this.runnable.run();        } finally {            TransmittableThreadLocal.restoreBackup(backup);        }    } else {        throw new IllegalStateException("TTL value reference is released after run!");    }}

这段代码的次要逻辑如下:

1.把过后的ThreadLocal做个备份,而后将父类的ThreadLocal拷贝过去。
2.执行真正的run办法,能够获取到父类最新的ThreadLocal数据。
3.从备份的数据中,复原过后的ThreadLocal数据。

  1. ThreadLocal有哪些用处?
    上面列举几个常见的场景:

1.在spring事务中,保障一个线程下,一个事务的多个操作拿到的是一个Connection。
2.在hiberate中治理session。
3.在JDK8之前,为了解决SimpleDateFormat的线程平安问题。
4.获取以后登录用户上下文。
5.长期保留权限数据。
6.应用MDC保留日志信息。