1. ThreadLocal是Java提供的本地存储机制,利用这个机制可以将数据缓存在线程内部,在任何时候都可以获取使用。
  2. ThreadLocal底层是通过ThreadLocalMap来实现的,每一个Thread对象中都存在一个ThreadLocalMap,Map的Key是ThreadLocal对象,Map的Value是缓存的值。
  3. 如果在线程池中使用ThreadLocal会造成内存泄露的问题,需要手动进行remove。因为线程池中的线程不会回收,而是去执行下一个任务,线程不被回收,Entry对象就不会被回收,造成了内存泄露。

    ThreadLocal原理

    image.png

    Thread中如何存储

    既然是线程的变量,自然是存在Thread对象中的一个变量了,但是它是通过ThreadLocal这个类来维护的。 ```java //与此线程相关的ThreadLocal值,由ThreadLocal这个类维护 ThreadLocal.ThreadLocalMap threadLocals = null;

//与此线程相关的可继承的ThreadLocal值,由InheritableThreadLocal类来维护 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

  1. ThreadLocal中有一个内部类来ThreadLocalMap来维护这些线程本地变量,
  2. ```java
  3. static class ThreadLocalMap {
  4. //初始容量,2的n次方
  5. private static final int INITIAL_CAPACITY = 16;
  6. //根据需要调整数组大小,2的n次方
  7. private Entry[] table;
  8. //上面Entry数组中的元素数量
  9. private int size = 0;
  10. //The next size value at which to resize Default to 0
  11. private int threshold;
  12. }

ThreadLocalMap中的Entry结构如下,是一种key为弱引用(其目的就是Entry对象在GC时容易回收)的hashmap,其中key总是ThreadLocal。

  1. static class Entry extends WeakReference<ThreadLocal<?>> {
  2. /** The value associated with this ThreadLocal. */
  3. Object value;
  4. Entry(ThreadLocal<?> k, Object v) {
  5. super(k);
  6. value = v;
  7. }
  8. }

常用方法 get

get() 此方法是ThreadLocal最重要的方法之一,该方法返回此线程局部变量的当前线程副本中的值。 大概可分为以下几步:

  1. 先获取当前线程,然后再从线程中得到ThreadLocalMap。
  2. 然后使用ThreadLocal对象的threadLocalHashCode进行散列计算,得到一个数组的index
  3. 从Table数组中得到Entry,再对比Entry的key是不是和当前的ThreadLocal相等,如果相等就返回此Entry的value
  4. 如果上一步中得到的Entry与当前ThreadLocal不相等,则会在方法getEntryAfterMiss中进行遍历Entry数组table中的每一个元素,如果找不到就返回null。而且在遍历的过程中会顺便清理一下废弃的Entry。 ```java public T get() { //获取当前线程 Thread t = Thread.currentThread();

    //从当前线程中获取ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) {

    1. //获取map中当前ThreadLocal对象对应的entry
    2. ThreadLocalMap.Entry e = map.getEntry(this);
    3. if (e != null) {
    4. @SuppressWarnings("unchecked")
    5. T result = (T)e.value;
    6. return result;
    7. }

    } return setInitialValue(); }

private Entry getEntry(ThreadLocal<?> key) { //散列计算得到Entry中当前的index int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i];

  1. //如果Entry不是null而且key等于当前
  2. // ThreadLocal对象则返回此Entry
  3. if (e != null && e.get() == key)
  4. return e;
  5. else
  6. //Entry==null 或者其key不等于当前
  7. // ThreadLocal对象,遍历其余Entry
  8. return getEntryAfterMiss(key, i, e);

}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) //如果遍历过程中发现有Entry的Key为Null, // 则清除掉作废的Entry expungeStaleEntry(i); else //计算Entry数组下一个index i = nextIndex(i, len); e = tab[i]; } return null; }

  1. <a name="ebArA"></a>
  2. ### **常用方法 set**
  3. set(T value) 此方法将此线程局部变量的当前线程副本中的值设置为指定值。set线程本地变量步骤如下:
  4. 1. 首先依然是获取此线程的ThreadLocalMap
  5. 1. Map不为null时往map中插入数据,否侧创建map并插入数据
  6. 1. 具体的set方法依然是先遍历Entry数组中所有的的Entry,然后依次对比每个Entry的key是否等于当前ThreadLocal,如果相等则直接替换现有Entry的value。如果Entry的Key为null,则立马清理废弃的Entry,并用新的Entry来替换此卡槽。
  7. 1. 如果遍历完都没有return,则在在table中相应卡槽下新建Entry对象
  8. Entry就是是ThreadLocalMap里定义的节点,它继承了WeakReference类,所以它的key是一个弱引用,定义了一个类型为Object的value,用于存放塞到ThreadLocal里的值。
  9. ```java
  10. public void set(T value) {
  11. Thread t = Thread.currentThread();
  12. ThreadLocalMap map = getMap(t);
  13. if (map != null)
  14. map.set(this, value);
  15. else
  16. createMap(t, value);
  17. }
  18. private void set(ThreadLocal<?> key, Object value) {
  19. Entry[] tab = table;
  20. int len = tab.length;
  21. int i = key.threadLocalHashCode & (len-1);
  22. for (Entry e = tab[i];
  23. e != null;
  24. e = tab[i = nextIndex(i, len)]) {
  25. ThreadLocal<?> k = e.get();
  26. //如果原Entry的key就是当前ThreadLocal对象,
  27. // 则直接替换现有value
  28. if (k == key) {
  29. e.value = value;
  30. return;
  31. }
  32. if (k == null) {
  33. // 如果Entry的Key为null, 则直接替换为新的Entry
  34. replaceStaleEntry(key, value, i);
  35. return;
  36. }
  37. }
  38. // 如果前面的遍历没有return,
  39. // 则插入新的Entry对象到对应的卡槽
  40. tab[i] = new Entry(key, value);
  41. int sz = ++size;
  42. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  43. rehash();
  44. }

