【JUC基础】14. ThreadLocal

news2024/11/29 2:54:44

目录

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。

2、什么是ThreadLocal

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

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

3、ThreadLocal作用

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

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

4、ThradLocal基本使用

4.1、创建和初始化

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

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

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

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

4.2、存储和获取线程变量

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

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

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

String value = threadLocal.get();

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

4.3、清理和释放线程变量

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

threadLocal.remove();

另外,为了防止内存泄漏,最好将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);
            });
        }
    }
}

执行结果:

我们可以看到一些报错,这些报错可能不会复现。原因是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);
            });
        }
    }
}

执行结果:

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

5、ThreadLocal原理

要了解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;
}

从源码中可以看到,当进行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;

而当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();
}

相应的,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;
}

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

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

5.4、ThreadLocalMap

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

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

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

而正因为是弱引用,就导致了 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();
}

执行结果:

显然子线程获取到的是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();

}

执行结果:

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

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

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

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

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

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

相关文章

硬链接与符号链接

硬链接与符号链接(Hard Link vs Symbolic Link) 两者对于 Linux 操作系统的异同、优缺点。 什么是链接&#xff1f; 在最一般的意义上&#xff0c;链接是两个对象之间的连接。更具体地说是在计算方面&#xff1b;根据牛津定义&#xff0c;链接是与代码或指令的连接&#xff0c…

【读书笔记】《平凡的世界》- 路遥

他又进一步想&#xff0c;郝红梅抛开他而和顾养民相好&#xff0c;也完全是正常的啊&#xff01;他自己在哪方面都无法和顾养民比较。男女相好&#xff0c;这是两厢情愿的事&#xff0c;而怎能像乡俗话说的“剃头担子一头热”呢&#xff1f; 青春激流打起的第一个浪头在内心渐渐…

Eclipse不用删除之前的项目也可以多次导入相同的项目,操作十分简单!!

问题引入 当我们在学习时&#xff0c;常常需要多次导入网上的同一个案例进行查看效果或者导入自己的项目、用于进行代码测试&#xff0c;原来的项目要继续保留&#xff0c;作为备份&#xff0c;防止代码测试对代码修改过火&#xff0c;一去不返。但当我们导入在Eclipse项目管理…

JavaScript蓝桥杯------学海无涯

目录 一、介绍二、准备三、目标四、代码五、完成 一、介绍 小蓝最近一直在云课平台学习&#xff0c;为了更好的督促自己&#xff0c;于是将每天的学习时间都记录了下来&#xff0c;但是如何更加直观的显示学习时间让小蓝很是苦恼。本题需要你使用 ECharts 帮助小蓝实现统计学习…

【C/C++】基础知识之动态申请内存空间new-delete

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c系列专栏&#xff1a;C/C零基础到精通 &#x1f525; 给大…

对Java远程热部署实践学习和分析

目录 一、热部署现状和必要性分析 &#xff08;一&#xff09;热部署定义和现状分析 &#xff08;二&#xff09;技术实现难度分析 &#xff08;三&#xff09;其必要性分析 二、走进美团Java远程热部署实践 &#xff08;一&#xff09;Sonic分析 &#xff08;二&#x…

Delphi11的多线程ⓞ,附送图片处理代码

Delphi11的多线程ⓞ OLD Coder , 习惯使用Pascal 接下来准备启用多线程&#xff0c;毕竟硬件多核&#xff0c;Timer不太爽了&#xff08;曾经的桌面&#xff0c;都是Timer——理解为“片”&#xff09; 突然想写写&#xff0c;不知道还有多少D兄弟们在。 从源码开始 用D11之…

第1节:vue cesium 概述(含网站地址+视频)

在开始介绍vue cesium之前&#xff0c;我们先聊聊cesiumjs&#xff0c;如果你对这块内容比较熟悉&#xff0c;可以直接跳过这节内容。 cesiumJS 简介 官方网址&#xff1a;https://cesium.com/platform/cesiumjs/ CesiumJS 是一个开源 JavaScript 库&#xff0c;主要用于基于…

Linux之理解文件系统——文件的管理

文章目录 前言一、磁盘1.磁盘的物理结构2.磁盘的存储结构3.磁盘的逻辑结构 二、文件系统与inode1.文件在磁盘中是如何存储的&#xff1f;2.对文件进行操作 三、软硬链接1.软链接创建软链接&#xff1a;inode删除软链接&#xff1a;软链接的作用&#xff1a; 2.硬链接创建硬链接…

