003 仿muduo实现高性能服务器组件_前置知识

​🌈个人主页:Fan_558
🔥 系列专栏:仿muduo
🌹关注我💪🏻带你学更多知识

文章目录

  • 前言
    • 时间轮timewheel设计
    • 正则表达式介绍(了解知道怎么使用)
    • 通用型any容器的实现
  • 小结

前言

在正式讲解模块设计前,将会介绍模块当中的一些前置知识以及组件设计(timewheel时间轮,正则表达式、通用型容器any)
在这里插入图片描述

时间轮timewheel设计

由于服务器的资源是有限的,为了避免某些客户端连接上来之后一直不通信而平白浪费服务器资源的情况,我们需要对非活跃连接设置定时销毁,而实现这个功能的前提是得有一个定时器。
这个小组件将会运用到TimerQueue子模块当中
Linux当中提供给我们了一个定时器,如下:

#include <sys/timerfd.h>
int timerfd_create(int clockid, int flags);
 clockid: CLOCK_REALTIME-系统实时时间,如果修改了系统时间就会出问题; 
 CLOCK_MONOTONIC-从开机到现在的时间是⼀种相对时间;
 flags: 0-默认阻塞属性

int timerfd_settime(int fd, int flags, struct itimerspec *new, struct
itimerspec *old);
 fd: timerfd_create返回的⽂件描述符
 flags: 0-相对时间, 1-绝对时间;默认设置为0即可.
 new: ⽤于设置定时器的新超时时间
 old: ⽤于接收原来的超时时间
 
 struct timespec {
 time_t tv_sec; /* Seconds */
 long tv_nsec; /* Nanoseconds */
 };
 
 struct itimerspec {
 struct timespec it_interval; /* 第⼀次之后的超时间隔时间 */
 struct timespec it_value; /* 第⼀次超时时间 */
 };
 定时器会在每次超时时,⾃动给fd中写⼊8字节的数据,表⽰在上⼀次读取数据到当前读取数据期间超
时了多少次。

以下是timefd的使用样例,了解即可

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdint.h>
#include <sys/timerfd.h>

int main()
{
    //创建定时器
    int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);
    if(timerfd < 0)
    {
        perror("timefd_create error");
        return -1;
    }
    struct itimerspec itime;
    //设置首次超时时间
    itime.it_value.tv_sec = 1;  //第一次超时在1s后
    itime.it_value.tv_nsec = 0;
    //设置后续超时时间间隔
    itime.it_interval.tv_sec = 3;   //第一次超时后,每隔3秒间隔超时一次
    itime.it_interval.tv_nsec = 0;
    timerfd_settime(timerfd, 0, &itime, NULL);

    //从timefd中读取超时次数
    while(1){
        uint64_t times;
        int ret = read(timerfd, &times, 8);
        if(ret < 0)
        {
            perror("read error");
            return -1;
        }
        printf("超时,距离上次超时了%ld次\n",times);
    }
    close(timerfd);
}

在这里插入图片描述
上述的例⼦,存在⼀个很⼤的问题,每次超时都要将所有的连接遍历⼀遍,如果有上万个连接,效率⽆疑是较为低下的

针对这个问题,我们可以以连接最近一次通信的系统时间为基准建立一个小跟堆,堆中的元素按照连接最近一次通信的系统时间排序,使得堆顶元素始终是最近一次通信时间最早的连接,然后就只需要不断取出堆顶已超时的连接执行超时任务,直到没有超时连接即可。

我们也可以采用时间轮的方法,时间轮的思想来源于钟表,由此我们可以设计出一个数组,由一个指针指向起始位置,只需要让这个指针每秒钟往后走一步,及秒针(tick)走到哪里执行哪里的超时连接销毁任务即可
在这里插入图片描述

1、如果我们的超时时间很长应该怎么办呢?

比如我们的超时时间为一天,我们是不是要定义一个 60 * 60 * 60s 的数组?解决办法很简单,我们可以将时间轮分级,即分别定义秒级时间轮、分级时间轮以及时级时间轮,如下:
在这里插入图片描述
此时我们仅需要 3 * 60 个整形的空间就可以实现 60 小时内的定时器了 (如果使用位图来定义定时轮仅需要 4*3 个字节的空间)。

