ThreadLocal 适合用在哪些实际生产的场景中?

2023-05-16

在通常的业务开发中,ThreadLocal有两种典型的使用场景

场景1,ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保线程安全

场景2,ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念

典型场景1

这种场景通常用于保存线程不安全的工具类,典型的需要使用的类就是 SimpleDateFormat

场景介绍

在这种情况下,每个 Thread 内都有自己的实例副本,且该副本只能由当前 Thread 访问到并使用,相当于每个线程内部的本地变量,这也是 ThreadLocal 命名的含义。因为每个线程独享副本,而不是公用的,所以不存在多线程间共享的问题

SimpleDateFormat 的进化之路

  • 2 个线程都要用到 SimpleDateFormat

假设有个需求,即 2 个线程都要用到 SimpleDateFormat。代码如下所示

public class ThreadLocalDemo01 {
 
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            String date = new ThreadLocalDemo01().date(1);
            System.out.println(date);
        }).start();
        Thread.sleep(100);
        new Thread(() -> {
            String date = new ThreadLocalDemo01().date(2);
            System.out.println(date);
        }).start();
    }
 
    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
        return simpleDateFormat.format(date);
    }
}
复制代码

在以上代码中可以看出,两个线程分别创建了一个自己的 SimpleDateFormat 对象,如图所示

这样一来,有两个线程,那么就有两个 SimpleDateFormat 对象,它们之间互不干扰,这段代码是可以正常运转的,运行结果是

  00:01
  00:02
复制代码
  • 10 个线程都要用到 SimpleDateFormat

假设我们的需求有了升级,不仅仅需要 2 个线程,而是需要 10 个,也就是说,有 10 个线程同时对应 10 个 SimpleDateFormat 对象。我们就来看下面这种写法

public class ThreadLocalDemo02 {
 
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                String date = new ThreadLocalDemo02().date(finalI);
                System.out.println(date);
            }).start();
            Thread.sleep(100);
        }
    }
 
    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
        return simpleDateFormat.format(date);
    }
}
复制代码

上面的代码利用了一个 for 循环来完成这个需求。for 循环一共循环 10 次,每一次都会新建一个线程,并且每一个线程都会在 date 方法中创建一个 SimpleDateFormat 对象,示意图如下

可以看出一共有 10 个线程,对应 10 个 SimpleDateFormat 对象。代码的运行结果

00:00
00:01
00:02
00:03
00:04
00:05
00:06
00:07
00:08
00:09
复制代码
  • 需求变成了 1000 个线程都要用到 SimpleDateFormat

但是线程不能无休地创建下去,因为线程越多,所占用的资源也会越多。假设我们需要 1000 个任务,那就不能再用 for 循环的方法了,而是应该使用线程池来实现线程的复用,否则会消耗过多的内存等资源。

在这种情况下,我们给出下面这个代码实现的方案

public class ThreadLocalDemo03 {
 
    public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
 
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalDemo03().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }
 
    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
        return dateFormat.format(date);
    }
}
复制代码

我们用了一个 16 线程的线程池,并且给这个线程池提交了 1000 次任务。每个任务中它做的事情和之前是一样的,还是去执行 date 方法,并且在这个方法中创建一个 simpleDateFormat 对象。程序的一种运行结果是(多线程下,运行结果不唯一)

00:00
00:07
00:04
00:02
...
16:29
16:28
16:27
16:26
16:39
复制代码

程序运行结果正确,把从 00:00 到 16:39 这 1000 个时间给打印了出来,并且没有重复的时间。我们把这段代码用图形化给表示出来,如图所示

图的左侧是一个线程池,右侧是 1000 个任务。我们刚才所做的就是每个任务都创建了一个 simpleDateFormat 对象,也就是说,1000 个任务对应 1000 个 simpleDateFormat 对象

但是这样做是没有必要的,因为这么多对象的创建是有开销的,并且在使用完之后的销毁同样是有开销的,而且这么多对象同时存在在内存中也是一种内存的浪费

  • 所有的线程都共用一个 simpleDateFormat 对象
public class ThreadLocalDemo04 {
 
    public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
 
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalDemo04().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }
 
    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        return dateFormat.format(date);
    }
}
复制代码