chatgpt赋能python:Python另存为对话框:如何在Python中创建一个另存为对话框

Python 另存为对话框&#xff1a;如何在Python中创建一个另存为对话框 如果你是一名 Python 开发者&#xff0c;你可能会常常需要为你的应用程序添加一个另存为对话框。这个对话框使用户可以将他们的数据保存为一个新的文件&#xff0c;而不是覆盖原始文件。然而&#xff0c;很…

【JavaEE】Tomcat-Servelet第一个helloworld程序

Tomcat & Servelet第一个程序helloworld&#xff01; 文章目录 JavaEE & Tomcat & 第一个Servelet程序1. HTTP服务器 - Tomcat1.1 Tomcat的目录结构&#xff1a;1.2 启动Tomcat1.3 Tomcat的优点 2. Servelet框架2.1 创建Maven项目2.2 引入依赖2.3 创建目录2.4 写代…

【Java】wait和notify方法

wait方法wait()和join()的区别wait()和sleep()的区别notify()和notifyAll()实例 wait()和notify()方法都是Object类中的方法。由于每个类都会继承Object类&#xff0c;所以每个对象中都会包含这些方法。 wait方法 wait() 是让线程等待一段时间&#xff0c;死等。对应到线程的…

Linux账号管理与ACL权限设定(一)

Linux的账号与群组 Linux系统中&#xff0c;关于账号和群组&#xff0c;实际记录的是UID和GID的数字&#xff1b; 关于账号有两个非常重要的文件&#xff1a;/etc/passwd 和 /etc/shadow &#xff1b; /etc/passwd 文件结构&#xff1a; 账号名称&#xff1a;密码&#xff…

chatgpt赋能python:Python中另起一行输出的方法

Python中另起一行输出的方法 在Python编程中&#xff0c;我们需要经常输出内容到控制台或者文件中。而有时候&#xff0c;我们可能需要将输出的内容另起一行来符合排版或格式要求。这篇文章将介绍Python中另起一行输出的方法。 使用print函数 Python中最简单的输出方法就是使…

阵列信号处理笔记(1):预备知识、阵列流形、波数-频率响应

阵列信号处理笔记&#xff08;1&#xff09; 文章目录 阵列信号处理笔记&#xff08;1&#xff09;预备知识从延时到阵列流形矢量频率波数响应 预备知识 如图所示的球坐标系中&#xff0c;任意一阵元的位置可以用 ( r , ϕ , θ ) (r,\phi,\theta) (r,ϕ,θ)唯一表示&#xff…

前端045_单点登录SSO_实现流程

单点登录SSO_实现流程 1、背景2、基于同域下 Cookie 实现 SSO1、背景 在企业发展初期,企业使用的系统很少,通常一个或者两个,每个系统都有自己的登录模块,运营人员每天用自己的账号登录,很方便。 但随着企业的发展,用到的系统随之增多,运营人员在操作不同的系统时,需要…

Linux命令(28)之locate

Linux命令之locate 1.locate介绍 linux命令locate用于查找文件所在位置&#xff0c;与which、whereis命令类似&#xff0c;locate命令将会在预先建立好的档案数据库中查询文件。 locate档案数据库路径&#xff1a;/var/lib/mlocate locate档案数据库名称&#xff1a;mlocat…

SpringBoot之Transactional事务

目录 一、事务管理方式二、事务提交方式三、事务隔离级别四、事务传播行为1、Propagation.REQUIRED2、Propagation.SUPPORTS3、Propagation.MANDATORY4、Propagation.REQUIRES_NEW5、Propagation.NOT_SUPPORTED6、Propagation.NEVER7、Propagation.NESTED 五、事务回滚六、只读…

前后端分离项目之登录页面(前后端请求、响应和连接数据库)

目录 一、前端登录发起请求 二、后端请求接收 三、连接数据库 四、后端响应 五、前端处理 六、在前端验证用户是否登录 七、web会话跟踪 八、请求拦截器和响应拦截器 本文Vue-cli前端项目基于文章&#xff1a; Vue-cli搭建项目(包含Node.js安装和ElementUI安装)_小俱的…

2. requests.get()函数访问网页(小白入门)

2. requests.get()函数访问网页(小白入门) 文章目录 2. requests.get()函数访问网页(小白入门)1. 人工访问网页2. 爬虫第一步&#xff1a;发起网络请求3. requests库的安装4. requests.get()函数&#xff1a;发送网络请求5. 代码解析1. 导入库的语法2. 指定网址3. 发送请求4. 输…