2、同一时刻的定时任务只能添加一个,需要考虑如何在同一时间时刻支持添加多个定时任务

解决方案:将时间轮的一维数组设计为二维数组(时间轮)

3、 如何实现定时任务的延时?

解决方法:类的析构函数+智能指针share_ptr,通过这两个计数可以实现定时任务 1、使用一个类,对定时任务进行封装,类实例化的每一个对象,就是一个定时任务对象,当对象被销毁的时候即是执行定时任务的时候(将定时任务的执行放到析构函数当中) 但是当一个连接建立成功后,我们给这个连接设置了一个30s后定时销毁任务,但是在10s后这个连接进行了一次通信(连接非活跃30s后则定时销毁),此时我们应该在40s时才关闭连接,那么当连接进行通信的时候,我们需要重新刷新定时任务时间,类的实例化与销毁就不太适合用于刷新定时任务了,该如何做呢? 这⾥,我们就⽤到了智能指针shared_ptr,shared_ptr有个计数器,当计数为0的时候,才会真正释放 ⼀个对象,那么如果连接在第10s进⾏了⼀次通信,则我们继续向定时任务时间轮(_wheel)中添加⼀个30s后(也就 是第40s)的任务类对象的shared_ptr,则这时候两个任务shared_ptr计数为2,则第30s的定时任务被释放的时候,计数-1,变为1,并不为0,则并不会执⾏实际的析构函数,那么就相当于这个 第30s的任务失效了,只有在第40s的时候,计数器减为0,这个任务才会被真正释放。 基于这个思想,我们可以使用share_ptr来管理定时任务对象

下面是秒级时间轮和定时任务对象类的代码实现

using TaskFunc = std::function<void()>;
using ReleaseFunc = std::function<void()>;

//定时任务类
class TimerTask{
public:
    TimerTask(uint64_t id, uint32_t delay, const TaskFunc &cb)
    :_id(id)
    ,_timeout(delay)
    ,_task_cb(cb)
    {}
    //析构的时候执行定时任务
    ~TimerTask(){
        if(_canceled == false) _task_cb(); //定时任务执行
        _release(); //释放定时器任务对象
    }
    //设置release_cb回调函数
    void SetRelease(const ReleaseFunc& cb)
    {
        if(_canceled == false)
        _release = cb;
    }
    //返回定时器任务超时时间
    uint64_t TimeOut()
    {
        return _timeout; 
    }
    //取消定时任务
    void Cancel()
    {
        _canceled = false;
    }
 private:
    uint64_t _id;   //定时器任务对象ID
    uint32_t _timeout;  //定时任务的超时时间
    TaskFunc _task_cb;  //定时器对象要执行的定时任务
    ReleaseFunc _release;   //用于删除TimerWheel中保存的定时器对象信息
    bool _canceled; //定时任务是否被取消

};
using TaskWeak = std::weak_ptr<TimerTask>;     //别名:指向TimerTask类的对象强引用,会增加引用计数
using TaskPtr = std::shared_ptr<TimerTask>;    //别名:指向TimerTask类的对象弱引用,不会增加引用计数

