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

news2025/1/16 8:48:28

在通常的业务开发中,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 直接获取到,避免了传参

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/198163.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【内网安全-隧道搭建】内网穿透_Frp上线、测试

目录 Frp&#xff08;简易上线&#xff09; 1、简述&#xff1a; 2、工具&#xff1a; 3、使用&#xff1a; 1、准备&#xff1a; 2、服务端&#xff08;公网&#xff09;&#xff1a; 2、客户端&#xff08;内网&#xff09;&#xff1a; 3、测试方法&#xff1a; 4、…

【Linux】基础IO --- 软硬链接、acm时间、动静态库制作、动静态链接、动静态库加载原理…

我用执着烧死了所有的幼稚和任性&#xff0c;那片荒野慢慢长出了理智冷漠和清醒。 文章目录一、软硬链接1.软硬链接的区别&#xff08;是否具有独立的inode&#xff09;2.软硬链接的作用2.1 软链接作用&#xff08;建立快捷方式&#xff09;2.2 硬链接作用&#xff08;防止误删…

PLC信号处理系列之滤波器设计(MATLAB滤波器设计工具箱介绍)

在学习和应用滤波器解决工程问题之前,需要了解一定的信号处理相关基础知识,有关信号和系统的学习笔记可以参看下面的专栏: https://blog.csdn.net/m0_46143730/article/details/128788864https://blog.csdn.net/m0_46143730/article/details/128788864命令行窗口输入>&g…

排序模型进阶-FTRL

5.8 排序模型进阶-FTRL 学习目标 目标 无应用 无 5.8.1 问题 在实际项目的时候&#xff0c;经常会遇到训练数据非常大导致一些算法实际上不能操作的问题。比如在推荐行业中&#xff0c;因为DSP的请求数据量特别大&#xff0c;一个星期的数据往往有上百G&#xff0c;这种级别…

combit Report Server 28

combit Report Server 28 Added Microsoft SharePoint Online task action.Added support for PostgreSQL 15.Added support for Microsoft SQL Server 2022.Added new option to Microsoft Excel export template. Converting texts starting with “” into Excel formula.Im…

力扣:除自身以外数字的乘积(详解)

前言&#xff1a;本期是关于除自身以外数字的乘积的详解&#xff0c;内容包括四大模块&#xff1a;题目&#xff0c;代码实现&#xff0c;大致思路&#xff0c;代码解读&#xff0c;今天你c了吗&#xff1f; 题目&#xff1a; 给你一个整数数组 nums&#xff0c;返回 数组 ans…

【javaEE】文件

目录 文件概念 文件路径 绝对路径 相对路径 文件类型 文本文件 二进制文件 Java中对文件的操作 对文件系统的操作 get相关方法 文件类型判断和创建 文件删除 文件目录的创建 文件重命名 对文件内容的操作 字符流(操作字符数据) 代码例子 删除文件 复制文件 …

算法刷题-求素数、数据流的中位数、不同的二叉搜索树

求素数、数据流的中位数、不同的二叉搜索树求素数数据流的中位数不同的二叉搜索树求素数 求1-100内的素数&#xff1a; public static void main(String[] args){for(int i0;i<100;i) {checkPrime(i);}}private static void checkPrime(int x){boolean isPrime true;if(x…

nodejs+vue大学生提问论坛系统vscode

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 前端技术&#xff1a;nodejsvueelementui,视图层其实质就是vue页面&#xff0c;通过编写vue页面从而展示在浏览器中&#xff0c;编写完成的vue页面…

Linux-时间和日期、磁盘信息和进程信息

为了方便通过远程终端维护服务器时&#xff0c;查看服务器上当前系统日期和时间 /磁盘空间占用情况/程序执行情况&#xff0c;本节基本都是查询命令时间和日期datecal磁盘和目录空间dfdu进程信息pstopkill1.时间和日期序号命令作用01cal查看一个月日历&#xff0c; -y选项可以查…

consul集群解决单注册弊端

