简介

从名称看,ThreadLocal 也就是thread和local的组合,也就是说每一个线程有一个本地的变量副本。它与syncronized相反的思想,ThreadLocal 则从另一个角度来解决多线程的并发访问。 ThreadLocal 会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。 ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal 。
对于多线程资源共享的问题,同步机制采用了 “以时间换空间 ” 的方式,而 ThreadLocal 采用了 “ 以空间换时间 ” 的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。因此,ThreadLocal提供了一种与众不同的线程安全方式,它不是在发生线程冲突时想办法解决冲突,而是彻底的避免了冲突的发生。

实现原理

简单使用

  1. public class ThreadLocalTest {
  2. private static ThreadLocal<Integer> local = new ThreadLocal<>();
  3. public static void main(String[] args) {
  4. // 启动三个线程,存储随机数,然后不同的访问者去获取当前线程存储的值,可以看到每个线程的值互不干扰
  5. IntStream.range (0,3).forEach (value -> {
  6. new Thread (() -> {
  7. local.set (new Random ().nextInt ());
  8. new A ().get ();
  9. new B ().get ();
  10. },"线程" + value).start ();
  11. });
  12. }
  13. static class A {
  14. public void get() {
  15. System.out.println("A获取到的值:" + Thread.currentThread().getName() + " :" + local.get());
  16. }
  17. }
  18. static class B {
  19. public void get() {
  20. System.out.println("B获取到的值:" + Thread.currentThread().getName() + " :" + local.get());
  21. }
  22. }
  23. }

怎么使用我这里就不做过多阐述,因为在项目中经常使用到。比如我们接口中用到的token,一般存储在redis中,通过redis get(“${token}”)方法,拿到存储的用户信息(任何常用的信息),然后将我们常用的信息存储在上下文的contenxt中即可。
由于ThreadLocal里设置的值,只有当前线程自己看得见,这意味着你不可能通过其他线程为它初始化值。为了弥补这一点,ThreadLocal提供了一个withInitial()方法统一初始化所有线程的ThreadLocal的值:

  1. private ThreadLocal<Integer> localInt = ThreadLocal.withInitial(() -> 6);

Thread和ThreadLocal关系

点击查看【processon】
通过上图我们似乎对为啥线程能够自己单独存储一份变量副本,因为它内部自身就有一个tl的map。如果你用到tl,并且你用tl去set值了,意味着当前线程的成员变量threadlocals就开始有值了。
具体的ThreadLocalMap实例并不是ThreadLocal保持,而是每个Thread持有,且不同的Thread持有不同ThreadLocalMap实例, 因此它们是不存在线程竞争的(不是一个全局的map), 另一个好处是每次线程死亡,所有map中引用到的对象都会随着这个Thread的死亡而被垃圾收集器一起收集。

内存泄漏

内存泄漏是指:不再被使用的对象或者变量一直被占据在内存中。长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露。
如下b1、b2已经使用完了但是由于被list所持有的(可达性算法),而

  1. public class Demo {
  2. static List<Byte[]> list = new ArrayList<>();
  3. public static void main(String[] args) {
  4. int M = 1024*1024;
  5. Byte[] b1,b2,b3,b4;
  6. b1 = new Byte[2*M];
  7. b2 = new Byte[2*M];
  8. list.add(b1);
  9. list.add(b2);
  10. }
  11. }

引用类型划分

第六章 ThreadLocal - 图1

理解TL的内存泄漏问题

ThreadLocal.ThreadLocalMap是一个比较特殊的Map,它的每个Entry(虽然是weakRef的子类,但没啥卵用,因为他的父类中真实的对象不是他本身,而是threadlocal对象)的key都是一个弱引用:这样设计的好处是,如果这个变量也就是key不再被其他对象使用时,可以自动回收这个ThreadLocal对象,避免可能的内存泄露(注意,Entry中的value是强引用,会导致内存泄漏)。

  1. static class Entry extends WeakReference<ThreadLocal<?>> {
  2. Object value;
  3. //key就是一个弱引用
  4. Entry(ThreadLocal<?> k, Object v) {
  5. super(k);
  6. value = v;
  7. }
  8. }

