图解,深入浅出带你理解ThreadLocal

简介: 图解,深入浅出带你理解ThreadLocal

前言

ThreadLocal 是我们在学习java时必须优先掌握的内容,而且应用场景广泛。比如以前一些项目,会把前台传的一部分参数放入ThreadLocal,随线程流转;又或者我们经常使用的Spring框架的@Transactional注解,也用到了ThreadLocal。


所以学习ThreadLocal,是一个必备,且越早学习越好的基础内容


一、ThreadLocal是什么?

ThreadLocal 是一个类,和线程有关,但并不是一个Thread。


这个类能够提供线程局部变量


也就是说这个类的很多属性都被包含在线程实例中,比如下图就是Thread的字段展示,我们可以看到有一个threadLocals,它的实际类型是ThreadLocal.ThreadLocalMap

8aa9b932576f44b4a298762f4fabedf4.png


当然,他和普通的变量有所不同。它本身是唯一的对象,你可以把它new出来,但每个线程去访问或者设置它的时候,读取和设置的并不是这个对象,而是本线程内,这个对象的副本。


这也意味着,这个对象在不同的线程中,副本的值是不一样的


二、如何使用

代码如下:

public class ThreadlocalTest {
    static ThreadLocal<String> mylocalVar = new ThreadLocal<>();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                mylocalVar.set("ABCDE");
                //打印本地变量
                System.out.println("thread1 val: " + mylocalVar.get());
            }
        });
        t1.start();
        Thread.sleep(1000);
        System.out.println("main thread val: " + mylocalVar.get());
    }
}

我们先new了这么一个ThreadLocal 出来,然后使用一个线程去set一个值,然后分别在该线程和主线程内读取这个ThreadLocal,看看结果

a14a89a4220149c888b9af5defb983b3.png


答案不出所料,t1 线程设置的值,只有 t1 自己能看到。主线程是看不到的。


这里的要点有二个:


  • ThreadLocal 和集合类一样,在创建时需要指定类型,上如图就指定的 String 类型
  • ThreadLocal 的读写和设置不是用的等于号 , 而是要使用该ThreadLocal 的 set 和 get 方法

三、ThreadLocal 实现及图解

1. 源码解析

public class ThreadLocal<T> {
    public T get() {
      Thread t = Thread.currentThread();  // 任何一个线程查询值时,都会获取当前线程
           // 返回当前线程里的map属性,线程类Thread中,有一个名为threadLocals,
           // 类型为ThreadLocalMap的成员变量,返回该成员变量
      ThreadLocalMap map = getMap(t);  
      if (map != null) {
                   // 这个成员变量实际就是个HsahMap,把本TreadLocal对象传进去,
                   // 即上文的mylocalVar对象,可返回该线程下+该treadlocal对象的存储值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                    @SuppressWarnings("unchecked")
              T result = (T)e.value;
              return result;
            }
      }
    return setInitialValue();
    }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    public void set(T value) {
      Thread t = Thread.currentThread();  
      ThreadLocalMap map = getMap(t);  
      if (map != null)
             map.set(this, value);
      else
           createMap(t, value);
    }
    ThreadLocalMap getMap(Thread t) {
      return t.threadLocals;
    }
}
public class Thread implements Runnable {
    //...
    // 此处类型为ThreadLocal类下的静态内部类ThreadLocalMap
    ThreadLocal.ThreadLocalMap threadLocals = null;   
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
public class ThreadLocal<T> {
    static class ThreadLocalMap {
        private Entry[] table;  //重要,最终的数据是存储在一个Entry数组中
        static class Entry extends WeakReference<ThreadLocal<?>> {
          Object value;
          Entry(ThreadLocal<?> k, Object v) {
              super(k);  //仅有key值使用了super(),即为key创建弱引用
              value = v;
          }
        }
        //...
        private Entry getEntry(ThreadLocal<?> key) {
          int i = key.threadLocalHashCode & (table.length - 1);
          Entry e = table[i];
          if (e != null && e.get() == key)
              return e;
          else
              return getEntryAfterMiss(key, i, e);
        }
        private void set(ThreadLocal<?> key, Object value) {
        }
        private void remove(ThreadLocal<?> key) {
      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)]) {
            if (e.get() == key) {
               e.clear();
               expungeStaleEntry(i);
               return;
            }
          }
        }
    }
}

