【JUC2022】第五章 ThreadLocal

news2025/1/8 5:29:15

【JUC2022】第五章 ThreadLocal

文章目录

  • 【JUC2022】第五章 ThreadLocal
  • 一、是什么
  • 二、案例
  • 三、使用规范
  • 四、源码分析
  • 五、内存泄漏问题
  • 六、实际应用 Demo

一、是什么

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

实现了每一个线程都有自己专属的本地变量副本,线程可以通过使用 get() 和 set() 方法,获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题

T get()																		返回当前线程的此线程局部变量副本中的值
protected T initialValue()													返回此线程局部变量的当前线程的“初始值”
void remove()																删除此线程局部变量的当前线程值
void set(T value)															将此线程局部变量的当前线程副本设置为指定值
static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)		创建一个线程局部变量

二、案例

5 个销售卖房子,各自有独立的销售额,

package com.sisyphus.ThreadLocal;

import java.util.Random;
import java.util.concurrent.TimeUnit;

class House{
    int saleCount = 0;

    public synchronized void saleHouse(){
        ++saleCount;
    }

//    ThreadLocal<Integer> saleVolume = new ThreadLocal<Integer>(){
//        @Override
//        protected Integer initialValue(){
//            return 0;
//        }
//    };

    ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(()->0);

    public void saleVolumeByThreadLocal(){
        saleVolume.set(saleVolume.get()+1);
    }
}

public class ThreadLocalDemo {
    public static void main(String[] args) {
        House house = new House();

        for(int i = 0; i < 5; i++){
            new Thread(()->{
                int size = new Random().nextInt(5) + 1;
                try{
                    for(int j = 0; j < size; j++){
                        house.saleHouse();
                        house.saleVolumeByThreadLocal();
                    }
                }finally {
                    house.saleVolume.remove();
                }
                System.out.println(Thread.currentThread().getName() + "\t" + "号销售卖出:" + house.saleVolume.get());
            },String.valueOf(i)).start();
        }

        //暂停毫秒
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "\t" + "共计卖出多少套:" + house.saleCount);
    }
}

三、使用规范

必须回收自定义的 ThreadLocal 变量,尤其是在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄漏等问题。尽量在代理中使用 try-finally 块进行回收

objectThreadLocal.set(userInfo);
try{
	//...
} finally{
	objectThreadLocal.remove();
}
package com.sisyphus.ThreadLocal;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class MyData{
    ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(()->0);

    public void add(){
        threadLocalField.set(1 + threadLocalField.get());
    }
}

