图解,深入浅出带你理解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里


目录
相关文章
|
19天前
|
存储 缓存 安全
ConcurrentHashMap的实现原理,非常详细,一文吃透!
本文详细解析了ConcurrentHashMap的实现原理,深入探讨了分段锁、CAS操作和红黑树等关键技术,帮助全面理解ConcurrentHashMap的并发机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
ConcurrentHashMap的实现原理,非常详细,一文吃透!
|
3月前
|
Java 调度
【多线程面试题十四】、说一说synchronized的底层实现原理
这篇文章解释了Java中的`synchronized`关键字的底层实现原理,包括它在代码块和方法同步中的实现方式,以及通过`monitorenter`和`monitorexit`指令以及`ACC_SYNCHRONIZED`访问标志来控制线程同步和锁的获取与释放。
|
存储 安全 Java
synchronized原理详解(通俗易懂超级好)
当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
synchronized原理详解(通俗易懂超级好)
|
Java 数据库连接
ThreadLocal原理和实践
ThreadLocal是线程本地变量,解决多线程环境下成员变量共享存在的问题。ThreadLocal为每个线程创建独立的的变量副本,他的特性是该变量的引用对全局可见,但是其值只对当前线程可用,每个线程都将自己的值保存到这个变量中而各线程不受影响。
162 0
ThreadLocal原理和实践
|
存储 算法 安全
【底层原理之旅—ThreadLocal深入浅出的源码分析|Java 刷题打卡
【底层原理之旅—ThreadLocal深入浅出的源码分析|Java 刷题打卡
144 0
【底层原理之旅—ThreadLocal深入浅出的源码分析|Java 刷题打卡
|
安全 Java 数据库连接
深入浅出ThreadLocal
ThreadLocal相信大家都有用过的,一般用作存取一些全局的信息。比如用户信息,流程信息,甚至在Spring框架里面通过事务注解Transactional去获取数据库连接的实现上,也有它的一份功劳。
147 0
|
安全 Java
ThreadLocal相关知识点
ThreadLocal相关知识点
109 0
ThreadLocal相关知识点
|
存储 缓存 算法
ThreadLocal从入门到精通
ThreadLocal从入门到精通
160 0
|
安全 Java
【面试题系列】CurrentHashMap的实现原理
CurrentHashMap的实现原理 JDK8 实现原理 1,实现方式:synchronized+CAS+HashEntry+红黑树 2,线程安全:内部大量采用CAS机制操作+Synchronized保证线程安全 3,数据结构:数组+链表+红黑树 4,锁颗粒度:Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性。 5.查询时间复杂度:遍历红黑树O(logN)。
139 0
|
存储 Java
关于ThreadLocal的九个知识点,看完别再说不懂了!
ThreadLocal顾名思义是保存在每个线程本地的数据,ThreadLocal提供了线程局部变量,即每个线程可以有属于自己的变量,其他线程无法访问。如果创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本。每个线程可以通过set()和get()方法去访问ThreadLocal变量。