在代码中可以看出,其他的没有变化,变化之处就在于,我们把这个 simpleDateFormat 对象给提取了出来,变成 static 静态变量,需要用的时候直接去获取这个静态对象就可以了。看上去省略掉了创建 1000 个 simpleDateFormat 对象的开销,看上去没有问题,我们用图形的方式把这件事情给表示出来

从图中可以看出,我们有不同的线程,并且线程会执行它们的任务。但是不同的任务所调用的 simpleDateFormat 对象都是同一个,所以它们所指向的那个对象都是同一个,但是这样一来就会有线程不安全的问题

  • 线程不安全,出现了并发安全问题

控制台会打印出(多线程下,运行结果不唯一)

00:04
00:04
00:05
00:04
...
16:15
16:14
16:13
复制代码

执行上面的代码就会发现,控制台所打印出来的和我们所期待的是不一致的。我们所期待的是打印出来的时间是不重复的,但是可以看出在这里出现了重复,比如第一行和第二行都是 04 秒,这就代表它内部已经出错了

  • 加锁

出错的原因就在于,simpleDateFormat 这个对象本身不是一个线程安全的对象,不应该被多个线程同时访问。所以我们就想到了一个解决方案,用 synchronized 来加锁。于是代码就修改成下面的样子

public class ThreadLocalDemo05 {
 
    public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
 
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalDemo05().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }
 
    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        String s = null;
        synchronized (ThreadLocalDemo05.class) {
            s = dateFormat.format(date);
        }
        return s;
    }
}
复制代码

可以看出在 date 方法中加入了 synchronized 关键字,把 simpleDateFormat 的调用给上了锁

运行这段代码的结果(多线程下,运行结果不唯一)

00:00
00:01
00:06
...
15:56
16:37
16:36
复制代码

这样的结果是正常的,没有出现重复的时间。但是由于我们使用了 synchronized 关键字,就会陷入一种排队的状态,多个线程不能同时工作,这样一来,整体的效率就被大大降低了。有没有更好的解决方案呢?

我们希望达到的效果是,既不浪费过多的内存,同时又想保证线程安全。经过思考得出,可以让每个线程都拥有一个自己的 simpleDateFormat 对象来达到这个目的,这样就能两全其美了

  • 使用 ThreadLocal
public class ThreadLocalDemo06 {
 
    public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
 
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalDemo06().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }
 
    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return dateFormat.format(date);
    }
}
 
class ThreadSafeFormatter {
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("mm:ss");
        }
    };
}
复制代码

在这段代码中,我们使用了 ThreadLocal 帮每个线程去生成它自己的 simpleDateFormat 对象,对于每个线程而言,这个对象是独享的。但与此同时,这个对象就不会创造过多,一共只有 16 个,因为线程只有 16 个

代码运行结果(多线程下,运行结果不唯一)

00:05
00:04
00:01
...
16:37
16:36
16:32
复制代码

这个结果是正确的,不会出现重复的时间。用图来看一下当前的这种状态

在图中的左侧可以看到,这个线程池一共有 16 个线程,对应 16 个 simpleDateFormat 对象。而在这个图画的右侧是 1000 个任务,任务是非常多的,和原来一样有 1000 个任务。但是这里最大的变化就是,虽然任务有 1000 个,但是我们不再需要去创建 1000 个 simpleDateFormat 对象了。即便任务再多,最终也只会有和线程数相同的 simpleDateFormat 对象。这样既高效地使用了内存,又同时保证了线程安全

典型场景2

每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)

例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的

在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦

比如说我们是一个用户系统。假设不使用 ThreadLocal,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就会依次调用 service-1()、service-2()、service-3()、service-4(),这 4 个方法可能是分布在不同的类中的

在 service-1() 的时候它会创建一个 user 的对象,用于保存比如说这个用户的用户名等信息,后面 service-2/3/4() 都需要用到这个对象的信息,比如说 service-2() 代表下订单、service-3() 代表发货、service-4() 代表完结订单,在这种情况下,每一个方法都需要用户信息,所以就需要把这个 user 对象层层传递下去,从 service-1() 传到 service-2(),再从 service-2() 传到 service-3(),以此类推

这样做会导致代码非常冗余,那有没有什么办法可以解决这个问题呢?我们首先想到的方法就是使用一个 HashMap,如下图所示