//时间轮类
class TimerWheel{
public:
    TimerWheel()
    :_capacity(60)
    ,_tick(0)
    ,_wheel(_capacity)
    {}
    //添加定时任务(将管理任务对象的智能指针添加到时间轮当中)
    void TimerAdd(uint64_t id, uint32_t timeout, const TaskFunc &cb)
    {
        //使用智能指针管理定时类任务
        TaskPtr tp(new TimerTask(id, timeout, cb));
        //添加WeakPtr与id的关联关系
        _timers[id] = TaskWeak(tp);
        //释放定时任务对象(这样做的好处可以添加当前任务id,及各种信息)
        tp->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));
        uint64_t pos = (timeout + _tick) % _capacity;
        //添加任务
        _wheel[pos].push_back(tp);
    }
    // 刷新/延迟定时任务(查看定时任务是否还存在,存在就刷新时间轮)
    void TimerRefresh(uint64_t id)
    {
      auto it = _timers.find(id);
      if(it == _timers.end()) return;
      //刷新定时任务
      TaskPtr tp = (it->second).lock();
      int pos = (tp->TimeOut() + _tick) % _capacity;
      _wheel[pos].push_back(tp);
    }
    //取消定时任务(如果管理任务对象的智能指针存在,则设置任务对象状态为取消状态)
    void TimerCancel(uint64_t id)
    {
        auto it = _timers.find(id);
        if(it == _timers.end()) return; //未找到定时任务
        TaskPtr tp = it->second.lock();
        if(tp) tp->Cancel();
    }
    //执行定时任务,此函数一秒被执行一次,相当于秒针向后走一步(清空时间轮当中的智能指针)
    void RunTimerTask()
    {
        _tick = (_tick + 1) % _capacity;
        _wheel[_tick].clear();  //清空数组指定的位置,将所有管理定时任务对象的share_ptr释放掉
    }

    private:
    //SetRelease回调函数,表示任务已经执行完,从unordered_map中将定时任务信息删除
    void RemoveTimer(uint64_t id)
    {
        auto it = _timers.find(id);
        if(it != _timers.end()) _timers.erase(it);
    }
    int _tick; //秒针:走到哪里,执行哪里的任务
    int _capacity; //表盘的最大数量---最大延迟时间
    std::vector<std::vector<TaskPtr>> _wheel;   //时间轮
    std::unordered_map<uint64_t, TaskWeak> _timers; //定时器任务id与管理定时任务对象的weak_ptr之间的关联关系
}; 

class Test {
public:
    Test() { std::cout << "构造" << std::endl; }
    ~Test() { std::cout << "析构" << std::endl; }
};

void Deltest(Test *t) { delete t; }

int main()
{
    Test *t = new Test();
    TimerWheel tw;
    tw.TimerAdd(1, 5, std::bind(Deltest, t));
    // 刷新定时任务
    for(int i = 0; i < 5; i++) {
        sleep(1);
        // 刷新定时任务
        tw.TimerRefresh(1);  
        // 向后移动秒针
        tw.RunTimerTask();
        std::cout << "刷新,定时任务5s后启动" << std::endl;
    }
    while(true) {
        std::cout << "滴答滴答..." << std::endl;
        tw.RunTimerTask();
        sleep(1);

    }
    return 0;
}

在这里插入图片描述

正则表达式介绍(了解知道怎么使用)

由于我们要实现的是一个带有应用层协议 (HTTP) 支持的服务器组件,因此必然会涉及到对 HTTP 请求的解析,比如我们接收到了这样的一个 HTTP 请求

那么我们需要从 HTTP 请求中提取出以下的信息:

GET – 请求方法,我们需要针对不同的请求方法执行不同的操作

/login – 请求URL,我们需要知道客户端想要访问哪里的数据

user=Fan_558&pass=123456 – 请求数据

HTTP/1.1 – 协议版本

正则表达式是基于某种字符串匹配规则来提取字符串中的特定数据。
对于简单的 regex 使用,我们只需要掌握regex_match 函数的使用即可:

bool regex_match(string src, smatch matches, regex e);
src: 用于匹配的原始字符串;
e: 字符串的匹配规则;
matches: 用于存在根据匹配规则e对原始字符串src进行匹配得到的结果;
返回值:匹配成功返回true,匹配失败返回false

常用的正则表达式的匹配规则:

\

将下一个字符标记为一个特殊字符、或一个原义字符、或一个向后引用、或一个八进制转义符

*

匹配前面的子表达式零次或多次

+

匹配前面的子表达式一次或多次

?

匹配前面的子表达式零次或一次

.

匹配除“\n”之外的任何单个字符

x|y

匹配x或y

代码示例

