多线程学习之ThreadLocal详细笔记

news2024/11/10 13:19:52

ThreadLocal详细笔记

  • 一、ThreadLocal的基本概念
  • 二、ThreadLocal的独特性
    • 2.1 数据访问方式
    • 2.2 线程安全实现
    • 2.3 适用场景
  • 三、ThreadLocal 的简单使用
  • 四、ThreadLocal 的工作原理
  • 五、ThreadLocal和内存泄漏的关系
    • 5.1 ThreadLocalMap的Entry的Key设计成弱引用
    • 5.2 弱引用会导致内存泄漏
  • 六、ThreadLocal 的注意事项
    • 6.1 线程复用问题
    • 6.2 初始化值的正确设置
    • 6.3 理解线程局部性的范围
    • 6.4 适当的命名和文档说明

一、ThreadLocal的基本概念

在这里插入图片描述

ThreadLocal 属于Java中能在多线程环境下存储线程局部变量的机制。作用在于给每个线程都准备单独的变量副本,通过这种方式防止线程之间出现竞争条件。
ThreadLocal 的工作方式是在每个线程内部创建一个独立的变量副本,而且每个线程仅仅可以访问属于自己的那个副本。
主要可以处理的多线程并发问题:

  • 避免多线程对共享变量的并发访问冲突:比如在一个 Web 应用中,存储每个线程的用户会话信息等。每个线程都能独立地操作自己的 ThreadLocal 变量副本,而不需要担心其他线程对其的干扰。
  • 简化代码中的线程安全问题处理:当某些对象或数据只在特定线程内使用,并且不希望受到其他线程的影响时,使用 ThreadLocal 可以更方便地管理这些数据,而不需要复杂的同步机制。

二、ThreadLocal的独特性

传统的共享变量方式与 ThreadLocal 有很大的不同,ThreadLocal 的独特性主要体现在:

2.1 数据访问方式

  • 传统共享变量:
多个线程可直接访问和修改同一个共享变量。需要使用同步机制(如synchronized关键字或锁)来确保数据的一致性和完整性,以避免数据竞争和不一致的情况。
例如,多个线程同时对一个共享计数器进行递增操作,如果不进行适当的同步,可能会导致计数器的值不准确。

所有线程都能看到共享变量的最新状态,任何一个线程对其的修改都会立即对其他线程可见。这在某些情况下是必要的,但增加复杂性,因为线程需要时刻考虑其他线程可能对共享变量的影响。
  • ThreadLocal:
每个线程都有自己独立的变量副本,线程只能访问自己的副本,不会直接影响其他线程的副本。可消除对复杂同步机制的需求,极大地简化多线程编程。
例如,在使用 ThreadLocal 存储每个线程的用户会话信息时,不同线程可以独立地修改自己的会话信息,而不用担心与其他线程的冲突。

每个线程的变量副本对于该线程是私有的,修改自己的副本不会影响其他线程的副本状态,各个线程之间的数据相互隔离。

2.2 线程安全实现

  • 传统共享变量:
要保证线程安全,需要开发者谨慎地使用同步代码块或方法,确保在并发访问时正确地协调对共享变量的读写操作。这不仅需要深入理解多线程同步的原理,还容易出现死锁、活锁等并发问题。
例如,使用synchronized关键字不当可能导致线程长时间等待锁,从而影响程序性能和响应性。

对于复杂的数据结构作为共享变量,还需要考虑更高级的并发控制策略,如使用读写锁等,以提高并发性能。
  • ThreadLocal:
由于每个线程都操作自己独立的变量副本,从本质上就保证线程安全。不需要额外的同步代码来保护变量的读写操作,减少因同步带来的性能开销和潜在的并发问题。
开发者可以更专注于单个线程内的业务逻辑,而不必过多担心多线程并发访问的复杂性。

2.3 适用场景

  • 传统共享变量:
适用于需要多个线程共享数据并进行协作的场景。
比如在一个生产者-消费者模型中,生产者和消费者线程需要共享一个队列来传递数据。这种情况下,共享变量作为数据传递和协作的媒介是必要的。

在一些需要全局状态管理的系统中,共享变量可以用来存储系统的配置信息等,供所有线程读取和使用(在修改时需要注意同步)。
  • ThreadLocal:
常用于需要保存每个线程自身状态的情况。
例如在 Web 应用中,每个线程处理不同的用户请求,使用 ThreadLocal 可以方便地保存每个用户请求相关的上下文信息,如用户身份认证信息、事务状态等。
这样不同的请求线程可以独立地维护自己的状态,互不干扰。

