【JUC基础】14. ThreadLocal

简介: 一般提到多线程并发总是要说资源竞争,线程安全。而通常保证线程安全的其中一种方式便是控制资源的访问,也就是加锁。其实还有另一种方式,那么便是增加资源来保证所有对象不竞争少数资源。比如,有100个人需要填写信息表,如果只有一只笔,那么要么变成串行,一个一个填写,要么就是我写一半你写一半。那么如果准备100只笔,100个人每个人都有一只笔能够填写信息表,那么就不会出现竞争的情况,也就能顺利的保证信息表的填写。这支笔也就是我们今天要说的ThreadLocal。

 目录

1、前言

2、什么是ThreadLocal

3、ThreadLocal作用

4、ThradLocal基本使用

4.1、创建和初始化

4.2、存储和获取线程变量

4.3、清理和释放线程变量

4.4、小结

4.5、示例代码

5、ThreadLocal原理

5.1、set()

5.2、get()

5.3、变量清理

5.4、ThreadLocalMap

6、InheritableThreadLocal


1、前言

一般提到多线程并发总是要说资源竞争,线程安全。而通常保证线程安全的其中一种方式便是控制资源的访问,也就是加锁。其实还有另一种方式,那么便是增加资源来保证所有对象不竞争少数资源。比如,有100个人需要填写信息表,如果只有一只笔,那么要么变成串行,一个一个填写,要么就是我写一半你写一半。那么如果准备100只笔,100个人每个人都有一只笔能够填写信息表,那么就不会出现竞争的情况,也就能顺利的保证信息表的填写。这支笔也就是我们今天要说的ThreadLocal。

image.png

2、什么是ThreadLocal

ThreadLocal类是Java中的一个线程局部变量。它的作用是使得每个线程都拥有一个独立的变量副本,每个线程可以对自己的变量副本进行修改,但不会影响到其他线程的变量副本。

image.png

也就是说,只有当前线程可以访问,既然只有当前线程可以访问,那就必然是线程安全的。

3、ThreadLocal作用