image.png
虽然ThreadLocalMap中的key是弱引用,当不存在外部强引用的时候,就会自动被回收,但是Entry中的value依然是强引用。这个value的引用链见上图:
可以看到,只有当Thread被回收时,这个value才有被回收的机会,否则,只要线程不退出,value总是会存在一个强引用。但是,要求每个Thread都会退出,是一个极其苛刻的要求,对于线程池来说,大部分线程会一直存在在系统的整个生命周期内,那样的话,就会造成value对象出现泄漏的可能。
我们试想一下如果这里tlMap的key为强引用,当我们没有手动删除threadlocal,GC在回ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,ThreadLocal就不会被回收,导致内存泄漏。
所以这里将Entry中的key(ThreadLocal)设置成弱引用,即便没有手动删除,也会被回收(GC)。当key为null意味着被回收掉了。这里要注意的就是每个线程内部的tlMap不是一个喔,每个thread都有自己的tlMap。如果单个线程用到多个tl,这里tlMap只是一份喔,别被误导了。
在下一次你调用set(),get(),remove()方法的时候会被清除value的值,为什么这么说呢。看源码分析就知道了。

tl的set()方法

创建tlMap

第一次set值,会构建一个tlMap,tlMap的构建是懒加载的。当至少有一个entry实例需要放入到map中的时候,我们才创建一个map容器。
获取当前Thread中的ThreadLocalMap,如果此时是null,则用ThreadLocal实例 + value 构建一个map设置到当前线程的属性threadLocals中, 如果线程的tlMap不为null,则通过ThreadLocal对象作为key直接将ThreadLocal实例和value放到当前Thread已存在的map中(可能产生冲突)

  1. public void set(T value) {
  2. Thread t = Thread.currentThread();
  3. // 1.拿到线程的tlMap,判断当前线程tlMap的成员变量的值是否为空
  4. ThreadLocalMap map = getMap(t);
  5. if (map != null)
  6. // 3.存在则拿到ThreadLocalMap直接在新增一个entry
  7. map.set(this, value);
  8. else
  9. // 2.不存在则为此线程创建一个ThreadLocalMap
  10. createMap(t, value);
  11. }
  12. void createMap(Thread t, T firstValue) {
  13. t.threadLocals = new ThreadLocalMap(this, firstValue);
  14. }
  15. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  16. // 新建Entry数组,INITIAL_CAPACITY默认为16
  17. table = new Entry[INITIAL_CAPACITY];
  18. // hash出下标 位运算:通过魔数0x61c88647 自增 及其长度算得tab中的下脚标值
  19. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  20. // 第一次set,肯定不会出现hash冲突
  21. table[i] = new Entry(firstKey, firstValue);
  22. size = 1;
  23. // 设置扩容的阈值,值为(INITIAL_CAPACITY * 2 / 3);
  24. setThreshold(INITIAL_CAPACITY);
  25. }

魔法因数