public class ThreadLocalDemo2 {
    public static void main(String[] args) {
        MyData myData = new MyData();
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        try{
            for(int i = 0; i < 10; i++){
                threadPool.submit(()->{
                    try {
                        Integer beforeInt = myData.threadLocalField.get();
                        myData.add();
                        Integer afterInt = myData.threadLocalField.get();
                        System.out.println(Thread.currentThread().getName() + "\t" + "beforeInt:" + beforeInt + "\t afterInt:" + afterInt );
                    } finally {
                        myData.threadLocalField.remove();
                    }
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}

四、源码分析

Thread、ThreadLocal、ThreadLocalMap 之间的关系

Entry 内部类

		/**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

ThreadLocalMap 从字面上就可以看出这是一个保存 ThreadLocal 对象的 map(其实就是以 ThreadLocal 为 Key),不过是经过了两层包装的 ThreadLocal 对象
在这里插入图片描述
JVM 内部维护了一个线程版的 Map<ThreadLocal, Value>(通过 ThreadLocal 对象的 set 方法,把 ThreadLocal 实例当作 key 放入 ThreadLocalMap),每个线程要用到这个 ThreadLocal 值的时候,用当前的线程去 Map 里获取,这样每个线程就都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量

ThreadLocal 是一个壳子,真正的存储结构是 ThreadLocal 里有 ThreadLocalMap 这么个内部类,每个 Thread 对象维护着一个 ThreadLocalMap 的引用,ThreadLocalMap 是 ThreadLocal 的内部类,用 Entry 来进行存储

  • 调用 ThreadLocal 的 set() 方法时,实际上就是往 ThreadLocalMap 设置值,key 是 ThreadLocal 对象,值 valule 是传递进来的对象
  • 调用 ThreadLocal 的 get() 方法时,实际上就是往 ThreadLocalMap 设置值,key 是 ThreadLocal 对象

ThreadLocal 本身并不存储值,它只是自己作为一个 key 来让线程从 ThreadLocalMap 获取 value

五、内存泄漏问题

不会再被使用的对象或者变量占用的内存无法被回收,就是内存泄漏

在这里插入图片描述
强引用

当内存不足,JVM 开始垃圾回收,对于强引用的对象,就算是出现了 OOM 也不会对该对象进行回收

强引用是我们最常见的普通对象的引用,只要还有强引用指向一个对象,就表明该对象还存活

Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用

由于即使该对象以后永远都不会被用到,JVM 也不会回收,因此强引用是造成 Java 内存泄漏的主要原因之一

对于一个普通对象,如果没有其他的引用关系,只要超过了引用变量的作用域,或者将引用变量指向 null,一般就认为可以被垃圾回收器收集

软引用

对于只有软引用的对象来说,当系统内存充足时,它不会被回收;当系统内存不足时,它会被回收。软引用通常用在对内存敏感的程序中,比如高速缓存

假如有一个应用需要读取大量的本地图片:

  • 如果每次读取图片都从硬盘读取,将会严重影响性能
  • 如果一次性全部加载到内存中又可能造成内存溢出

此时使用软引用,可以解决这个问题。用一个 HashMap 来保存图片的路径和图片对象关联的软引用之间的映关系,在内存不足时,JVM 会自动回收这些缓存图片对象占用的空间,从而有效地避免了 OOM 的问题

Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

弱引用

对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,都会回收该对象占用的内存

虚引用

虚引用必须和引用队列(ReferenceQueue)联合使用,虚引用顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象

PhantomReference 的 get 方法总是返回 null。虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被 finalize 以后,做某些事情的通知机制。PhantomReference 的 get 方法总是返回 null,因此无法访问对应的引用对象

设置虚引用关联对象的唯一目的,就是在这个对象被垃圾收集器回收的时候收到一个系统通知或者后续添加进一步的处理,用来实现比 finalize 机制更灵活的回收操作

ThreadLocal 为什么要用弱引用
在这里插入图片描述
从图中我们可以看到,ThreadLocal 对象同时被 ThreadLocalRef 和 Entry.key 引用,我们设想一个场景,如果 Entry.key 对 ThreadLocal 是强引用,并且 CurrentThread 是固定线程池中的核心线程。当 CurrentThread 和 ThreadLocal 都完成了自己的任务,CurrentThread 重新回到线程池,ThreadLocalRef 不再指向 ThreadLocal。这时就产生了一个问题,Entry.key 对 ThreadLocal 的引用无法释放,导致 ThreadLocal 无法被回收,并且 Entry.value 指向的对象也无法被回收,这就造成了内存泄漏。但如果 Entry.key 对 ThreadLocal 对象是弱引用呢?那么,在下一次 GC 的时候就会自动回收掉 ThreadLocal 对象

为什么要在 finally 代码块中调用 remove

但 Entry.value 指向的对象还是无法被回收,该怎么办呢?这个问题 ThreadLocal 的设计者们已经为我们想到了,当调用 set、get、remove 三个方法的任意一个方法时,都会先去判断 Entry.key 是否指向 null,如果指向 null,就会把 Entry.value 也指向 null。因此,只要在 ThreadLocalRef 还没有指向 null 之前调用 remove 即可

如果我们没有调用 remove 方法,那么根据 ThreadLocalRef 的指向,会产生两种不同的后果:

  1. CurrentThread 回到了线程池,ThreadLocalRef 仍然指向 ThreadLocal 对象。此时来了一个新的请求,线程池又将原先的 CurrentThread 分配给了这个请求,那么由于 Entry.key 并没有指向 null,get 方法也就不会先将 Entry.value 指向 null,这样就造成了“脏读”。本次请求的 ThreadLocal 初始值变成了上一个请求执行过程中修改过的值。比如说初始值本该是 0,但是变成了上一次请求执行过程中修改成的 1。我们调用 get 方法,想获取初始值 0,但是我们不知道获取到的值是 1,很可能会导致一系列我们不想看到的的连锁反应。但是我们还是有机会调用 set、get、remove 方法去释放 Entry.value 指向的对象占用的内存的
  2. CurrentThread 回到了线程池,ThreadLocalRef 指向了 null。那么 Entry.value 指向的对象占用的内存将永远无法被释放,直到线程池被关闭,假如这个线程池是 Tomcat,那么也就意味着随着服务请求次数的增加,ThreadLocalMap 中存在的无法被释放的 (null, Object) Entry 也会越来越来越多,最终一定会导致内存溢出,服务器宕机

六、实际应用 Demo

在这里插入图片描述
用户第一次发送请求,可通过 UserInterceptor 进行拦截,获取到 Request 里的 UserInfo,并保存到 UserContextHolder 工具类中,作为用户信息的上下文。当有多个用户请求 Web APP 时,每个请求都有自己的 ThreadLocal,都会去使用自己的 UserContextHolder

User

package com.sisyphus.ThreadLocal;

import java.util.Date;

public class User {
    private String uuid;
    private Date lastLogin;

    public String getUuid() {
        return uuid;
    }

    public void setUuid(String uuid) {
        this.uuid = uuid;
    }

    public Date getLastLogin() {
        return lastLogin;
    }

    public void setLastLogin(Date lastLogin) {
        this.lastLogin = lastLogin;
    }
}

UserContextHolder

package com.sisyphus.ThreadLocal;

public class UserContextHolder {
    
    private static ThreadLocal<User> userInfoHolder = new ThreadLocal<>();
    
    public static void setCurrentUser(User user){
        if(userInfoHolder.get() == null){
            userInfoHolder.set(user);
        }
    }
    
    public static User getCurrentUser(){
        return userInfoHolder.get();
    }
    
    public static void removeCurrentUser(){
        userInfoHolder.remove();
    }
}

UserInterceptor

package com.sisyphus.ThreadLocal;

public class UserInterceptor implements HandlerInterceptor{
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
        User user = (User)request.getSession().getAttribute("user");
        if(user != null){
            UserContextHolder.setCurrentUser(user);
            return true;
        }
        return false;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception{
        UserContextHolder.removeCurrentUser();
    }
}

UserController

package com.sisyphus.ThreadLocal;

@Controller("/user")
public class UserController {
    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String login(){
        User user = UserContextHolder.getCurrentUser();
        String result = null;
        //todo
        return result;
    }
}

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

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

相关文章

Qt Widget之QMainWindow

目录 一 QMainWindow整体概况 二 菜单栏 三 工具栏 四 状态栏 五 铆接部件 六 核心部件 七 总体效果 一 QMainWindow整体概况 QMainWindow是主窗口程序&#xff0c;由以下几个部分构成&#xff1a;菜单栏&#xff0c;状态栏&#xff0c;工具栏&#xff0c;铆接部件和核…

Nginx优化服务和防盗链

Nginx优化服务和防盗链一、长连接1、修改主配置文件2、测试3、在主配置文件添加4、验证二、Nginx第三方模块1、开源的echo模块2、查看是否成功3、加echo模块步骤4、网页测试验证三、搭建虚拟主机1、编译安装好nginx后&#xff0c;对主配置文件进行修改2、创建文件3、验证四、防…

Android 源码中的 JNI,到底是如何使用的?

Linux下 JNI的使用学习 Android 其中涉及对 JNI 的使用&#xff1b;JNI的使用对于 Android 来说又是十分的重要和关键。那么到底 Java 到底是如何调用 C/C 的&#xff0c;下面是非常简单的计算器源码&#xff0c;只是用来熟悉JNI的基本语法&#xff0c;其中我自己碰到过的一个问…

Unity 使用L2Localization实现App切换多国语言

Unity 使用 L2 Localization实现App切换多国语言一、L2 Localization介绍二、使用步骤1.导入插件&#xff0c;新建Language和Term2.开始使用&#xff0c;切换语言总结提示&#xff1a;以下是本篇文章正文内容&#xff0c;会提供自己的插件下载地址 一、L2 Localization介绍 这…

day24_多线程进阶

今日内容 上课同步视频:CuteN饕餮的个人空间_哔哩哔哩_bilibili 同步笔记沐沐霸的博客_CSDN博客-Java2301 零、 复习昨日 一、作业 二、线程安全的集合 三、死锁 四、线程通信 五、生产者消费者 六、线程池 零、 复习昨日 创建线程的几种方式 1) 继承 2) 实现Runnable 3) calla…

【jeecg】vue-cli-service不是内部或外部命令,也不是可运行的程序

npm run serve时&#xff0c;报错 vue-cli-service不是内部或外部命令,也不是可运行的程序 参考该链接 &#xff0c;找到生成的log文件&#xff0c; 在log文件里看到了C盘的地址 找到这个地址&#xff0c;发现里面有 我之前下载的yarn &#xff08;原来下在了这里&#xff09;没…

微服务一 实用篇 - 4.1 RabbitMQ部署

《微服务一 实用篇 - 4.1 RabbitMQ部署》 提示: 本材料只做个人学习参考,不作为系统的学习流程,请注意识别!!! 《微服务一 实用篇 - 4.1 RabbitMQ部署》《微服务一 实用篇 - 4.1 RabbitMQ部署》RabbitMQ部署指南1.单机部署1.1.下载镜像1.2.安装MQ2.集群部署2.1.集群分类2.2.设…

B - Build Roads (最小生成树 + 打表)

https://vjudge.net/problem/Gym-103118B/origin 在猫的国度里&#xff0c;有n个城市。猫国国王想要修n -1条路来连接所有的城市。第i市有一家ai经验价值的建筑公司。要在第i市和第j市之间修建公路&#xff0c;两个城市的建筑公司需要相互合作。但是&#xff0c;在修路的过程中…

Windows安装VMware+创建Linux虚拟机

目录1&#xff1a;简介2&#xff1a;功能特点3&#xff1a;VM下载地址4&#xff1a;安装VMware5&#xff1a;下载iso虚拟机镜像6&#xff1a;创建Linux虚拟机7&#xff1a;配置静态ip1&#xff1a;简介 VMware Workstation Pro&#xff08;曾用名VMware Workstation&#xff09…

供应商绩效管理指南:挑战、考核指标与管理工具

管理和优化供应商绩效既关键又具有挑战性。要知道价格并不是一切&#xff0c;如果你的供应商在商定的价格范围内向你开具发票&#xff0c;但服务达不到标准或货物不合格&#xff0c;你也无法达到节约成本的目标。 供应商绩效管理可以深入了解供应商可能带来的风险&#xff0c…

SpringCloud之认识微服务

文章目录一、传统项目转型二、走进 SpringCloud三、微服务项目搭建3.1 创建一个 SpringBoot 项目3.2 创建三个 Maven 子工程3.3 为子工程创建 application.yml3.4 引入依赖3.5 数据库 建库建表3.6 编写业务提示&#xff1a;以下是本篇文章正文内容&#xff0c;SpringCloud系列学…

Vim笔记

文章目录VIM四种模式命令模式编辑模式末行模式可视化模式扩展内容VIM四种模式 1、命令模式 2、编辑模式 3、末行模式 4、可视化模式 5、VIM四种模式的关系 命令模式 1、命令模式下我们能做什么&#xff1a;① 移动光标 ② 复制 粘贴 ③ 剪切 粘贴 删除 ④ 撤销与恢复 2、进…

【分布式】分布式唯一 ID 的 8 种生成方案

文章目录前言正文什么是分布式ID&#xff1f;分布式ID的特性&#xff1f;分布式ID的生成方案1. UUID2. 数据库自增ID3. 批量生成ID4. Redis生成ID5. Twitter的snowflake算法6. 百度UidGenerator7. 美团Leaf8.滴滴&#xff08;Tinyid&#xff09;小结前言 在互联网的业务系统中…

Leaflet基础入门教程(一)

leaflet是一个前端的轻量的gis框架,为什么说它轻量呢。因为相比于传统的“庞大的”GIS框架比如openlayers和mapbox,leaflet不仅代码体积小,而且API构成也极为简单。是GIS行业小白入门级别学习的最好的框架,没有之一。 那么话不多说我们首先来学习一下如何使用leaflet搭建一…

Vue3中hook的使用及使用中遇到的坑

目录前言一&#xff0c;什么是hook二&#xff0c; hook函数的使用2.1 铺垫2.2 hook函数的写法2.3 使用写好的hook函数后记前言 在学习Es6的时候&#xff0c;我们开始使用类与对象&#xff0c;开始模块化管理&#xff1b;在Vue中我们可以使用mixin进行模块化管理&#xff1b;Vu…

2023-03-03干活小计

今天见识了 归一化的重要性&#xff1a;归一化 不容易爆炸 深度了解了学习率&#xff1a;其实很多操作 最后的结果都是改变了lr 以房价预测为例&#xff1a;一个点一个点更新 比较 矩阵的更新&#xff1a; 为什么小批量梯度下降 优于随机梯度下降 优于批量梯度下降&#xff…

Kubernetes 中 Deployment 使用

Deployment的使用 之前我们了解了Replication Controller和Replica Set两种资源对象&#xff0c;RC和RS的功能基本上是差不多的&#xff0c;唯一的区别就是RS支持集合的selector。也了解到了用RC/RS来控制Pod副本的数量&#xff0c;也实现了滚动升级Pod的功能。现在看上去似乎…

【面试题】前端路由分类

0. 前言 在Web开发中&#xff0c;路由是指根据用户请求的URL地址&#xff0c;确定用户访问的页面资源、参数等&#xff0c;是前端开发中不可缺少的重要部分。Vue router中提供了两种路由模式&#xff0c;一种是hash模式&#xff0c;另一种是history模式。 1. Hash模式路由 H…

爆款升级!新系列南卡Neo最强旗舰杀到,业内首款无线充骨传导耳机!

中国专业骨传导耳机品牌NANK南卡于近日发布了全新南卡Neo骨传导运动耳机&#xff0c;打造一款佩戴最舒适、音质体验最好的骨传导耳机。推出第2代声学響科技技术&#xff0c;提供更优质的开放式骨传导听音体验&#xff0c;透过不一样的音质体验&#xff0c;打造更好的骨传导耳机…

MySQL基础(二)排序与分页、多表查询、单行函数

上接 MySQL基础&#xff08;一&#xff09;SQL分类、导入、SELECT语句&#xff0c;运算符_独憩的博客-CSDN博客 目录 排序与分页 排序 二级排序 分页 多表查询 基础多表查询 等值连接vs非等值连接 自连接vs非自连接 内连接vs外连接 自然连接 单行函数 数值函数 …