ThreadLocal不过如此

2023年 8月 18日 13.8k 0

前言

在并发情况下为了保证线程安全往往会选择加锁,但是无论是哪种锁总对性能有所影响,而使用ThreadLocal可以为线程创建一个独享变量,从而避免线程间竞争的情况,达到线程安全的作用。

ThreadLocal也是面试过程当中经常会问到的,所以对于准备面试的同学也是很有必要学习ThreadLocal的。

先给出几个面试题,本文后后面会给出答案:

  • ThreadLocal是什么?
  • ThreadLocal的结构是怎么样的?
  • 使用ThreadLocal需要注意哪些问题?
  • ThreadLocalMap为什么key要设置成弱引用呢?
  • 那为什么value不设置成弱引用呢?
  • 为什么会出现内存泄漏?你是怎么发现内存泄漏的?
  • 怎么避免出现脏数据问题?
  • ThreadLocal的基本使用

    创建

  • 通过new关键字 一个ThreadLocal变量
  • ThreadLocal threadLocal = new ThreadLocal();
    
  • ThreadLocal.withInitial静态方法创建一个带有初始值的ThreadLocal

    参数是一个Supplier的函数式接口

    如果对函数式接口不了解的可以看我的之前的分享 函数式接口一文看懂

  • ThreadLocal withInitialValueThreadLocal = ThreadLocal.withInitial(()->"hello,ThreadLocal");
    

    两种方式差不多,只是第二种方式会自带一个初始值

    赋值

    赋值只需要调用ThreadLocal的set方法,就可以将值保存到ThreadLocal中

    ThreadLocal变量也是一个变量,使用上完全可以把他当作一个普通的变量来使用,只是他天生是线程安全的,因为这个变量的值不会受其他任何线程所影响

    threadLocal.set("aaa");
    

    取值

    赋值只需要调用ThreadLocal的get()方法,不需要任何参数

    threadLocal.get(); // aaa
    

    删除

    ThreadLocal和普通变量不同的地方在于不用时建议手动删除,避免内存泄露(虽然不手动删除也不一定内存泄露,但是还是建议手动删除)

    threadLocal.remove()
    

    ThreadLocal变量为什么线程独享的呢?

    原理图

    image-20230815204032996

    这是我从一文详解ThreadLocal截取过来的原理图,先大致讲一下。

    每个线程Thread内部都有一个ThreadLocalMap,为ThreadLocal赋值其实就是到线程内部的Map里插入一个以ThreadLocal变量作为key,变量值为Value的键值对,取值也是去线程内部的ThreadLocalMap中以当前的ThreadLocal变量作为key调用Map的get方法返回结果,remove则是在ThreadLocalMap中删除以ThreadLocal变量作为了Map的key的键值对。

    现在看不懂也没关系,下面就来从源码来看一下ThreadLocal的工作流程

    ThreadLocal-set方法

    流程解析

  • 取得当前的线程
  • 取得当前线程内部的ThreadLocalMap
  • 调用Map的set方法 加入一个以当前ThreadLocal变量作为key,变量值为Value的键值对
  • 如果Map还为创建则为线程创建一个ThreadLocalMap,并
  • 源码分析

    public void set(T value) {
      Thread t = Thread.currentThread(); //取得当前的线程
      ThreadLocalMap map = getMap(t);
      if (map != null) {
        map.set(this, value);  // this 这是ThreadLocal的方法,指代的就是当前的ThreadLocal
      } else {
        createMap(t, value);
      }
    }
     ThreadLocalMap getMap(Thread t) {
       return t.threadLocals; // 取得线程内部的ThreadLocalMap
    }
    // 
    void createMap(Thread t, T firstValue) {
      t.threadLocals = new ThreadLocalMap(this, firstValue); // 创建Map 并加入键值对
    }
    

    get方法

    流程解析

  • 取得当前的线程
  • 取得当前线程内部的ThreadLocalMap
  • 调用ThreadLocalMap的get方法返回结果
  • 如果线程内ThreadLocalMap还未创建或者ThreadLocalMap内还未保存当前ThreadLocal的键值对,则调用初始化方法setInitialValue(),如果有初始化方法则初始化并返回初始值,没有返回null
  • 源码分析

    public T get() {
      Thread t = Thread.currentThread(); //取得当前的线程
      ThreadLocalMap map = getMap(t); //取得当前线程内部的ThreadLocalMap
      if (map != null) { //程内ThreadLocalMap还未创建
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) { // ThreadLocalMap内还未保存当前ThreadLocal的键值对
          @SuppressWarnings("unchecked")
          T result = (T)e.value;
          return result;
        }
      }
      return setInitialValue(); // 有初始化方法则初始化并返回初始值,没有返回null
    }
    

    remove方法

    流程解析

  • 取得当前的线程并取得当前线程内部的ThreadLocalMap
  • 如果ThreadLocalMap存在,则调用ThreadLocalMap的remove方法删除以当前ThreadLocal变量作为key的键值对
  • 源码分析

      public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread()); //取得当前的线程并取得当前线程内部的ThreadLocalMap
             if (m != null) {
                 m.remove(this); //删除以当前ThreadLocal变量作为key的键值对
             }
         }
    

    从上面的部分其实已经基本了解了ThreadLocal的工作原理了,但是你会发现他所有set、get、remove其实都是调用了线程内部那个ThreadLocalMap的方法,所以下面我们就更深度的解析一下ThreadLocalMap的源码,面试经常问到的内存泄露问题在了解了源码之后也很好理解了。

    ThreadLocalMap源码解析

    ThreadLocalMap的源码有两个原因导致其比较复杂

  • ThreadLocalMap内部却是使用的开放地址法中的线性探测法来处理的Hash冲突

  • ThreadLocalMap为了尽可能避免内存泄露,所以Entry的Key值(ThreadLocal)使用的是弱引用,也就是说随时都有可能存在Map里某个Entry的Key被GC回收了变成了null值,他在每次get、set和remove操作时都需要考虑某些特殊情况下GC可能引起的错误,以及每次都会清除一部分被GC的清理掉Key值的Entry

    static class Entry extends WeakReference

  • 相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论