这里需要提到的二个点:一是table的长度也就是Entry[]数组的长度是为2的n次方,二是就是这个魔数非常具有魔幻力量,private static final int HASH_INCREMENT = 0x61c88647;它可以使得我们可以达到散列防冲突的目的。

  1. /**
  2. * The table, resized as necessary.
  3. * table.length MUST always be a power of two.
  4. */
  5. private Entry[] table;
  1. public class HashCode_Magic {
  2. static int HASH_INCREMENT = 0x61c88647;
  3. /**
  4. * int i = key.threadLocalHashCode & (len-1);
  5. * 要求n必须是为2的n次方
  6. */
  7. public static void magic_hash(int n){
  8. List<Integer> list = new ArrayList<>();
  9. for (int i = 0; i < n; i++) {
  10. int nextHashCode = i * HASH_INCREMENT + HASH_INCREMENT;
  11. int index = nextHashCode & (n - 1);
  12. System.out.println(index + "\t");
  13. list.add(index);
  14. }
  15. List<Integer> collect = list.stream().sorted().collect(Collectors.toList());
  16. System.out.println(collect.toString());
  17. }
  18. public static void main(String[] args) {
  19. magic_hash(16);
  20. }
  21. }
  22. 7
  23. 14
  24. 5
  25. 12
  26. 3
  27. 10
  28. 1
  29. 8
  30. 15
  31. 6
  32. 13
  33. 4
  34. 11
  35. 2
  36. 9
  37. 0
  38. [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

hash冲突

tlMap和java.util.HashMap的实现是不同的。对于java.util.HashMap使用的是链表法来处理冲突,它是把它放在下一个槽位。而tlMap则显得简单多了,直接放在冲突的下一个槽位。而tl之所以会发生冲突是因为单个线程可能存在多个tl的可能(当然一般用法推荐使用单个static threadlocal),如果只存在一个tl虽然会进入set()方法的for循环,但这并不能算真正的冲突。因为 e = tab[i = nextIndex(i, len)] ,由于tab[nextIndex]始终等于null,所以这里for循环便只会循环一遍。
image.png

set核心流程

这里是set值最为核心的流程。先是计算出应该插入的位置

  1. 同一个线程set值其计算出i的值也就是槽的index都是一致的,必定会冲突
  2. 同一个线程多个tl则i的值很有可能不一致(但是用的是同一个tlMap,不会再次创建,看上面set源码可知)

进入set方法首先计算出index的值,如果发生“冲突”,再看entry的k是不是同一个引用(存在单个线程用多个tl的可能)如果单个线程只用了一个tl,并且tl不为null,则这里直接替换原来槽的值即可。
如果该位置的Entry已经被gc清除后(强调一下,这里是指key变null了,整个Entry还是存在的),它不急着立刻插入进去,而是在[null->staleSlot->null]之间,先把[null,staleSlot]之间所有的过期Entry给清除,然后再在[statleSlot,null]之间寻找是否有重复key的Entry(设tab[i]),顺便把[statleSlot,null]中第一个过期的Entry下标赋值给slotToExpunge,并交换tab[staleSlot]和tab[i]的位置,清除tab[i](实际上清除的就是原来的tab[staleSlot])。遇到null,则结束循环
结束循环后,终于可以在tab[staleSlot]上放入新的Entry,最后在结束前执cleanSomeSlots(expungeStaleEntry(slotToExpunge), len),目的是来重新hash右边第一个null之后的所有Entry,目的是为了防止因前面set后出现的新的null,导致其他get set获取不到本应该读取到的值的情况发生

可能有人要问了,为什么set执行后,若有新的位置上出现null,可能会导致get set失效呢?因为无论是哪种hash算法,一定会产生hash冲突,为了解决hash冲突,ThreadLocalMap采用的是开放地址法,冲突了就往后找。因此如果被清除的位置出现在冲突处,会导致产生冲突的Entry找不到,因为冲突处变null了,set在遇到null时会直接插入,map里就可能会出现两个key相同的Entry,而get时只能找到其中一个

  1. private void set(ThreadLocal<?> key, Object v){
  2. Entry[] tab = table;
  3. int len = tab.length;
  4. /**
  5. * for循环快速找到插入位置,根据hash找到数组中的一个位置,判断是否发生冲突
  6. * 如果发生冲突则就要一直往下找,直到找到可用的位置(下一个槽的Entry不为null)
  7. */
  8. int i = key.threadLocalHashCode & (len-1);
  9. for (Entry e = tab[i];
  10. e != null;
  11. e = tab[i = nextIndex(i, len)]) {
  12. // 到了循环里则说明已经冲突了
  13. ThreadLocal<?> k = e.get();
  14. // 如果是重置值则简单覆盖就行
  15. if (k == key) {
  16. e.value = value;
  17. return;
  18. }
  19. // 如果key为null则说明原来的key被回收了,(没设置成static就存在被回收的可能,多个tl)
  20. // 这个Entry就是StaleEntry也就是脏Entry,需要先清除旧value再设置新value
  21. if (k == null) {
  22. replaceStaleEntry(key, value, i);
  23. return;
  24. }
  25. }
  26. // 无冲突则会到这里,直接把这个entry放进去tab数组中去,
  27. // 其中tab是ThreadLocalMap成员变量private Entry[] table;
  28. tab[i] = new Entry(key, value);
  29. //size初始为0,set一个就加1
  30. int sz = ++size;
  31. //清理其余过期对象,若sz大于threshold(阈值就是长度的三分之二),则需要扩容,重新hash
  32. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  33. rehash();
  34. }
  35. private boolean cleanSomeSlots(int i, int n) {
  36. boolean removed = false;
  37. Entry[] tab = table;
  38. int len = tab.length;
  39. do {
  40. i = nextIndex(i, len);
  41. Entry e = tab[i];
  42. if (e != null && e.get() == null) {
  43. n = len;
  44. removed = true;
  45. i = expungeStaleEntry(i);
  46. }
  47. } while ( (n >>>= 1) != 0);
  48. return removed;
  49. }

 我们知道,map中k和v是1对1的关系,而ThreadLocalMap底层只是个Entry[]数组,我们如何维护这种1对1关系呢?replaceStaleEntry()的意义就在于此。

tl的get()方法

public T get() {
    Thread t = Thread.currentThread();
    // 1.拿到线程的成员变量tl.map
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 2.ThreadLocalMap的key就是当前ThreadLocal对象实例
        ThreadLocalMap.Entry e = map.getEntry(this);// 内存泄漏 标题下有这个方法
        if (e != null) {
            // 拿到enrty对象就意味着拿到了value,构造方法(Tl tl,T value)
            T result = (T)e.value;
            return result;
        }
    }
    // 如果map没有初始化,那么在这里初始化一下
    return setInitialValue();
}

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
面试题:什么情况下不会执行initialValue? 为什么不会执行?
答: ThreadLocal是懒加载的,当调用了get 方法之后,才会尝试执行initialValue (初始化)方法,
尝试获取一下ThreadLocal set的值,如果获取到了值,那么初始化方法永远不会执行。
/**
 * 我们调用local.get()方法拿到entry,然后enrty.value拿到set的值
 */
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // e.get() 反馈ref的引用也就是key
    if (e != null && e.get() == key)
        return e;
    else
        // 如果找不到,就会尝试清理,如果你总是访问存在的key,那么这个清理永远不会进来
        return getEntryAfterMiss(key, i, e);
}