常用方法 remove

remove则相对简单,直接遍历ThreadLocalMap中Entry数组table,找到对应的Entry,将Entry的key置为null,然后再清理相应的Entry。

  1. private void remove(ThreadLocal<?> key) {
  2. ...
  3. for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
  4. if (e.get() == key) {
  5. //Entry 的key置为null
  6. e.clear();
  7. // 清理对应卡槽,
  8. expungeStaleEntry(i);
  9. return;
  10. }
  11. }
  12. }

ThreadLocalMap和HashMap

ThreadLocalMap是ThreadLocal的静态内部类。特殊的Map,每个线程都有,线程创建的时候就有,生命周期和线程相同,存放Entry,key是ThreadLocal,val是保存的数据,是弱引用对象。

  1. 两者都是Key和Value的形式,但是ThreadLocalMap的Key是指定的(ThreadLocal),HashMap的是任意值。
  2. 都是使用了数组去存储数据。
  3. set或者put的值的时候,使用的哈希算法不一样。
  4. 解决哈希冲突的算法不一样。HashMap使用的是链地址法。ThreadLocalMap使用的是开放寻址法。

    解决Hash冲突

    ThreadLocalMap采用「线性探测」的方式,什么是线性探测呢?就是根「据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置」。ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
    这种方式的话如果一个线程里面有大量的ThreadLocal就会产生性能问题,因为每次都需要对这个table进行遍历,清空无效的值。所以我们在使用的时候尽可能的使用少的ThreadLocal,不要在线程里面创建大量的ThreadLocal,如果需要设置不同的参数类型我们可以通过ThreadLocal来存放一个Object的Map这样的话,可以大大减少创建ThreadLocal的数量。

    内存泄露

    ThreadLocal内存泄露是一个不确定的因素,因为它的key是一个弱引用,可能会被GC进行回收,这是就会出现一个key为null的Entry,但是它的value是一个强引用,并且是伴随着线程的生命周期的,所以线程一直存在,value就会一直占用着空间,导致内存的堆积。
    解决办法可以谨慎使用,不放一些比较大的对象,使用完成后手动remove。并且针对于这种情况 ThreadLocalMap在设计中,已经考虑到这种情况的发生,你只要调用了set()、get()、remove()方法都会调用cleanSomeSlots()、expungeStaleEntry()方法去清除key为null的value。
    1656222895876.jpg
    image.png

    1. public static void main(String[] args) throws InterruptedException {
    2. ThreadLocal<Long []> threadLocal = new ThreadLocal<>();
    3. for (int i = 0; i < 50; i++) {
    4. run(threadLocal);
    5. }
    6. Thread.sleep(50000);
    7. // 去除强引用
    8. threadLocal = null;
    9. System.gc();
    10. System.runFinalization();
    11. System.gc();
    12. }
    13. private static void run(ThreadLocal<Long []> threadLocal) {
    14. new Thread(() -> {
    15. threadLocal.set(new Long[1024 * 1024 *10]);
    16. try {
    17. Thread.sleep(1000000000);
    18. } catch (InterruptedException e) {
    19. e.printStackTrace();
    20. }
    21. }).start();
    22. }

    key为什么要设置成弱引用

    ThreadlocalMap是和线程绑定在一起的,如果这样线程没有被销毁,而我们又已经不会再某个threadlocal引用,那么key-value的键值对就会一直在map中存在,这对于程序来说,就出现了内存泄漏。
    为了避免这种情况,只要将key设置为弱引用,那么当发生GC的时候,就会自动将弱引用给清理掉,也就是说:假如某个用户A执行方法时产生了一份threadlocalA,然后在很长一段时间都用不到threadlocalA时,作为弱引用,它会在下次垃圾回收时被清理掉。
    而且ThreadLocalMap在内部的set,get和扩容时都会清理掉泄漏的Entry,内存泄漏完全没必要过于担心。

    value为什么要设置成强引用

    key作为ThreadLocal对象,不仅被弱引用,还被强引用着,因为一直在使用这个对象,这个对象就会被强引用,当长时间后,这个对象没有被使用了,这个时候是希望被回收的,并且现在他只有弱引用,所以key被回收了。
    但是value只是一个参数传递的过程,只是传递给了Thread,这时value只有弱引用,这个时候发生GC,就直接把它带走了,再去获取值得时候就会发生为空的情况。