在一些日志记录框架中,ThreadLocal 可以用来保存每个线程的日志级别等配置信息,使得不同线程可以根据自己的需求进行独立的日志记录,而不需要在每次记录日志时都传递相关配置信息。

总而言之,ThreadLocal 以其独特的数据隔离和线程安全特性,为多线程编程中处理线程局部数据提供一种简洁而有效的方式,与传统共享变量方式对比,在特定的应用场景中具有很大的优势。

三、ThreadLocal 的简单使用

以日期转换工具类为例,使用ThreadLocal。
在多线程中进行日期的格式转换:

public class DateUtil {

    private static final SimpleDateFormat simpleDateFormat =
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static Date parse(String dateString) {
        Date date = null;
        try {
            date = simpleDateFormat.parse(dateString);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 50; i++) {
            executorService.execute(()->{
                System.out.println(DateUtil.parse("2024-08-08 20:20:20"));
            });
        }
        executorService.shutdown();
    }
}

报错如下:

Exception in thread "pool-1-thread-6" Exception in thread "pool-1-thread-23" Exception in thread "pool-1-thread-21" Exception in thread "pool-1-thread-20" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at org.example.threadLocal.DateUtil.parse(DateUtil.java:19)
at org.example.threadLocal.DateUtil.lambda$main$0(DateUtil.java:31)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)

因为SimpleDateFormat不是线性安全的,并发多线程场景下即会报错,不同的线程都能基于初始值进行独立计算。
使用ThreadLocal对SimpleDateFormat进行处理:

public class SafeDateUtil {

    private static final ThreadLocal<SimpleDateFormat> simpleDateFormat =
    ThreadLocal.withInitial(
            ()->new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static Date parse(String dateString) {
        Date date = null;
        try {
            date = simpleDateFormat.get().parse(dateString);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 50; i++) {
            executorService.execute(()->{
                System.out.println(SafeDateUtil.parse("2024-08-08 20:20:20"));
            });
        }
        executorService.shutdown();
    }
}

结果如下:

Thu Aug 08 20:20:20 CST 2024
Thu Aug 08 20:20:20 CST 2024
Thu Aug 08 20:20:20 CST 2024
Thu Aug 08 20:20:20 CST 2024
Thu Aug 08 20:20:20 CST 2024
Thu Aug 08 20:20:20 CST 2024
Thu Aug 08 20:20:20 CST 2024
Thu Aug 08 20:20:20 CST 2024
Thu Aug 08 20:20:20 CST 2024
Thu Aug 08 20:20:20 CST 2024

四、ThreadLocal 的工作原理

ThreadLocal的内存结构图:
在这里插入图片描述
Thread类中,有个ThreadLocal.ThreadLocalMap 的成员变量。
ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型对象值。
源码中:
在这里插入图片描述

Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
在这里插入图片描述
并发场景下,每个线程Thread,在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而可以实现了线程隔离。

ThreadLocal类中的关键set()方法:

public void set(T value) {
    Thread t = Thread.currentThread(); //获取当前线程t
    ThreadLocalMap map = getMap(t);  //根据当前线程获取到ThreadLocalMap
    if (map != null)  //如果获取的ThreadLocalMap对象不为空
        map.set(this, value); //K,V设置到ThreadLocalMap中
    else
        createMap(t, value); //创建一个新的ThreadLocalMap
}

ThreadLocalMap getMap(Thread t) {
   return t.threadLocals; //返回Thread对象的ThreadLocalMap属性
}

void createMap(Thread t, T firstValue) { //调用ThreadLocalMap的构造函数
    t.threadLocals = new ThreadLocalMap(this, firstValue); // this表示当前类ThreadLocal
}

ThreadLocal类中的关键get()方法:

public T get() {
    Thread t = Thread.currentThread();//获取当前线程t
    ThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMap
    if (map != null) { //如果获取的ThreadLocalMap对象不为空
        //由this(即ThreadLoca对象)得到对应的Value,即ThreadLocal的泛型值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value; 
            return result;
        }
    }
    return setInitialValue(); //初始化threadLocals成员变量的值
}

private T setInitialValue() {
    T value = initialValue(); //初始化value的值
    Thread t = Thread.currentThread(); 
    ThreadLocalMap map = getMap(t); //以当前线程为key,获取threadLocals成员变量,它是一个ThreadLocalMap
    if (map != null)
        map.set(this, value);  //K,V设置到ThreadLocalMap中
    else
        createMap(t, value); //实例化threadLocals成员变量
    return value;
}

五、ThreadLocal和内存泄漏的关系

5.1 ThreadLocalMap的Entry的Key设计成弱引用