2. 结构图解

我们先借用一张很好的图:每个线程里有map,map的结构我们都懂,就是一个个Entry构成的数组。而每一个Entry我们也是知道的,包含key 和 value。所谓的key,就是我们 new 出来的threadlocal对象。

515fc026390045e39940817d8e560628.png

结构解析:


该功能的实现其实由两部分组成,1. 一部分是每一个线程对象里都持有一个map对象,所以不同线程里有不同线程对象,不同对象互不干扰;2. 另一个部分是每定义一个threadlocal对象,这个对象都会作为key用来帮助每个线程在map里定位;


从上面的结构图,我们已经窥见ThreadLocal的核心机制:


  • 我们new出来的threadlocal对象,仅仅作为一个key
  • 每个Thread线程内部都有一个Map,Map里面存储threadlocal对象(key)和本线程的对该threadlocal对象设定的值(value)

所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。


3. 弱引用

ThreadLocalMap的内部类 Entry 被设计为实现了WeakReference,Entry用来存放数据。在构造Entry对象时,将传进来的ThreadLocal (key)对象包装成了弱引用对象,而Entry对象和内部的value对象之间是强引用的


217a22c56b224ae1a21f79bc2ca18bc2.png


四、内存泄漏

我们上面说了 ThreadLocal 里使用了弱引用,为什么这里要弄出这个特殊设计呢? 我们继续分析


因为目前程序运行大多采用的是线程池模式,线程存在时间很长,如果不断往其中加入线程私有对象而得不到回收,会导致OOM。所以为了减少程序员手动回收,同时兼顾避免OOM,设计了一套弱引用自动回收机制。


当使用 mylocalVal = null 的时候,断绝 ClassA 对象和 mylocalVal 对象的关系,这样由线程持有的mylocalVal 就只有一个来自Entry的虚引用(虚线部分)了,我们知道,仅有虚引用的对象会被自动回收:

d69ede683e4e45c5aabebd4847247ff3.png


但是,需要注意的是,尽管采用了Entry—弱引用—key,来保证当 mylocalVal 对象置空时,回收Entry中的key,但此时 Entry 以及 value仍然存在,依然存在泄露的可能

296161239ec74e7e807dcfa031cff24a.png

因此当使用get()、set()这些方法时时,本线程会遍历ThreadLocalMap里面的Entry,把key为Null的Entry及value置为Null,这样下次GC就可以回收掉了,但如果本线程后续没有使用get()、set()怎么办呢?自然是无法回收了。


所以,我们在使用时,如有需要,应该显式的清除ThreadLocal中数据,即 mylocalVar.remove(), 从本线程种清除该entry


另一个点:threadLocalAge一般建议是类变量,成员变量亦可,但可能导致创建多个threadLocalAge实例,不影响使用,因为ThreadLocal变量,以obj.get() obj.set()为方法,所以存取时只要是同一个对象就行,多对象不过多占用了一些内存


另外使用线程池时,线程会复用,使用get()会获取到上次的值。需要特别注意及时清理


五、inheritableThreadLocals

我们上面说的内容其实主要是Threadlocal.ThreadLocalMap 这个类型的结构。然而在Thread中,使用这个map 结构的却有两个字段

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

其中我们对threadlocal做设置和访问时,其实使用到的是上述的 threadlocals 属性,那另一个map,即inheritableThreadLocals 字段又是干什么用的呢?


顾名思义inheritable 就是可遗传的,在这里就是说明threadlocals无法遗传,而该inheritableThreadLocals 段的值能被子线程获取


我们把最开始的代码略微改动,new出InheritableThreadLocal,然后使用主线程赋值,子线程也进行读取和改动

public class ThreadlocalTest {
    static InheritableThreadLocal<String> mylocalVar = new InheritableThreadLocal<>();
    public static void main(String[] args) throws InterruptedException {
        mylocalVar.set("ABCDE");
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //打印本地变量
                System.out.println("thread1 val: " + mylocalVar.get());
                mylocalVar.set("FGHIJ");
                System.out.println("thread1 val: " + mylocalVar.get());
            }
        });
        t1.start();
        Thread.sleep(2000);
        System.out.println("main thread val: " + mylocalVar.get());
    }
}