bool test() {
    std::string str = "GET /login?user=zhangsan&pass=123456 HTTP/1.1\r\n";
    std::smatch matches;

    // 提取请求方法:(GET|POST|PUT|DELETE|HEAD) 
    // |表示或者   ()表示提取匹配结果   (GET|POST|PUT|DELETE|HEAD)整体表示提取其中任意一个字符串
    // 提取请求URI:_([^?]*) 
    // 我们用_表示空格  [^?]表示匹配除?号外的所有字符  *表示可以多次匹配
    // 提取数据:\\?(.*) 
    //\\?表示普通的?字符
    //(.*)表示可以多次匹配任意字符并提取
    //外边的(?:)?表示如果没有数据则匹配但不提取

    //提取协议版本:_ (HTTP/1\\.[01]) 
    // _表示空格
    //HTTP/1\\.表示匹配原字符串中的HTTP/1\\.   其中\\.表示普通的.  最后[01]表示匹配0或者1

    // \r\n处理:(?:\n|\r\n)? 
    //(?:xxx)表示匹配xxx格式字符串但不提取   最后的?表示执行前面的表达式一次或零次
    std::regex e("(GET|POST|PUT|DELETE|HEAD) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?");
    if(std::regex_match(str, matches, e) == false) return false;
    for(int i = 0; i < matches.size(); i++) {
        std::cout << i << ":" << matches[i] << std::endl;
    }
    return true;
}

通用型any容器的实现

由于后续将会对socket内核接收缓冲区到来的数据进行HTTP协议格式的解析处理,我们需要拿到一个完整的HTTP请求,由于 TCP
是面向字节流的,因此服务器在接收客户端数据的时候就可能出现 socket
中的数据不足一条完整请求的情况,因此我们需要为客户端连接设置一个请求处理的上下文,用来保存请求接收,解析以及处理的状态,它决定着下一次从缓冲区中取出的数据如何进行处理,从哪里开始处理等。同时对于一条完整的请求,我们还需要对其进行解析,得到各种关键的要素,比如HTTP请求中的请求方法、请求URL、HTTP版本等,这些信息都会被保存到请求处理的上下文当中。另外地,今天我们上层使用的是HTTP协议,服务器可支持的协议可能会不断增多,不同的协议,可能都会有不同的上下文结构,所以需要有一个容器结构不同结构的数据

C++中,boost库和C++17给我们提供了⼀个通⽤类型any来灵活使⽤,如果考虑增加代码的移植性,尽量减少第三⽅库的依赖,则可以使⽤C++17特性中的any,或者⾃⼰来实现。

这里我们选择自己实现
如何设计一个可以保存任意类型数据的容器呢?我们首先能够想到的起始时模板类,模板可以接收任意类型的数据,但是当实例化对象的时候,必须传入一个指定的类型

预期设计:
不带有任何的特定的类型,但是可以保存或接收各种类型的数据
Any a; 
a = "Fan";
a = 1;

于是我们便可以在Any类中设计一个继承体系,子类placeHolder专门用于保存其它类型的数据,而父类Holder的指针去指向派生类,父类只是一个普通类不带有模板参数,再将父类Holder的指针作为Any类的成员变量,这样就可以达到我们的预期设计,不带有任何特定的类型(实例化对象的时候不需要指定类型),就可以保存任意数据类型的数据

class Any{
private:
    class Holder
    {}
    template<class T>
    class placeHolder : public Holder{
    public:
        T _val;
    };
     Holder* _content;
};

具体代码如下

class Any{
private:
    class Holder{
        public:
            virtual ~Holder() {}
            virtual const std::type_info& type() = 0;
            virtual Holder* clone() = 0;
        };

    template<class T>
    class placeHolder : public Holder{
    public:
        placeHolder(const T &val): _val(val){}
        virtual ~placeHolder() {}
        //获取子类对象保存的数据类型
        virtual const std::type_info& type()   
        {
            return typeid(T);
        }
        //针对当前的对象自身,克隆出一个新的子类对象
        virtual Holder* clone()
        {
            return new placeHolder(_val);
        }
    public:
        T _val;
    };
    
     Holder* _content;
public:
    //空的构造,直接赋值即可
    Any() :_content(nullptr) {}
    template<class T>
    Any(const T &val) :_content(new placeHolder<T>(val)) {}
    //通过其它的通用型容器构造自己的容器
    Any(const Any &other)
    : _content(other._content ? other._content->clone() : nullptr)
    {} 
    ~Any()
    {
        delete _content;
    }
    Any &swap(Any &other)
    {
        std::swap(_content, other._content);
        return *this;
    }