在这里插入图片描述
在 Java 中,弱引用对象只要垃圾回收器运行,就会被回收,不管内存是否充足。比如ThreadLocal中ThreadLocalMap的Key用弱引用,就是为在Key没有其他强引用的时候,能尽快被回收,防止因为一直保留不需要的Key而导致内存泄漏。

  • 防止内存泄漏:
    当ThreadLocal对象作为ThreadLocalMap中Entry的Key时,如果Key是强引用,那么即使在外部没有对ThreadLocal对象的强引用(例如,在方法调用结束后,局部的ThreadLocal变量不再被使用),但由于ThreadLocalMap中Entry的Key对它有强引用,ThreadLocal对象及其关联的值将无法被垃圾回收。
    这可能会导致内存泄漏,尤其是在长期运行的应用程序中,如果有大量的线程创建和销毁,而这些ThreadLocal对象没有被正确清理,就会占用不必要的内存。
    通过将Key设计成弱引用,当没有其他强引用指向ThreadLocal对象时,垃圾回收器可以回收该ThreadLocal对象,从而避免这种内存泄漏问题。
  • 符合线程局部变量的生命周期特点:
    线程局部变量ThreadLocal的生命周期通常与创建它的线程相关。
    当线程结束时,理论上它的所有线程局部变量都应该可以被回收。
    然而,如果Key是强引用,即使线程结束了,但ThreadLocalMap可能仍然持有对ThreadLocal对象的引用,导致相关资源无法及时释放。
    设计成弱引用可以使得在合适的时候(当没有其他强引用时)自动清理不再使用的ThreadLocal对象及其关联的值,更符合线程局部变量的实际生命周期管理需求。
  • 避免无用数据的累积:
    在多线程环境下,如果不将Key设计成弱引用,随着线程的创建和销毁,可能会有很多不再使用的ThreadLocal对象残留在ThreadLocalMap中,导致ThreadLocalMap占用的内存不断增加。
    使用弱引用Key可以在一定程度上自动清理这些无用的数据,保持内存的合理使用。

例如,如果Key不是弱引用,可能导致的问题:

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

public class ThreadLocalMemoryLeakExample {
    static final int THREAD_COUNT = 5;

    public static void main(String[] args) {
        // 创建固定线程数的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);

        for (int i = 0; i < THREAD_COUNT; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    // 创建一个 ThreadLocal 对象
                    ThreadLocal<String> threadLocal = new ThreadLocal<>();
                    threadLocal.set("Some value for this thread");
                    // 这里假设执行一些其他操作后,threadLocal 变量在方法内不再被使用,但由于 Key 是强引用,它可能无法被回收
                    System.out.println(Thread.currentThread().getName() + " set value in ThreadLocal");
                }
            });
        }

        // 关闭线程池
        executorService.shutdown();
    }
}

可见,如果ThreadLocal的Key不是弱引用,那么每次循环创建的ThreadLocal对象即使在任务执行完毕后可能也无法被回收,因为ThreadLocalMap中的Entry一直持有对它的强引用。而将Key设计成弱引用可以在合适的时候让这些不再使用的ThreadLocal对象被垃圾回收。

因此,将ThreadLocalMap中Entry的Key设计成弱引用是为了更好地管理内存,防止内存泄漏,并符合线程局部变量的生命周期特点。

5.2 弱引用会导致内存泄漏

TreadLocal的引用示意图:
在这里插入图片描述
hreadLocalMap使用ThreadLocal的弱引用作为key,当ThreadLocal变量被手动设置为null,即一个ThreadLocal没有外部强引用来引用它,当系统GC时,ThreadLocal一定会被回收。这样的话,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如线程池的核心线程),这些key为null的Entry的value就会一直存在一条强引用链:

Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object 

对象永远无法回收,造成内存泄漏。

实际上,ThreadLocalMap的设计中已经考虑到这种情况。所以有一些防护措施:即在ThreadLocal的get,set,remove方法,都会清除线程ThreadLocalMap里所有key为null的value。
set方法:
在这里插入图片描述
get方法:
在这里插入图片描述

六、ThreadLocal 的注意事项

综上,在使用ThreadLocal时需要注意以下几点:

6.1 线程复用问题

在一些应用服务器或者线程池的环境中,线程可能会被复用。如果在一个线程中使用了ThreadLocal,并且没有正确清理,当下次这个线程被复用执行其他任务时,可能会得到上一次遗留下来的数据,从而导致错误的结果。所以在这种情况下,更要确保在合适的时机(比如任务执行结束时)清理ThreadLocal的数据。

6.2 初始化值的正确设置