比如说我们使用了这样的 Map 之后,就不需要把 user 对象层层传递了,而是在执行 service-1() 的时候,把这个用户信息给 put 进去,然后后面需要拿用户信息的时候,直接从静态的 User map 里面 get 就可以了。这样一来,无论你执行哪个方法,都可以直接获取到这个用户信息。当然,我们也要考虑到 web 服务器通常都是多线程的,当多个线程同时工作的时候,我们也需要保证线程安全

所以在这里,如果我们使用 HashMap 是不够的,因为它是线程不安全的,那我们就可以使用 synchronized,或者直接把 HashMap 替换成 ConcurrentHashMap,用类似的方法来保证线程安全,这样的改进如下图所示

在这个图中,可以看出有两个线程,并且每个线程所做的事情都是访问 service-1/2/3/4()。那么当它们同时运行的时候,都会同时访问这个 User map,于是就需要 User map 是线程安全的

无论我们使用 synchronized 还是使用 ConcurrentHashMap,它对性能都是有所影响的,因为即便是使用性能比较好的 ConcurrentHashMap,它也是包含少量的同步,或者是 cas 等过程。相比于完全没有同步,它依然是有性能损耗的。所以在此一个更好的办法就是使用 ThreadLocal

这样一来,我们就可以在不影响性能的情况下,也无需层层传递参数,就可以达到保存当前线程所对应的用户信息的目的。如下图所示

在这个图中可以看出,同样是多个线程同时去执行,但是这些线程同时去访问这个 ThreadLocal 并且能利用 ThreadLocal 拿到只属于自己的独享对象。这样的话,就无需任何额外的措施,保证了线程安全,因为每个线程是独享 user 对象的。代码如下所示

public class ThreadLocalDemo07 {
 
    public static void main(String[] args) {
        new Service1().service1();
 
    }
}
 
class Service1 {
 
    public void service1() {
        User user = new User("vincent");
        UserContextHolder.holder.set(user);
        new Service2().service2();
    }
}
 
class Service2 {
 
    public void service2() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service2拿到用户名:" + user.name);
        new Service3().service3();
    }
}
 
class Service3 {
 
    public void service3() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用户名:" + user.name);
        UserContextHolder.holder.remove();
    }
}
 
class UserContextHolder {
 
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}
 
class User {
 
    String name;
 
    public User(String name) {
        this.name = name;
    }
}
复制代码

在这个代码中我们可以看出,我们有一个 UserContextHolder,里面保存了一个 ThreadLocal,在调用 Service1 的方法的时候,就往里面存入了 user 对象,而在后面去调用的时候,直接从里面用 get 方法取出来就可以了。没有参数层层传递的过程,非常的优雅、方便

代码运行结果

Service2拿到用户名:vincent
Service3拿到用户名:vincent
复制代码

总结

场景1,ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,每个线程都只能修改自己所拥有的副本, 而不会影响其他线程的副本,这样就让原本在并发情况下,线程不安全的情况变成了线程安全的情况

场景2,ThreadLocal 用作每个线程内需要独立保存信息的场景,供其他方法更方便得获取该信息,每个线程获取到的信息都可能是不一样的,前面执行的方法设置了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