    template<class T>
    //返回子类对象保存的数据的指针
    T* get()
    {
        //想要获取的数据类型,必须要与保存的数据类型一致
        assert(typeid(T) == _content->type());
        //强转程子类对象类型从而能访问到子类的_val
        return &((placeHolder<T>*)_content)->_val;
    }
    template<class T>
    //赋值运算符的重载函数
    Any& operator=(const T &val)
    {
        //为val构造一个临时的通用容器,然后与当前容器自身进行指针交换,临时释放对象的时候,原先保存的书也就被释放
        Any(val).swap(*this);
        return *this;
    }
    Any& operator=(const Any &other)
    {
        //根据一个other构造一个对象
        Any(other).swap(*this);
        return *this;
    } 
};

测试

int main()
{
    Any a;
    a = 10;
    int* pa = a.get<int>();
    std::cout << *pa << std::endl;
    a = std::string("Fan_558");
    std::string* ps = a.get<std::string>();
    std::cout << *ps << std::endl;
    return 0;
}

在这里插入图片描述

小结

下一篇将会向你带来Buffer模块与Socket模块的实现

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

5-26作业

网络聊天室 服务器&#xff1a; 1 #include <myhead.h>2 int main(int argc, const char *argv[])3 {4 if(argc!3)5 {6 printf("请输入IP和端口号\n");7 return -1;8 }9 int sfd socket(AF_INET, SOCK_DGRAM, 0);10 if(…

6千古诗文必背名句大全ACCESS\EXCEL数据库

古诗&#xff0c;是古代诗歌的一种体裁&#xff0c;又称古体诗或古风&#xff0c;指的是产生于唐代以前并和唐代新出现的近体诗&#xff08;又名今体诗&#xff09;相对的一种诗歌体裁。其特点是格律限制不太严格。 从小我们就被教“熟读唐诗三百首,不会吟诗也会吟”&#xff…

男士内裤什么品牌质量好?男内裤品牌排行榜汇总

大家都知道&#xff0c;为了私处健康&#xff0c;每天都必须换内裤。而且&#xff0c;使用频率较高的内裤最好 3&#xff5e;6 个月换一批&#xff0c;一旦变形、材质变干硬或污渍洗不净&#xff0c;就得及时扔&#xff01;但有一说一&#xff0c;现在男性同胞们想挑选到合适自…

MySQL数据表的“增删查改“

我们学习数据库, 最重要的就是要学会对数据表表进行"增删查改"(CRUD).(C -- create, R -- retrieve, U -- update, D -- delete) 目录 一. "增"(create) 1. 普通新增 2. 指定列新增 3. 一次插入多行 4. 用insert插入时间 5. 小结 二. "查"…

css样式,点击 箭头方向上下转换

实现效果&#xff1a; 点击切换箭头方向 实现代码 <divclass"modelPart"click"showClick"><div class"modelPart_left"><img:srcaidefalutIconclass"sNodeIcon"><div>{{ selectModel }}</div><div …

【Java EE】网络原理——HTTP请求

目录 1.认识URL 2.认识“方法&#xff08;method&#xff09;” 2.1GET方法 2.1.1使用Fiddler观察GET请求 2.1.2 GET请求的特点 2.2 POST方法 2.2.1 使用FIddler观察POST方法 2.2.2 POST请求的特点 3.认识请求“报头”&#xff08;header&#xff09; 3.1 Host 3.2 C…

【edge浏览器】控制台报错信息隐藏-恢复

问题描述 解决方法&#xff1a;只需要清空筛选器

进程和用户管理

查看进程的命令 ps top pstree 发送信号命令 kill 使用是后加-l 用户管理命令 添加用户:sudo adduser 用户名 修改组:sudo usermod -G 用户名1 用户名2 修改家目录:sudo usermod -d /home/用户名 -m 用户名 删除用户名:sudo deluser --remove -home 用户名

Java 使用WebMagic爬取网页(简单示例)

框架简介 WebMagic是一个基于Java的开源网络爬虫框架&#xff0c;它提供了很多简单易用的API接口&#xff0c;可以帮助使用者快速构建出高效、可扩展的网络爬虫程序&#xff0c;WebMagic由四个组件(Downloader、PageProcessor、Scheduler、Pipeline)构成&#xff0c;核心代码非…

GitHub打不开的解决方案