如果使用ThreadLocal的withInitial方法来设置初始值,要确保初始值的计算是线程安全的。因为这个初始值的供应函数只会在每个线程第一次获取ThreadLocal的值时被调用,如果这个供应函数内部有不安全的操作,可能会导致问题。例如:

ThreadLocal<Integer> counter = ThreadLocal.withInitial(() -> {
   // 这里如果有不安全的操作,可能会有问题
   return someSharedVariable++; 
});

这里someSharedVariable的自增操作如果没有正确同步,在多线程环境下会导致错误的初始值。

6.3 理解线程局部性的范围

ThreadLocal变量只在当前线程内是局部的。不要错误地认为它在整个应用程序中都是隔离的。不同线程之间的ThreadLocal变量是相互独立的,但是在同一个线程内,对ThreadLocal变量的修改会影响到该线程后续对这个变量的使用。比如,在一个线程中修改ThreadLocal的值,那在该线程的后续代码中获取到的就是修改后的值。

6.4 适当的命名和文档说明

使用ThreadLocal时,应该起一个有意义的名称,并且在代码中做好说明,以便其他开发者能够清楚地理解这个ThreadLocal变量的用途和生命周期管理的重要性。

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

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

相关文章

超简单4行代码-STM32F103的HAL实现有源蜂鸣器报警

1、概述 在嵌入式项目开发中&#xff0c;使用蜂鸣器作为简单的报警或提示装置非常常见。根据不同的应用场景&#xff0c;我们可能会选择有源蜂鸣器或无源蜂鸣器。本文将重点介绍如何通过STM32F103系列单片机&#xff0c;利用HAL库&#xff0c;仅通过4行代码实现对有源蜂鸣器的…

django学习入门系列之第九点《初识MySQL》

文章目录 9.1 初识网站9.2 初识MySQL下载安装创建配置文件初始化启动MySQL服务进入mysql查看已有文件夹退出&#xff08;关闭连接&#xff09;忘记密码 往期回顾 9.1 初识网站 Python相关:基础、函数、数据类型、面向、模块。前端开发直观:HTML、CSS、JavaScript、jQuery:[静态…

【运维】JetBrains Gateway (Pycharm) SSH免密连接,改为免密连接

一直要求输入密码&#xff0c;很烦人&#xff1a; 如何免密连接&#xff1f; 1 重新打开gateway&#xff0c;来到这个界面点新建连接&#xff1a; 2 点这里设置&#xff1a; 3 在这一页&#xff0c;你可以改你的所有配置&#xff0c;只要设置为password并且保存密码&…

详解Redis 高可用的方式 Redis Cluster

Redis 高可用方式 Redis 提供了多种高可用性方案&#xff0c;主要包括以下几种方式&#xff1a; 主从复制&#xff08;Replication&#xff09; 主从复制是最基本的高可用性方案&#xff0c;通过将数据从一个主节点复制到多个从节点来实现数据的冗余和读写分离。主节点负责所…

数据结构--数据结构概述

一、数据结构三要素 1. 数据的逻辑结构 数据的逻辑结构是指数据元素之间的关系和组织方式&#xff0c;通常分为线性结构和非线性结构。 线性结构&#xff1a;例如线性表&#xff0c;其中数据元素按照顺序排列&#xff0c;彼此之间存在一对一的关系。 非线性结构&#xff1a;…

android车载手机互联投屏新专题-实战作业布置

背景&#xff1a; 学习了马哥的投屏实战开发课程后&#xff0c;大家都可以实现如下图一样的手机车机多端互联的投屏场景。 即已经实现了手机和车机投屏互动&#xff0c;车机上手机画面屏幕可以与手机实体屏幕一样就是常见的Mirror模式&#xff0c;如果不一样就是课程里面讲的扩…

解析网络流量管理方案:简化基于云的DNS负载均衡

数字化时代&#xff0c;网络规模和流量需求的增长&#xff0c;催生了用户对可用性的需求、管理员对更好的访问和管理等需求。在大型的网络应用中&#xff0c;为保障站点的稳定性&#xff0c;会为服务或站点提供多台服务器&#xff0c;以平均分配每台服务器上的压力&#xff0c;…

上门做饭小程序项目源码功能介绍

上门做饭小程序通常包含以下功能&#xff0c;以便用户方便地享受到上门做饭的服务&#xff1a; 用户注册与登录&#xff1a;允许用户创建账户并登录&#xff0c;管理个人信息和偏好。 菜品浏览与选择&#xff1a;提供各种菜品的列表或菜单&#xff0c;用户可以浏览菜品详情、价…

C++STL详解(五)——list类的具体实现