ThreadLocal的主要作用是在多线程环境下,为每个线程提供一个独立的变量副本,以实现线程间的数据隔离。它具有以下几个常见的用途:

    1. 线程封闭性:通过将变量存储在ThreadLocal中,可以将其限制在单个线程内部,避免了线程安全性问题。每个线程都可以独立地修改和访问自己的副本,而不会干扰其他线程。
    2. 线程上下文传递:在某些情况下,需要在线程之间传递上下文信息,例如在Web应用中传递请求信息、用户身份认证等。使用ThreadLocal可以在不修改方法签名的情况下,将上下文信息存储在ThreadLocal中,从而在同一个线程的不同方法中共享这些信息。
    3. 避免参数传递的开销:在某些场景下,多个方法需要共享相同的数据,如果每次都通过方法参数传递这些数据会增加代码的复杂性和开销。使用ThreadLocal可以避免显式参数传递,将数据存储在ThreadLocal中,使得多个方法可以方便地访问和修改这些数据。

    4、ThradLocal基本使用

    4.1、创建和初始化

    通过ThreadLocal类的构造函数来创建ThreadLocal对象,例如:

    ThreadLocal<String> threadLocal = new ThreadLocal<>();

    image.gif

    在创建ThreadLocal对象后,可以使用set()方法来初始化线程本地变量,例如:

    threadLocal.set("Hello, ThreadLocal!");

    image.gif

    4.2、存储和获取线程变量

    存储线程本地变量是通过ThreadLocal对象的set()方法实现的,每个线程都有自己的线程本地变量副本。例如,在一个线程中存储线程本地变量:

    threadLocal.set("Value stored in ThreadLocal");

    image.gif

    可以使用get()方法来获取线程本地变量的值,例如:

    String value = threadLocal.get();

    image.gif

    注意,每个线程都只能访问和修改自己的线程本地变量,而无法直接访问其他线程的副本。这种线程间的数据隔离确保了线程安全性。

    4.3、清理和释放线程变量

    在使用完线程本地变量后,需要及时清理和释放,以避免内存泄漏和潜在的问题。可以使用ThreadLocal的remove()方法来清理线程本地变量,例如:

    threadLocal.remove();

    image.gif

    另外,为了防止内存泄漏,最好将ThreadLocal对象定义为静态变量,或者使用ThreadLocal的静态工厂方法initialValue()来初始化ThreadLocal对象。这样可以确保在使用完线程本地变量后,及时清理ThreadLocal对象的引用,从而避免对线程的引用导致的内存泄漏。

    至于为什么会内存泄露,我们稍后讲到。

    4.4、小结

    基本使用步骤可以归结为:

      • 创建ThreadLocal对象后,使用set()方法存储线程本地变量。
      • 使用get()方法获取线程本地变量的值。
      • 使用remove()方法清理线程本地变量,避免内存泄漏。
      • 将ThreadLocal对象定义为静态变量或使用initialValue()方法来初始化ThreadLocal对象,以避免内存泄漏。

      4.5、示例代码

      先来看一段代码:

      public class ThreadLocalTest {
          private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
          public static void main(String[] args) {
              ExecutorService executorService = Executors.newFixedThreadPool(10);
              for (int i = 1; i <= 10; i++) {
                  executorService.execute(() -> {
                      Date date = null;
                      try {
                          date = SIMPLE_DATE_FORMAT.parse("2023-06-03 10:00:00");
                      } catch (ParseException e) {
                          e.printStackTrace();
                      }
                      System.out.println(date);
                  });
              }
          }
      }

      image.gif

      执行结果:

      image.png

      我们可以看到一些报错,这些报错可能不会复现。原因是SimpleDateFormat并不是线程安全的,因此在线程池中共享这个对象实例必然会有线程安全问题。

      那么结合前面介绍的思路,是否可以使用ThreadLocal为每个线程创造一个SimpleDateFormat对象实例,从而解决线程安全问题。

      代码:

      public class ThreadLocalTest {
      //    private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
          // 构建ThreadLocal对象
          private static ThreadLocal<SimpleDateFormat> formatThreadLocal = new ThreadLocal<>();
          public static void main(String[] args) {
              ExecutorService executorService = Executors.newFixedThreadPool(10);
              for (int i = 1; i <= 10; i++) {
                  executorService.execute(() -> {
                      Date date = null;
                      try {
                          // 如果当前实例不存在,则初始化
                          if(formatThreadLocal.get() == null){
                              formatThreadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
                          }
                          // 从线程副本中获取SimpleDateFormat对象
                          date = formatThreadLocal.get().parse("2023-06-03 10:00:00");
                      } catch (ParseException e) {
                          e.printStackTrace();
                      } finally {
                          // 用完记得销毁
                          formatThreadLocal.remove();
                      }
                      System.out.println(date);
                  });
              }
          }
      }

      image.gif

      执行结果:

      image.png

      需要注意的是:上述代码中可以看到,为每一个线程分配一个对象的工作并不是由ThreadLocal完成,而是需要在应用层保证。如果在应用上为每一个线程分配了相同的对象实例,那么ThreadLocal也未必能保证线程安全

      5、ThreadLocal原理

      image.png

      要了解ThreadLocal的实现原理,我们主要关注的是set()和get()方法。

      5.1、set()

      /**
       * Sets the current thread's copy of this thread-local variable
       * to the specified value.  Most subclasses will have no need to
       * override this method, relying solely on the {@link #initialValue}
       * method to set the values of thread-locals.
       *
       * @param value the value to be stored in the current thread's copy of
       *        this thread-local.
       */
      public void set(T value) {
          // 获取到当前线程
          Thread t = Thread.currentThread();
          // 从当前线程中获取ThreadLocalMap
          ThreadLocalMap map = getMap(t);
          if (map != null) {
              map.set(this, value);
          } else {
              createMap(t, value);
          }
      }
      /**
       * Get the map associated with a ThreadLocal. Overridden in
       * InheritableThreadLocal.
       *
       * @param  t the current thread
       * @return the map
       */
      ThreadLocalMap getMap(Thread t) {
          return t.threadLocals;
      }

      image.gif

      从源码中可以看到,当进行set时,首先获取的是当前线程对象,然后通过getMap()方法拿到线程中的ThreadLocalMap,并将值存入 ThreadLocalMap 中。而 ThreadLocalMap 可以理解为一个 Map (可以把它简单地理解成 HashMap),但是它是定在 Thread 内部的成员。可以看Thread的源码有这样的一个成员变量定义:

      /* ThreadLocal values pertaining to this thread. This map is maintained
       * by the ThreadLocal class. */
      ThreadLocal.ThreadLocalMap threadLocals = null;

      image.gif

      而当set操作时,也正是写入了 threadLocals 的这个 Map。其中,key为ThreadLocal当前对象,value 就是我们需要的值(如上面示例代码中的SimpleDateFormat对象实例)。而 threadLocals 本身就保存了当前自己所在线程的所有“局部变量”,也就是一个 ThreadLocal 变量的集合。

      5.2、get()

      /**
       * Returns the value in the current thread's copy of this
       * thread-local variable.  If the variable has no value for the
       * current thread, it is first initialized to the value returned
       * by an invocation of the {@link #initialValue} method.
       *
       * @return the current thread's value of this thread-local
       */
      public T get() {
          // 获取当前线程
          Thread t = Thread.currentThread();
          // 当前线程中获取map
          ThreadLocalMap map = getMap(t);
          if (map != null) {
              // 获取对象实例
              ThreadLocalMap.Entry e = map.getEntry(this);
              if (e != null) {
                  @SuppressWarnings("unchecked")
                  T result = (T)e.value;
                  return result;
              }
          }
          return setInitialValue();
      }

      image.gif

      相应的,get()操作便是从这个Map中获取数据。当get()操作时,先获取当前线程的ThreadLocalMap对象,然后通过自己作为key取得内部的实际数据。

      5.3、变量清理

      前面我们说到,ThreadLocal这些变量是维护在Thread内部的,这也意味着只要线程不退出,这些对象的引用将一直存在。

      当线程退出时,Thread类会进行清理工作,其中就包含了ThreadLocalMap的清理:

      /**
       * This method is called by the system to give a Thread
       * a chance to clean up before it actually exits.
       */
      private void exit() {
          if (threadLocals != null && TerminatingThreadLocal.REGISTRY.isPresent()) {
              TerminatingThreadLocal.threadTerminated();
          }
          if (group != null) {
              group.threadTerminated(this);
              group = null;
          }
          /* Aggressively null out all reference fields: see bug 4006245 */
          target = null;
          /* Speed the release of some of these resources */
          // 这里清理了threadlocalMap
          threadLocals = null;
          inheritableThreadLocals = null;
          inheritedAccessControlContext = null;
          blocker = null;
          uncaughtExceptionHandler = null;
      }

      image.gif

      当使用线程池的时候,就意味着线程未必会退出(如固定大小线程池,线程总是存在)。如果这样,将一些大的对象设置到ThreadLocal中(因为实际保存在线程持有的ThreadLocalMap中),可能会导致内存泄露。

      因此,在使用ThreadLocal时,最好使用ThreadLocal.remove()将其变量移除。

      5.4、ThreadLocalMap

      前面我们说过,ThreadLocal其实就是将变量存在了ThreadLocalMap中,而ThreadLocalMap是一个类似HashMap的集合,更准确的说,其实是类似WeakHashMap类。

      ThreadLocalMap的实现使用了弱引用。JVM虚拟机在GC时,如果发现有弱引用,会立即回收。ThreadLocalMap内部由一系列Entry构成,每个Entry都是WeakReference。

      image.png

      这里的参数k就是Map的key,v就是Map的value。其中k也是ThreadLocal实例,作为弱引用使用。因此这里虽然使用了ThreadLocal作为Map的key,但实际上他并不真的持有ThreadLocal引用。而当ThreadLocal的外部强引用被回收时,ThreadLocalMap中的key就会变成null。当系统对ThreadLocalMap清理时,就会将这些垃圾数据进行回收。

      image.png

      而正因为是弱引用,就导致了 ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

      解决内存泄露的方法就是:在使用完之后即使的调用remove()清理掉他。

      6、InheritableThreadLocal

      ThreadLocal是让每个线程的读取ThreadLocal的变量都是独立的。那既然是这样的话,自然的线程间通信就成了问题。比如子线程需要读取父线程的变量:

      public static void main(String[] args) {
          ThreadLocal<String> threadLocal = new ThreadLocal<>();
          threadLocal.set("Hello, InheritableThreadLocal!");
          Thread thread = new Thread(() -> {
              String value = threadLocal.get();
              System.out.println("Value in child thread: " + value);
          });
          thread.start();
      }

      image.gif

      执行结果:

      image.png

      显然子线程获取到的是null。

      使用InheritableThreadLocal:

      public static void main(String[] args) {
          InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
          inheritableThreadLocal.set("Hello, InheritableThreadLocal!");
          Thread thread = new Thread(() -> {
              String value = inheritableThreadLocal.get();
              System.out.println("Value in child thread: " + value);
          });
          thread.start();
      }

      image.gif

      执行结果:

      image.png

      我们可以看到子线程已经正常获取到父线程的变量。

      InheritableThreadLocal通过在子线程创建时,将父线程的线程本地变量副本复制到子线程中,实现了父线程与子线程之间线程本地变量的传递。这种特性对于一些场景非常有用,例如在父线程中设置一些上下文信息,然后在子线程中继续使用这些上下文信息。

      使用InheritableThreadLocal的步骤与ThreadLocal类似。可以通过InheritableThreadLocal类的构造函数创建InheritableThreadLocal对象,然后使用set()方法设置线程本地变量,使用get()方法获取线程本地变量。子线程可以继承父线程的InheritableThreadLocal变量副本,而无需显式传递。

      需要注意的是,虽然InheritableThreadLocal可以实现父子线程之间的线程本地变量传递,但它也有一些潜在的问题,比如可能增加线程间的耦合性和复杂性,以及对于线程池中的线程可能导致意外的结果。因此,在使用InheritableThreadLocal时,需要谨慎考虑使用场景和潜在的影响。

      相关文章
      |
      3月前
      |
      存储 Java
      JUC并发编程之深入理解ThreadLocal
      ThreadLocal是Java标准库提供的一个工具类,位于java.lang包下。它允许你创建一个线程局部变量,每个线程都可以独立地访问自己的变量副本,互不干扰。这在某些场景下非常有用,比如在多线程环境下,每个线程需要维护自己的状态信息,但又不想通过方法参数传递的方式来实现。
      |
      9月前
      |
      安全 Java 数据安全/隐私保护
      JUC基础(四)—— 并发集合
      JUC基础(四)—— 并发集合
      87 0
      |
      11月前
      |
      存储 安全 Java
      【并发编程】ThreadLocal详解
      【并发编程】ThreadLocal详解
      |
      程序员 Java 安全
      【JUC基础】03. 几段代码看懂synchronized
      程序员经常听到“并发锁”这个名词,而且实际项目中也确实避免不了要加锁。那么什么是锁?锁的是什么?今天文章从8个有意思的案例,彻底弄清这两个问题。
      225 0
      【JUC基础】03. 几段代码看懂synchronized
      |
      安全 Java
      《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全(二)
      《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全
      《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全(二)
      《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全(三)
      《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全
      《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全(三)
      |
      安全 容灾 Java
      《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全(一)
      《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全
      《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全(一)
      |
      缓存 安全 Java
      如何使用ThreadLocal避免线程安全问题?
      这篇文章是关于ThreadLocal的第二篇文章。 在上一篇文章,Yasin给大家介绍了什么是ThreadLocal,以及ThreadLocal的基本原理。 那在实际工作中,ThreadLocal一般用来做什么呢?今天我们以一个简单的应用场景为例,给大家介绍如何用ThreadLocal来帮助我们解决多线程的安全问题。
      363 0
      |
      存储 缓存 Java
      JUC并发编程——ThreadLocal
      JUC并发编程——ThreadLocal
      114 0
      JUC并发编程——ThreadLocal
      |
      SQL Java 数据安全/隐私保护
      JUC(三)ThreadLocal
      JUC(三)ThreadLocal
      JUC(三)ThreadLocal