1、打开https://sites.ipaddress.com/github.com/找到DNS Resource Records&#xff0c;复制github的ip地址&#xff0c;先保存起来&#xff1a; 140.82.112.32、打开https://sites.ipaddress.com/fastly.net/找到DNS Resource Records&#xff0c;复制其中一个ip地址&#xf…

OC IOS 文件解压缩预览

热很。。热很。。。。夏天的城市只有热浪没有情怀。。。 来吧&#xff0c;come on。。。 引用第三方库&#xff1a; pod SSZipArchive 开发实现&#xff1a; 一、控制器实现 头文件控制器定义&#xff1a; // // ZipRarViewController.h // // Created by carbonzhao on 2…

ARTS Week 31

Algorithm 本周的算法题为 1556. 千位分隔数 给你一个整数 n&#xff0c;请你每隔三位添加点&#xff08;即 "." 符号&#xff09;作为千位分隔符&#xff0c;并将结果以字符串格式返回。 示例 1&#xff1a;输入&#xff1a;n 123456789输出&#xff1a;"123.…

(四)MySQL 事务

欢迎访问 事务有哪些特性&#xff1f; 事务是由 MySQL 的引擎来实现的&#xff0c;我们常见的 InnoDB 引擎它是支持事务的。 不过并不是所有的引擎都能支持事务&#xff0c;比如 MySQL 原生的 MyISAM 引擎就不支持事务&#xff0c;也正是这样&#xff0c;所以大多数 MySQL 的…

凤香的“蜜”密

执笔 | 文 清 编辑 | 古利特 “遇水则漏&#xff0c;遇酒生香”。酒海&#xff0c;一种大型盛酒容器&#xff0c;因盛酒量以“吨”计算&#xff0c;故称“海”&#xff0c;传于唐宋&#xff0c;兴盛于明清&#xff0c;距今有1400多年的历史。文人墨客笔下&#xff0c;也多有…

部署Prometheus + Grafana实现监控数据指标

1.1 Prometheus安装部署 Prometheus监控服务 主机名IP地址系统配置作用Prometheus192.168.110.27/24CentOS 7.94颗CPU 8G内存 100G硬盘Prometheus服务器grafana192.168.110.28/24CentOS 7.94颗CPU 8G内存 100G硬盘grafana服务器 监控机器 主机名IP地址系统配置k8s-master-0…

融合基因组序列识别scATAC-seq的细胞类型

利用scATAC-seq技术进行单细胞分析&#xff0c;可以在单细胞分辨率下深入了解基因调控和表观遗传异质性&#xff0c;但由于数据的高维性和极端稀疏性&#xff0c;scATAC-seq的细胞注释仍然具有挑战性。现有的细胞注释方法大多集中在细胞峰矩阵上&#xff0c;没有充分利用潜在的…

利用基于CNN的人员检测与关键词识别的TinyML实现无接触电梯

目录 说明 论文概述 摘要 引言 现有非接触式电梯解决方案 新解决方案的需求 tinyML实施 系统构建和算法管道 CNN和TinyML实现 结果与讨论 结论 视频演示和代码可用性 一点感想 说明 我一直使用Google Schloar订阅最新的论文消息&#xff0c;今天看到一篇论文的标…

窄通道、非液压、超续航、更安全,地牛AGV小车诠释高效物流!

agv 在智能时代&#xff0c;替代简单、机械、重复以及重体力工作的智能机器设备成为未来发展的趋势。这种趋势不仅可以提高工作效率和质量&#xff0c;还可以解放人力资源&#xff0c;使其更好地应用于创造性和高级智能任务上。 这不&#xff0c;现在有越来越多的工厂开始使用…

json/excel文件上传下载工具方法汇总

文章目录 浏览器下载json文件浏览器下载excel文件【Workbook】浏览器导入json文件【ObjectMapper】浏览器导入excel文件【Workbook】ResourceLoader读取类路径下单个jsonResourceLoader读取类路径下所有json文件 浏览器下载json文件 Operation(summary "设备模型导出(带分…

详解Java ThreadLocal

个人博客 详解Java ThreadLocal | iwts’s blog Java ThreadLocal ThreadLocal提供了线程内存储变量的能力&#xff0c;这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。 TreadLocal存储模型 ThreadLocal的静态…