一.本次所需实现的三个类及其成员函数接口 链表首先要有结点&#xff0c;因此我们需要实现一个结点类。 链表要有管理结点的结构&#xff0c;因此我们要有list类来管理结点。 链表中还要有迭代器&#xff0c;而迭代器的底层其实是指针。但是我们现有的结点类无法完成迭代器的…

在VB.net中,对数据排名次,用LINQ、SortedSet,还是用SortedList速度快

标题 在VB.net中&#xff0c;对数据排名次&#xff0c;用LINQ、SortedSet&#xff0c;还是用SortedList速度快 正文 在VB.NET中&#xff0c;选择最适合你需求的排序和索引方法时&#xff0c;需要考虑到数据的规模、是否需要频繁地更新数据结构、以及是否只需要排序结果或还需要…

【Hadoop】建立圈内组件的宏观认识

01存储02计算03调度04其他05回忆 众多组件们构建了大规模分布式计算和存储平台。本文介绍Hadoop生态圈中各个组件的主要功能和作用&#xff0c;辅助学者理解每个组件的定位和用途&#xff0c;从而建立对圈内组件的宏观认识。梳理清楚HDFS、MapReduce、YARN、Hive、HBase、Spark…

【大模型系列篇】Transformers综述--邱锡鹏

论文标题&#xff1a;A Survey of Transformers 论文作者&#xff1a;Tianyang Lin, Yuxin Wang, Xiangyang Liu, Xipeng Qiu 论文链接&#xff1a;https://arxiv.org/abs/2106.04554 Transformer 在许多人工智能领域&#xff08;如自然语言处理、计算机视觉和音频处理&#…

【区块链+金融服务】山西省信易贷平台 | FISCO BCOS应用案例

2022 年 8 月 8 日&#xff0c;山西省发展改革委按照国家的顶层设计&#xff0c;指导山西股权交易中心建设山西省信易贷平台&#xff0c;包 括三个子平台&#xff1a;一是建设集金融产品超市、融资需求精准匹配、融资监测等于一体的山西省融资综合信用服务平台&#xff1b; 二是…

微信小程序--25(WXSS模板样式了解)

一、WXSS和CSS关系 大部分特性相同 1.wxss独有 rpx尺寸单位import 样式导入 二、rpx 1.原理 rpx将所有屏幕宽度等分为750份&#xff0c;自动换成像素实现屏幕适配 2.rpx与px之间换算 约分计算不同设备比例不同 三、样式导入 1.语法 importt”相对路径“&#xff1b;…

Qt 0814作业

一、思维导图 二、登录窗口界面 自由发挥登录窗口的应用场景&#xff0c;实现一个登录窗口界面 要求&#xff1a;每行代码都有注释 【需要用到的图片或者动图&#xff0c;自己去网上找】 #include "mywidget.h"MyWidget::MyWidget(QWidget *parent): QWidget(par…

Grok-2的Beta版发布

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

高阶数据结构(Java):AVL树插入机制的探索

目录 1、概念 1.1 什么是AVL树 2.1 平衡因子 3、AVL树节点的定义 4、AVL树的插入机制 4.1 初步插入节点 4.2 更新平衡因子 4.3 提升右树高度 4.3.1 右单旋 4.3.2 左右双旋 4.4 提升左树高度 4.4.1 左单旋 4.4.2 右左双旋 5、AVL树的验证 6、AVL树的删除 1、概念 …

uni-app 使用九宫格(uni-grid)布局组件

1、运行环境 开发工具为 HBuilder X 4.23, 操作系统为 Windows 11。Vue.js 版本为 3. 2、操作步骤 首先&#xff0c;登录 HBuilder X。然后用桌面浏览器&#xff0c;访问官网组件网址。 https://ext.dcloud.net.cn/plugin?nameuni-grid 在组件网址右上角、点击“下载插…

出差学习笔记(1)汽车智能大灯一键标定功能

出差学习笔记&#xff08;1&#xff09;汽车智能大灯一键标定功能 今天看到了某公司制作的汽车智能大灯的一键标定功能&#xff0c;好奇&#xff0c;遂问之。 车前的两个大灯如何标定&#xff0c;我们可以将车辆开到一片墙前&#xff0c;将一些动态/静态图形打到墙上&#xff0…

Sublime Text常用快捷键大全

Sublime Text 是一款功能强大且广受欢迎的文本编辑器&#xff0c;其丰富的快捷键支持使得开发者能够更高效地编写和编辑代码。以下是 Sublime Text 中一些常用的快捷键&#xff0c;帮助你更加高效地使用这款工具&#xff1a; 功能分类快捷键 (Windows)快捷键 (Mac)新建文件Ctr…