这里假设value和threadlocal都是弱引用,假如threadlocal除了 被Entry这个弱引用所引用之外,还被强引用(使用threadLocal时threadLocal对象肯定是被强引用的,当不被使用时也就没被强引用这时候Gc可回收),但这时threadLocal.set(value),value的值没被其它对象引用,只是传递给thread,也就是value 这时只被Entry这个弱引用所引用,这时候发生gc,threadlocal不会被回收,value回被回收 ,导致通过threadlocal获得value值时获得为Null。

ThreadLocal变量

一个线程内的变量值是共享的,每个线程拥有一个变量的副本(有点和JMM同概念)当调用set方法时,会为当前线程创建一个static的ThreadLocalMap,这个Map有一个Entry数组,其中Entry的一个元素的key就是当前线程的引用,value就是设置的值。
优点:
不需要进行同步锁,提高了性能
每个变量的副本都可以控制保存在自己线程的内部,自然隔离,不会产生多线程访问共享变量的冲突问题

InheritableThreadLocal变量

是对ThreadLocal功能的扩展,可以继承值的ThreadLocal,即主线程的InheritableThreadLocal对象的值在子线程中也能根据该对象获取到,而ThreadLocal不行
注意: 在创建子线程前设置的值,子线程才能获取到

JMM与ThreadLocal

Java内存模型

Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。
JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

ThreadLocal

ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。
一般情况下, 线程如果要保证一个变量对其他线程不可见, 第一, 可以是自己的局部变量, 比如方法中的变量, 这个变量会存储在线程栈里, 对其他线程不可见. 第二, 那就可以用ThreadLocal了, 创建在ThreadLocal中的变量, 其实也是放在堆里, 你可以把他想象成堆中放了一个map, 每个线程名是key, 取变量的时候, 每个线程通过自己的名字获取变量, 这也不是自己的名字的变量当然获取不到.


1655799402971.jpg

ThreadLocal应用

浏览器可以理解为ThreadLocal思想,浏览网页的时候浏览器会把本地的Cookie发给网站,在使用谷歌登录的时候,换别的浏览器也一样需要重新登录,是因为不同浏览器之间做了隔离。
具体应用比如用户请求多的时候我们也会使用ThreadLocal存放用户数据;数据库也使用了ThreadLocal去实现,当前线程的操作都是在使用同一个Connection,保证了事务。

举个栗子

可以一个线程绑定多个threadlocal (ThreadLocalMap里面进行put,Map的Key是ThreadLocal对象,Map的Value是缓存的值。),也可以一个threadlocal绑定一个复杂类型的对象(比如Map)

  1. public static void main(String[] args) {
  2. ThreadLocal threadLocal = new ThreadLocal();
  3. threadLocal.set("我是一个粉刷匠!");
  4. threadLocal.set("我是一个程序员!");
  5. ThreadLocal threadLocal1 = new ThreadLocal();
  6. threadLocal1.set("1_我是一个粉刷匠!");
  7. threadLocal1.set("1_我是一个程序员!");
  8. System.out.println(threadLocal.get());
  9. System.out.println(threadLocal1.get());
  10. Map map = new HashMap<>();
  11. map.put("name","name");
  12. map.put("age","age");
  13. ThreadLocal threadLocal2 = new ThreadLocal();
  14. threadLocal2.set(map);
  15. Map res = (Map) threadLocal2.get();
  16. System.out.println(res.get("name"));
  17. threadLocal.remove();
  18. threadLocal1.remove();
  19. }

多数据源切换

image.png
说一下ThreadLocal - 图6

参考资料