Java ThreadLocal的使用详解

ThreadLocal是线程私有的局部变量存储容器,可以理解成每个线程都有自己专属的存储容器,用来存储线程私有变量。ThreadLocal 在日常开发框架中应用广泛,但用不好也会出现各种问题,本文就此讲解一下。

1. 应用场景

ThreadLocal 的常见应用场景有两种:

  1. 多线程并发场景中,用来保障线程安全。
  2. 处理较为复杂的业务时,使用ThreadLocal代替参数的显示传递。

1.1. 保障线程安全

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性,如:synchronized、Lock之类的锁。

ThreadLocal是除了加锁这种同步方式之外的一种,规避多线程访问出现线程不安全的方法。当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量,这样就不会存在线程不安全问题。

ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。

1.2. 显示传递参数

这里举几个例子:

示例1:获取接口的当前请求用户
在后台接口业务逻辑的全过程中,如果需要在多个地方获取当前请求用户的信息。通常的一种做法就是:在接口请求时,通过过滤器、拦截器、AOP等方式,从session或token中获取当前用户信息,存入ThreadLocal中。

在整个接口处理过程中,如果没有另外创建线程,都可以直接从ThreadLocal变量中获取当前用户,而无需再从Session、token中验证和获取用户。这种方案设计不仅提高性能,最重要的是将原本复杂的逻辑和代码实现,变得简洁明了。例如下面的这个例子:

(1)定义ThreadLocal变量:UserProfileThread.java

public class UserProfileThread {
    private static ThreadLocal<UserProfile> USER_PROFILE_TL =new ThreadLocal<>();

    public static void  setUserProfile(UserProfile userProfile){
        USER_PROFILE_TL.set(userProfile);
    }

    public static UserProfile getUserProfile() {
        return USER_PROFILE_TL.get();
    }

    public static String getCurrentUser() {
        return Optional.ofNullable(USER_PROFILE_TL.get())
                .map(UserProfile::getUid)
                .orElse(UserProfile.ANONYMOUS_USER);
    }
}

(2)在过滤器中设置变量值:

   @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        UserProfile userProfile = null;
        // ... 验证和获取用户信息 userProfile
        UserProfileThread.setUserProfile(userProfile);
        filterChain.doFilter(servletRequest, servletResponse);
    }

(3)获取当前用户信息

//获取当前用户
String uid=UserProfileThread.getCurrentUser();
//获取当前用户对象
UserProfile user=UserProfileThread.getUserProfile();

示例2:spring框架中保证数据库事务在同一个连接下执行

要想实现jdbc事务, 就必须是在同一个连接对象中操作,多个连接下事务就会不可控,需要借助分布式事务完成。那spring框架如何保证数据库事务在同一个连接下执行的呢?

DataSourceTransactionManager 是spring的数据源事务管理器,它会在你调用getConnection()的时候从数据库连接池中获取一个connection, 然后将其与ThreadLocal绑定,事务完成后解除绑定。这样就保证了事务在同一连接下完成。

2. 实现原理

ThreadLocal类提供set/get方法存储和获取value值,但实际上ThreadLocal类并不存储value值,真正存储是靠ThreadLocalMap这个类。

每个线程实例都对应一个TheadLocalMap实例,我们可以在同一个线程里实例化很多个ThreadLocal来存储很多种类型的值,这些ThreadLocal实例分别作为key,对应各自的value,最终存储在Entry table数组中。
我们看看ThreadLocal的set方法:

public class ThreadLocal<T> {
 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;
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    // 省略其他方法
}

set的逻辑比较简单,就是获取当前线程的ThreadLocalMap,然后往map里添加KV,K是当前ThreadLocal实例,V是我们传入的value。这里需要注意一下,map的获取是需要从Thread类对象里面取,看一下Thread类的定义。

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
    //省略其他
}

Thread类维护了一个ThreadLocalMap的变量引用。

因此,我们可以得出如下结论:

  1. 每个线程是一个Thread实例,其内部维护一个threadLocals的实例成员,其类型是ThreadLocal.ThreadLocalMap。
  2. ThreadLocal本身并不是一个容器,我们存取的value实际上存储在ThreadLocalMap中,ThreadLocal只是作为TheadLocalMap的key。

3. 注意事项

ThreadLocal实例有提供remove()方法,用于回收对象,清除对应的内存占用。这个方法通常容易被忽略,而导致出现了各种问题。如下面几种:

  • 线程复用:在“获取接口的当前请求用户”的例子中,Tomcat中是通过线程池来处理用户请求的,而线程池中线程是复用的。肯定会出现一个线程前后被不同用户的接口请求复用的情况,因此需要对用过的ThreaLocal变量进行覆盖或清除。
  • 内存溢出:由于ThreadLocalMap的生命周期跟Thread一样长,如果创建的ThreadLocal变量很多,即对应的key占用的内存很大,但却没有手动删除,到了一定程度就会导致内存泄漏。

以上就是Java ThreadLocal的使用详解的详细内容,更多关于Java ThreadLocal的使用的资料请关注我们其它相关文章!

(0)