consul集群前面我们用consul注册发现服务&#xff0c;这里的弊端显而易见&#xff0c;单机版如果挂掉&#xff0c;就无法正常使用。consul的集群是复制模式&#xff0c;每个节点的信息一致。当其中一个挂掉&#xff0c;其他正常运行。consul官网建议最好是3个节点&#xff0c;其…

MyEclipse安装及需要进行的配置

安装&#xff1a;https://blog.csdn.net/qq_30764991/article/details/82531415?spm1001.2101.3001.6650.9&utm_mediumdistribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-9-82531415-blog-126523828.pc_relevant_aa2&depth_1-utm_sou…

网络游戏与未来生存——谈一下不要因为认知不到位让孩子失去未来的机会

这个话题要从几个月前的一个事件谈起&#xff0c;先看看下面的截图&#xff1a;于是&#xff0c;那一天&#xff0c;各种阴谋论甚嚣尘上&#xff0c;大多数的话题都是直指腾讯&#xff0c;说腾讯的活动能力太强了。随后几天网上也是各种关于网络游戏毁掉孩子未来的说法和观点&a…

2022区块链投融资鸟瞰:总额超千亿元,数字货币坚挺,Web3叙事强劲

文/尹宁出品/陀螺研究院从整个区块链行业来看&#xff0c;2022年&#xff0c;在全球通胀与投资受限的环境下&#xff0c;资本圈与区块链之间可谓一波三折。在TMT、新消费、生物医药等赛道的各种消极表现下&#xff0c;年初Web3与元宇宙的横空出世正中资本下怀&#xff0c;a16z、…

c++ 快排思想

文章目录讲下思想时间复杂度优化代码实现讲下思想 我这边主要是分左右两边的指针寻找 left指针从左边寻找大于p的数 right 从右寻找小于p的数 然后找到进行交换&#xff0c;直到相遇在一起 说明这个位置是分隔的位置 而p的数就是分隔的数&#xff0c;所以对p和l进行一个…

什么都不会的应届生怎么找工作?

2023年&#xff0c;很多大学应届生都会面临到的一个问题&#xff1a;疫情三年上网课没有学到什么知识和技能&#xff0c;并且学习效果大多不理想&#xff1b;现在面临就业时对自身专业技能不自信&#xff0c;怀疑自己是否能找到工作&#xff0c;更别说是适合自己喜欢的工作。面…

Vue 3.2 源码系列:有点难的《最新 diff 算法详解》

本文首发自&#xff1a;稀土掘金、思否 博客代码已上传至github 点击这里 即可访问 另提供&#xff1a;完整代码&#xff08;tsrollup&#xff09;和视频教程 序 所谓算法指的是&#xff1a;把一种数据结构转化为另外一种数据结构的方法。 在runtime(运行时)阶段存在一个无论如…

内网渗透(四)之基础知识-搭建域环境

系列文章 内网渗透(一)之基础知识-内网渗透介绍和概述 内网渗透(二)之基础知识-工作组介绍 内网渗透(三)之基础知识-域环境的介绍和优点 注&#xff1a;阅读本编文章前&#xff0c;请先阅读系列文章&#xff0c;以免造成看不懂的情况&#xff01;&#xff01; 搭建域环境 在…

DataX案例实操

1、DataX使用 1.1、DataX任务提交命令 DataX的使用十分简单&#xff0c;用户只需根据自己同步数据的数据源和目的地选择相应的Reader和Writer&#xff0c;并将Reader和Writer的信息配置在一个json文件中&#xff0c;然后执行如下命令提交数据同步任务即可。 [songhadoop102 …

JavaScript 进阶5:WebAPI:DOM- 网页特效篇

JavaScript 进阶5&#xff1a;WebAPI&#xff1a;DOM- 网页特效篇 Date: January 28, 2023 Text: 轮播图高级版待解决 滚动事件、加载事件、元素大小和位置&#xff08;scroll、offset、client&#xff09;、轮播图案例 目标 能够制作常见的网页特效具备制作常见网页焦点图的…