ThreadLocal 适合用在哪些实际生产的场景中? 的相关文章

  • Java ThreadLocal

    ThreadLocal是什么 xff1f 定义 xff1a 提供线程局部 变量 xff1b 一个线程局部边用在多个线程中分别有独立的值 xff08 副本 xff09 特点 xff1a 简单 xff08 开箱即用 xff09 快速 xff08
  • ThreadLocal的深度解读

    一 J2SE的原始描述 This class provides thread local variables These variables differ from their normal counterparts in that eac
  • ThreadLocal,看我就够了!

    ThreadLocal 开胃菜 研究过Handler的应该对ThreadLocal比较眼熟的 线程中的Handler对象就是通过ThreadLocal来存放的 初识ThreadLocal的可能被它的名字有所误导 ThreadLocal初一看
  • InheritableThreadLocal类详解

    我们在使用ThreadLocal类的时候 可以保证各个线程使用自己的数据 而不相互干扰 但是如果我们有这样的一个需求 就是各个线程相互不干扰的情况下 各个线程的子线程可以访问到当前线程中的值 对于这个子线程来说就是访问父线程 public
  • 【Java】ThreadLocal详细解析

    ThreadLocal全面解析 前置知识 具有一定的javase和javaweb基础 熟悉synchronized关键字 熟悉HashMap 熟悉 JDBC技术 学习目标 了解ThreadLocal的介绍 掌握ThreadLocal的运用场
  • C++11中thread_local的使用

    C 11中的thread local是C 存储期的一种 属于线程存储期 存储期定义C 程序中变量 函数的范围 可见性 和生命周期 C 程序中可用的存储期包括auto register static extern mutable和thread
  • ThreadLocal详解

    如果有兴趣了解更多相关内容 欢迎来我的个人网站看看 瞳孔空间 一 基本介绍 ThreadLocal类能提供线程内部的局部变量 这种变量在多线程环境下访问时能保证各个线程的变量相对独立于其他线程内的变量 ThreadLocal实例通常来说都是
  • 将 Spring 应用程序上下文传递到与请求关联的 ThreadLocal 是否安全?

    在 JPA 中我想利用PrePersist带注释的方法来执行一些操作 而不是制作实现单例模式 使用 getInstance 等 所需的东西 我想知道是否通过 ThreadLocal 我在请求后关闭释放 传递 Spring Applicati
  • ThreadLocal - 用作带有 spring-boot 的 REST API 的上下文信息

    我有一些spring boot应用程序 它公开了 REST API 提到的 REST API 是由spring security 一切都很好 但是现在我需要设置上下文 用于服务请求 设置上下文是指根据用户上下文选择数据源 关键是Routin
  • C++:处理线程本地对象销毁

    我有一个日志系统 它基本上使用线程本地缓冲区来记录 这有助于减少锁定 可以将一堆消息写入线程本地缓冲区并一次性刷新 而且由于它是线程本地的 我们可以避免为每个日志消息分配缓冲区 无论如何 问题是在进程退出期间 我们在访问线程本地缓冲区时看到
  • EJB 容器中的 ThreadLocal(和 Singleton)

    我编写了一个授权系统 它依赖于代表当前用户的对象 为了简化编程并提高性能 我想在用户登录后将这些对象保存在 ThreadLocal 中 它看起来像这样 public class UserCache private static final
  • 扩展 java 的 ThreadLocal 以允许在所有线程中重置值

    看完之后这个问题 https stackoverflow com questions 2795447 is there no way to iterate over or copy all the values of a java thre
  • .NET 中本地可继承线程

    NET 4 0 推出ThreadLocal
  • 在并行任务期间跟踪失效的 WebDriver 实例

    我看到一些使用 Selenium WebDriver 运行并行嵌套循环 Web 压力测试的死实例怪异现象 简单的例子是 比如说 点击 300 个独特的页面 每个页面有 100 次展示 我 成功 获得 4 8 个 WebDriver 实例Th
  • Java 的 ThreadLocal 底层是如何实现的?

    ThreadLocal是如何实现的 它是用 Java 实现的 使用一些从 ThreadID 到对象的并发映射 还是使用一些 JVM 钩子来更有效地完成它 这里的所有答案都是正确的 但有点令人失望 因为它们在某种程度上掩盖了如何聪明Threa
  • 如何从 Java servlet Web 容器正确实现 RabbitMQ RPC?

    我希望传入的 Java servlet Web 请求使用 RPC 方法调用 RabbitMQ 如上所述here http www rabbitmq com tutorials tutorial six java html 但是 我不确定如何
  • C++11:重要的线程局部静态变量?

    我有一个X类 class X 我想做这个 void f thread local static X x 实际上我使用的是 gcc 所以关键字是 thread 但我不能 因为你只能有微不足道的 thread locals 对此最好的解决方法是
  • 我应该何时以及如何使用 ThreadLocal 变量?

    我什么时候应该使用ThreadLocal https docs oracle com javase 8 docs api java lang ThreadLocal html多变的 它是如何使用的 一种可能 也是常见 的用途是当您有一些非线
  • 为什么 ThreadLocal 实用程序在 Spring MVC 应用程序中总是返回 null?

    我编写了这个实用程序类来在 Spring MVC 应用程序中保存临时数据 public abstract class FooUtil private static final ThreadLocal
  • 变量的同步和本地副本

    我正在查看一些具有以下习惯用法的遗留代码 Map

随机推荐