一个博客

ThreadLocal 探究

Posted at — Aug 29, 2021

一、为什么要使用ThreadLocal

先来读一下JDK中对ThreadLocal的注释

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

该类提供线程局部变量。 这些变量与其正常的对应方式不同,因为访问一个的每个线程(通过其get或set方法)都有自己独立初始化的变量副本。 ThreadLocal实例通常是希望将状态与线程关联的类中的私有静态字段相关联(例如,用户ID或事务ID)。

个人理解就是在ThreadLocal提供了线程层面的可以提供线程隔离的变量空间,这样一些需要线程安全性的用户变量数据就可以进行隔离。

场景一:解决线程不安全的问题

ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。

从线程的角度看,这个变量就像是线程的本地变量,这也是类名中“Local”所要表达的意思。 线程局部变量并不是Java的新发明,很多语言(如IBM XL、FORTRAN)在语法层面就提供线程局部变量。在Java中没有提供语言级支持,而以一种变通的方法,通过ThreadLocal的类提供支持。所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,这也是为什么线程局部变量没有在Java开发者中得到很好普及的原因。

—摘自博客

场景如下:在线程池中创建十个线程执行时间格式化,由于SimpleDateFormat线程不安全所以产生了不符合预期的结果。

public class SimpleDateFormatTest {
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
    
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        // 执行 10 次时间格式化
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            // 线程池执行任务
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    // 创建时间对象
                    Date date = new Date(finalI * 1000);
                    // 时间格式化打印结果
                    System.out.println(simpleDateFormat.format(date));
                }
            });
        }
    }
}

我们可以采用加锁的方式解决,但是缺点也显而易见,那就是往往需要牺牲效率,此时我们可以利用ThreadLocal解决。

public class SimpleDateFormatTest {
    private static ThreadLocal<SimpleDateFormat> threadLocal =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
//初始化ThreadLocal对象存入SimpleDateFormat
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    Date date = new Date(finalI * 1000);
                    String result = threadLocal.get().format(date);
                    System.out.println(result);
                }
            });
        }
        threadPool.shutdown();
    }
}

场景二:解决不优雅的过度传递问题

ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。 ThreadLocal,顾名思义,它不是一个线程,而是线程的一个本地化对象。当工作于多线程中的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。

—摘自博客

preview

这个场景中,原作者希望使用CurrentUser中的信息,文章作者的优化如下:

具体可查看原文。总之就是,ThreadLocal不仅可以实现场景一中线程安全性的实现,同时可以通过隔离的变量空间特性,优化代码,减少不必要的参数传递。

场景三:Spring中ThreadLocal的影子

在后端开发中,ThreadLocal也有它的应用场景,Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接。由此可见ThreadLocal在Java体系中占有着举足轻重的地位。

—摘自博客

如下截取了部分源码,Spring在org.springframework.transaction.support中的类TransactionSynchronizationManager就是使用了ThreadLocal进行事物管理

package org.springframework.transaction.support;

public abstract class TransactionSynchronizationManager {
    private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
    private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");
    private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal("Transaction synchronizations");
    private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal("Current transaction name");
    private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal("Current transaction read-only status");
    private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal("Current transaction isolation level");
    private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal("Actual transaction active");
    
    .........

二、探究ThreadLocal

1.如何绑定线程?

ThreadLocal中的get方法如下,我们可以发现每个实例通过获取当前线程对象名字作为入口从而实现与线程的绑定,即数据隔离。

 public T get() {
        Thread t = Thread.currentThread();
        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();
    }

2.如何实现存储变量

打开源码我们可以发现ThreadLocal中维护了一个ThreadLocalMap的Key,Value结构,具体就是用它来实现的。具体原理可以参考最底部参考文章,看完之后应该能帮助你建立起对它的理解。

三、ThreadLocal的内存泄漏问题

我们发现,ThreadLocalMap中的Entry继承WeakReference,使ThreadLocal作为一个弱引用。我们知道,弱引用在发生GC时这个对象一定会被回收。通常来说使用弱引用是为了避免内存泄漏。这里也不例外,ThreadLocal使用弱引用可以避免内存泄漏问题的发生。但是并不代表一定不会出现内存泄漏,如果Thread一直在运行,那么此时由于强引用的value不能被回收,所以通常情况下在不需要使用到ThreadLocal时我们要手动remove

四、为什么要使用ThreadLocal而不使用线程同步机制

ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序缜密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal为每一个线程提供一个独立的变量副本,从而隔离了多个线程对访问数据的冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的对象封装,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度上简化ThreadLocal的使用,代码清单9-2就使用了JDK 5.0新的ThreadLocal版本。

概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式:访问串行化,对象共享化。而ThreadLocal采用了“以空间换时间”的方式:访问并行化,对象独享化。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

——来自博客

本文参考:

面试官:哈希表都不知道,你是怎么看懂HashMap的?

ThreadLocal在spring框架中的运用_babyZeng

Java并发系列番外篇:ThreadLocal原理其实很简单

谈谈 Java ThreadLocal 类的作用与用法、需要注意的坑

spring(基础20) threadLocal在spring框架中的运用