private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        // 整个e是entry ,也就是一个弱引用
        ThreadLocal<?> k = e.get();
        // 如果找到了,就返回
        if (k == key)
            return e;
        if (k == null)
            // 如果key为null,说明弱引用已经被回收了,那么就要在这里回收里面的value了
            expungeStaleEntry(i);
        else
            // 如果key不是要找的那个,那说明有hash冲突,这里是处理冲突,找下一个entry
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 直接置空
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

tl的remove()方法

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

private void remove(ThreadLocal<?> key) {
    //使用hash方式,计算当前ThreadLocal变量所在table数组位置
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    //再次循环判断是否在为ThreadLocal变量所在table数组位置
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            //调用WeakReference的clear方法清除对ThreadLocal的弱引用
            e.clear();
            //清理key为null的元素
            expungeStaleEntry(i);
            return;
        }
    }
}

真正用来回收value的是expungeStaleEntry()方法,在remove()和set()方法中,都会直接或者间接调用到这个方法进行value的清理:
从这里可以看到,ThreadLocal为了避免内存泄露,也算是花了一番大心思。不仅使用了弱引用维护key,还会在每个操作上检查key是否被回收,进而再回收value。如果key没有被回收,自然value也就不会被回收。但是从中也可以看到,ThreadLocal并不能100%保证不发生内存泄漏。

  1. 你的get()方法总是访问固定几个一直存在的ThreadLocal,那么清理动作就不会执行。比如tl是某个类的静态成员变量private static final ThreadLocal<T> _TL _= new ThreadLocal<>();这样就一直存在ThreadLocal的强引用,虽然能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,与此同时也存在set/get方法无法回收value的隐患。除非你调用remove方法强行删除
  2. 如果你的TL没有被设置成为static则,key值在下一次GC时候就会被回收,即使thread是存活的,value存在强引用。所以严格意义上来讲在未发生下一次GC之前,这都算泄漏。知道发生了GC,key值==null,但是之后你又未调用get、set、remove方法,那么就存在内存泄漏的可能,案例可参考下面的经典案例。

因此,一个良好的习惯依然是:当你不需要这个ThreadLocal变量时,主动调用remove(),这样对整个系统是有好处的。

经典案例

   public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {
            ThreadLocal<byte[]> local = new ThreadLocal<>();
            try {
                TimeUnit.SECONDS.sleep(15);
                local.set(new byte[1024*1024*100]);
                TimeUnit.SECONDS.sleep(15);
                local.set(new byte[1024*1024*100]);
                TimeUnit.SECONDS.sleep(15);
                local.set(new byte[1024*1024*100]);
                local = null;
                TimeUnit.HOURS.sleep(1);
                //local.remove(); 加上这段代码下一次GC会全部回收300M,不加的话只会回收掉200M
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"测试线程");
        thread.start();
        Thread.currentThread().join();
    }

可以通过visualVM工具查看堆得大小,无论进行多少次GC发现其内存会回收一部分这里200。因为虽然ThreadLocal被置为了null,由于其中key是弱引用可以在垃圾回收时被回收,但是其中的value,也就是Entry中的value是强引用不会被回收。也就是Entry key是null,但是Object value并没有回收,需要手动调用get或者remove()方法进行回收,或者是该线程结束了生命周期。
TODO:为什么会发生回收,而且只是部分回收,我还需要时间研究下是为什么。
image.png
image.png

总结

threadlocal优点:

  1. 避免线程安全问题
  2. 实现线程级别的数据传递。

threadlocal缺点:

  1. 正是因为做到了线程隔离,所以子线程中不能读取到父线程的值( InheritableThreadLocal可实现)
  2. 存在内存泄漏的可能。

由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果tlMap中的key被强引用(static),同样会导致内存泄漏。所以说ThreadLocal内存泄漏的根源是Thread未死亡,持有ThreadLocalMap的强引用,如果对应key得不到回收就存在泄漏的可能。
但是使用弱引用可以多一层保障:弱引用ThreadLocal之所以会减少内存泄漏的可能是因为,下一次GC时候,下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。

文章参考: csdn文章1: 地址 csdn文章2 地址

[

](https://blog.csdn.net/qq_35190492/article/details/116431270)