结果没有意外,主线程,和new出来的子线程最开始数据是一样的,相当于一个数据的两个备份,但是要注意的是:除了子线程创建的瞬间,子线程复制了父线程的一个备份,后面父子线程的内容是互不影响的。

9a7aa1a6a6034e128a178f6e51fb7a7e.png

当然,能够继承的原理,就在于Thread对象的初始化了,我们用主线程建立其他线程的时候,会在其他线程对象初始化的时候,读取主线程的InheritableThreadLocal,并创建一个新对象,复制父线程的数据到自己的InheritableThreadLocal里


目录
相关文章
|
存储 算法 NoSQL
还分不清 Cookie、Session、Token、JWT?看这一篇就够了
Cookie、Session、Token 和 JWT(JSON Web Token)都是用于在网络应用中进行身份验证和状态管理的机制。虽然它们有一些相似之处,但在实际应用中有着不同的作用和特点,接下来就让我们一起看看吧,本文转载至http://juejin.im/post/5e055d9ef265da33997a42cc
47374 13
|
6月前
|
存储 安全 Java
ThreadLocal - 原理与应用场景详解
ThreadLocal是Java中用于实现线程隔离的重要工具,为每个线程提供独立的变量副本,避免多线程数据共享带来的安全问题。其核心原理是通过 ThreadLocalMap 实现键值对存储,每个线程维护自己的存储空间。ThreadLocal 广泛应用于线程隔离、跨层数据传递、复杂调用链路的全局参数传递及数据库连接管理等场景。此外,InheritableThreadLocal 支持子线程继承父线程的变量值,而 TransmittableThreadLocal 则解决了线程池中变量传递的问题,提升了多线程上下文管理的可靠性。深入理解这些机制,有助于开发者更好地解决多线程环境下的数据隔离与共享挑战。
1168 43
|
11月前
|
存储 安全 Java
深入理解ThreadLocal:线程局部变量的机制与应用
在多线程编程中,`ThreadLocal`变量提供了一种线程安全的解决方案,允许每个线程拥有自己的变量副本,从而避免了线程间的数据竞争。本文将详细介绍`ThreadLocal`的工作原理、使用方法以及在实际开发中的应用场景。
254 3
|
存储 前端开发 Java
深入剖析ThreadLocal使用场景、实现原理、设计思想
深入剖析ThreadLocal使用场景、实现原理、设计思想
深入剖析ThreadLocal使用场景、实现原理、设计思想
|
11月前
|
存储 缓存 安全
ConcurrentHashMap的实现原理,非常详细,一文吃透!
本文详细解析了ConcurrentHashMap的实现原理,深入探讨了分段锁、CAS操作和红黑树等关键技术,帮助全面理解ConcurrentHashMap的并发机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
ConcurrentHashMap的实现原理,非常详细,一文吃透!
|
缓存 监控 安全
Spring AOP 详细深入讲解+代码示例
Spring AOP(Aspect-Oriented Programming)是Spring框架提供的一种面向切面编程的技术。它通过将横切关注点(例如日志记录、事务管理、安全性检查等)从主业务逻辑代码中分离出来,以模块化的方式实现对这些关注点的管理和重用。 在Spring AOP中,切面(Aspect)是一个模块化的关注点,它可以跨越多个对象,例如日志记录、事务管理等。切面通过定义切点(Pointcut)和增强(Advice)来介入目标对象的方法执行过程。 切点是一个表达式,用于匹配目标对象的一组方法,在这些方法执行时切面会被触发。增强则定义了切面在目标对象方法执行前、执行后或抛出异常时所
16487 4
|
Java 编译器 Spring
面试突击78:@Autowired 和 @Resource 有什么区别?
面试突击78:@Autowired 和 @Resource 有什么区别?
15133 6
|
存储 Java C++
JVM内存模型和结构详解(五大模型图解)
JVM内存模型和结构详解(五大模型图解)
|
XML Java 程序员
保姆级教程,手把手教你实现SpringBoot自定义starter
保姆级教程,手把手教你实现SpringBoot自定义starter
13031 2
保姆级教程,手把手教你实现SpringBoot自定义starter
|
存储 Java 编译器
【JVM】深入了解JVM方法区
【JVM】深入了解JVM方法区
371 0