相关推荐

  • java中ThreadLocal的应用场景实例分析

    说到线程的安全,我们可以通过ThreadLocal来解决.但作为一种强大的变量,它的应用场景远不止如此.在各类的框架中,我们依然可以使用来对它们进行管理.同时在使用ThreadLocal时需要注意内存泄漏的问题.下面我们就这两点进行分析,并带来对应代码的展示. 1.各种框架中的应用 Spring框架的事务管理中使用ThreadLocal来管理连接,每个线程是单独的连接,当事务失败时不能影响到其他线程的事务过程或结果,还有大家耳闻目睹的ORM框架.Mybatis也是用ThreadLocal管理,S

  • java中ThreadLocal取不到值的两种原因

    1.两种原因 第一种,也是最常见的一种,就是多个线程使用ThreadLocal 第二种,类加载器不同造成取不到值,本质原因就是不同类加载器造成多个ThreadLocal对象 public class StaticClassLoaderTest { protected static final ThreadLocal<Object> local = new ThreadLocal<Object>(); //cusLoader加载器加载的对象 private Test3 test3;

  • Java中ThreadLocal的一些理解

    前言 面试的时候被问到ThreadLocal的相关知识,没有回答好(奶奶的,现在感觉问啥都能被问倒),所以我决定先解决这几次面试中都遇到的高频问题,把这几个硬骨头都能理解的透彻的说出来了,感觉最起码不能总是一轮游. ThreadLocal介绍 ThreadLocal是JDK1.2开始就提供的一个用来存储线程本地变量的类.ThreadLocal中的变量是在每个线程中独立存在的,当多个线程访问ThreadLocal中的变量的时候,其实都是访问的自己当前线程的内存中的变量,从而保证的变量的线程安全.

  • JAVA开发常用类库UUID、Optional、ThreadLocal、TimerTask、Base64使用方法与实例详解

    1.UUID类库 UUID 根据时间戳实现自动无重复字符串定义 // 获取UUID public static UUID randomUUID() // 根据字符串获取UUID public static UUID fromString(String name) 应用:对文件进行自动命名处理 import java.util.UUID; class Demo { public static void main(String[] args) { System.out.println(UUID.ra

  • Java单线程ThreadLocal串值问题解决方案

    ThreadLocal ThreadLocal 适用于变量在线程间隔离,而在方法或类间共享的场景. 代码 @RestController public class ThreadLocalController { private static final ThreadLocal<String> currentUid = ThreadLocal.withInitial(() -> null); @GetMapping("bad") public Map doBad(@Re

  • Java中的ThreadLocal功能演示示例

    除了使用synchronized同步符号外,Java中的ThreadLocal是另一种实现线程安全的方法.在进行性能测试用例的编写过程中,比较简单的办法就是直接使用synchronized关键字,修饰对象.方法以及类.但是使用synchronized同步,这可能会影响应用程序的可伸缩性以及运行效率.但是如果要在多个线程之间共享对象又要保障线程安全,则除了synchronized之外没有特别适合测试的方法. Java中的ThreadLocal是实现线程安全的另一种方法,它不满足同步要求,而是通过为

  • java线程本地变量ThreadLocal详解

    介绍 ThreadLocal作为JDK1.2以来的一个java.lang包下的一个类,在面试和工程中都非常重要,这个类的主要目的是提供线程本地的变量,所以也有很多地方把这个类叫做线程本地变量 从字面理解,这个类为每个线程都创建了一个本地变量,实际上是ThreadLocal为变量在每个线程中都创建了一个副本,使得每个线程都可以访问自己内部的副本变量 通常提到多线程,都会考虑变量同步的问题,但是ThreadLocal并不是为了解决多线程共享变量同步的问题,而是为了让每个线程的变量不互相影响,相当于线

  • java中ThreadLocalRandom的使用详解

    在java中我们通常会需要使用到java.util.Random来便利的生产随机数.但是Random是线程安全的,如果要在线程环境中的话就有可能产生性能瓶颈. 我们以Random中常用的nextInt方法为例来具体看一下: public int nextInt() { return next(32); } nextInt方法实际上调用了下面的方法: protected int next(int bits) { long oldseed, nextseed; AtomicLong seed = t

  • Java ThreadLocal的使用场景总结

    使用场景1:本地变量 我们以多线程格式化时间为例,来演示 ThreadLocal 的价值和作用,当我们在多个线程中格式化时间时,通常会这样操作. ① 2个线程格式化 当有 2 个线程进行时间格式化时,我们可以这样写: import java.text.SimpleDateFormat; import java.util.Date; public class Test { public static void main(String[] args) throws InterruptedExcept

  • Java ThreadLocal用法实例详解

    本文实例讲述了Java ThreadLocal用法.分享给大家供大家参考,具体如下: 目录 ThreadLocal的基本使用 ThreadLocal实现原理 源码分析(基于openjdk11) get方法: setInitialValue方法 getEntry方法 set方法 ThreadLocalMap的set方法 replaceStaleEntry方法 cleanSomeSlots方法 rehash方法 expungeStaleEntries方法 resize方法 ThreadLocal实现

随机推荐