【深入理解SpringCloud微服务】深入理解微服务中的远程调用,并手写一个微服务RPC框架
【深入理解SpringCloud微服务】深入理解微服务中的远程调用,并手写一个微服务RPC框架
- 远程过程调用
- 微服务中的RPC框架
- 如何实现一个微服务中的RPC框架
- 接口扫描
- 生成代理对象
- 代理对象处理逻辑
- 手写一个微服务RPC框架
- @RPCClient
- @EnableRPCClient
- MicroServiceRPCClientRegistrar
- RPCClientBeanDefinitionScanner
- RPCClientFactoryBean
- RPCInvocationHandler
远程过程调用
远程过程调用简称RPC,也就是本地调用远程服务器上的方法。比如本地调用UserService的getUser(String userId)方法,经过网络通信,调用到远程服务器上的UserServiceImpl的getUser(String userId)方法。
一般的RPC框架,会屏蔽掉网络通信的细节,通过动态代理返回一个代理对象,我们调用这个代理对象,RPC框架就会帮我们向服务器发送RPC请求。
然后服务器那一端接收到RPC请求后,还要解析出接口类型、方法、参数等信息,然后调用对象的实现类进行处理。
如果一个接口有多个提供者提供服务,那么客户端的RPC框架还要处理负载均衡的逻辑。
上面说到的只是RPC中的服务调用流程,在此之前,还有服务暴露(或者叫服务注册)和服务引入(或者叫服务发现)两个过程。比如服务暴露就是把服务提供方的ip地址端口号,以及相关的接口信息发布到注册中心。而服务引入则是从注册中心把服务提供者发布上去的信息拉取到本地,根据该信息生成对应接口的代理对象。
以上这些,都是作为一个RPC框架需要实现的,可以看出来实现一个完整RPC框架还是比较复杂的。而我们作为使用者只要做相应的配置,既可以像调用本地方法一样调用远程服务。
微服务中的RPC框架
比起实现一个完整的RPC框架(比如Duboo),实现一个微服务中的RPC框架则要简单的多。由于微服务本身自带了注册中心和注册中心客户端等组件,服务暴露和服务引入就不需要RPC框架的实现者去考虑了。并且微服务还自带了负载均衡(比如Ribbon),RPC框架的实现者也可以复用,无需重复造轮子。
因此作为微服务中的RPC框架,只需要扫描接口生成代理对象,然后代理对象中复用微服务的负载均衡器即可。
如何实现一个微服务中的RPC框架
接口扫描
接口扫描这一步要做的事情就是扫描出所有需要生成代理对象的接口,这些接口在我们本地没有实现类,都是通过代理对象调用底层的远程调用逻辑请求到远程服务器上的接口实现类。
我们可以定义一个注解(比如OpenFeign的@FeignClient注解),注解需要指定服务名(表示这个接口是请求哪个微服务的),然后通过Spring的ClassPathBeanDefinitionScanner扫描我们自定义的注解修饰的接口,然后生成BeanDefinition,注册到Spring容器中。
生成代理对象
由于要根据扫描到的接口生成代理对象,因此扫描接口时生成的BeanDefinition的beanClass属性需要设置为Spring提供的FactoryBean类型。但是FactoryBean是一个接口,因此我们要实现自己的FactoryBean实现类,重写FactoryBean的getObject()方法,getObject()方法生成接口的代理对象,我们可以使用JDK动态代理。
代理对象处理逻辑
如果我们使用的是JDK动态代理,那么就会进入我们实现的InvocationHandler的handle()方法。
首先要解析修饰接口的自定义注解,获取注解上的服务名。
除服务名外,我们还要有请求的url,因此一般接口方法上也要有注解,我们们可以复用SpringMVC的注解(@RequestMapping、@GetMapping等),也可以自己重写定义一套。
获取到服务名后,然后要读取接口方法上的注解,解析注解上的url,从注解解析出来的url前面拼接上修饰接口的自定义注解的服务名,就形成了完成的请求地址。
得到请求地址后,我们复用负载均衡微服组件进行客户端负载均衡,然后通过RestTemplate或者OkHttp等其他的http工具发出http请求即可。
手写一个微服务RPC框架
@RPCClient
@RPCClient是自定义的用于修饰接口的注解,功能相当于OpenFegin的@FeignClient,用于被扫描并生成代理对象。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface RPCClient {String serviceName() default "";}
@EnableRPCClient
@EnableRPCClient注解也是自定义注解,功能与OpenFeign的@EnableFeignClients注解类型。用于修饰启动类,表示启动微服务RPC功能。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Import({MicroServiceRPCClientRegistrar.class})
public @interface EnableRPCClient {String[] scanBasePackages() default {};}
scanBasePackages属性指定了包扫描路径,@Import引入了一个MicroServiceRPCClientRegistrar,由MicroServiceRPCClientRegistrar进行包扫描并生成BeanDefinition。
MicroServiceRPCClientRegistrar
MicroServiceRPCClientRegistrar 实现了Spring的ImportBeanDefinitionRegistrar接口,并重写了registerBeanDefinitions方法。在registerBeanDefinitions方法中进行包扫描。
public class MicroServiceRPCClientRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware,ResourceLoaderAware {private Environment environment;private ResourceLoader resourceLoader;public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {// 创建包扫描使用的scannerClassPathBeanDefinitionScanner scanner = new RPCClientBeanDefinitionScanner(registry, false, environment, resourceLoader);// 设置扫描@RPCClient注解scanner.addIncludeFilter(new AnnotationTypeFilter(RPCClient.class));// 解析@EnableRPCClient注解,获取包扫描路径Set<String> packagesToScan = getPackagesToScan(importingClassMetadata);for (String packageToScan : packagesToScan) {// 调用scan方法进行包扫描scanner.scan(packageToScan);}}public void setEnvironment(Environment environment) {this.environment = environment;}public void setResourceLoader(ResourceLoader resourceLoader) {this.resourceLoader = resourceLoader;}private Set<String> getPackagesToScan(AnnotationMetadata metadata) {// 获取@EnableRPCClient注解中的属性AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(EnableRPCClient.class.getName()));// 获取@EnableRPCClient注解属性中的包扫描路径scanBasePackagesString[] basePackages = attributes.getStringArray("scanBasePackages");Set<String> packagesToScan = new LinkedHashSet<String>();packagesToScan.addAll(Arrays.asList(basePackages));return packagesToScan;}
}
Spring会在处理@Import注解时会调用ImportBeanDefinitionRegistrar的registerBeanDefinitions()方法。
MicroServiceRPCClientRegistrar的registerBeanDefinitions()方法:
- 创建一个RPCClientBeanDefinitionScanner,它继承了Spring的包扫描器ClassPathBeanDefinitionScanner
- 解析@EnableRPCClient注解中的包扫描路径scanBasePackages
- 调用RPCClientBeanDefinitionScanner的scan方法进行包扫描
RPCClientBeanDefinitionScanner
RPCClientBeanDefinitionScanner是自定义的包扫描器,继承了Spring的包扫描器ClassPathBeanDefinitionScanner,我们重写doScan()方法,当调用scan()方法时,就会调用到我们重写的doScan()方法。
public class RPCClientBeanDefinitionScanner extends ClassPathBeanDefinitionScanner implements BeanClassLoaderAware {private ClassLoader classLoader;public RPCClientBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters, Environment environment, ResourceLoader resourceLoader) {super(registry, useDefaultFilters, environment, resourceLoader);}@Overrideprotected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {// 只有接口才会被加载return beanDefinition.getMetadata().isInterface();}@Overrideprotected Set<BeanDefinitionHolder> doScan(String... basePackages) {// 调用父类ClassPathBeanDefinitionScanner的doScan()方法进行扫描Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);// 处理扫描结果for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {ScannedGenericBeanDefinition beanDefinition = (ScannedGenericBeanDefinition) beanDefinitionHolder.getBeanDefinition();String beanClassName = beanDefinition.getBeanClassName();Class<?> beanClass = ClassUtils.resolveClassName(beanClassName, classLoader);// 修改beanDefinition 的beanClass属性为RPCClientFactoryBeanbeanDefinition.setBeanClass(RPCClientFactoryBean.class);// 设置RPCClientFactoryBean中的type字段值为接口的类型beanDefinition.getPropertyValues().addPropertyValue("type", beanClass);// 设置自动装配类型为byTypebeanDefinition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);}return beanDefinitionHolders;}@Overridepublic void setBeanClassLoader(ClassLoader classLoader) {this.classLoader = classLoader;}
}
首先调用父类ClassPathBeanDefinitionScanner的doScan()方法进行扫描,获取扫描到的BeanDefinition集合。
然后处理扫描到的BeanDefinition集合:
- 修改BeanDefinition的beanClass属性为RPCClientFactoryBean,RPCClientFactoryBean实现了FactoryBean接口,后续Spring实例化Bean时会调用RPCClientFactoryBean的getObject()方法。我们会在getObject()方法方法中进行动态代理
- 设置RPCClientFactoryBean中的type字段值为接口的类型,type是动态代理时指定的接口类型
- 设置自动装配类型为byType
RPCClientFactoryBean
RPCClientFactoryBean实现了Spring的FactoryBean接口,并重写了getObject()方法。Spring在实例化Bean时,如果BeanDefinition的beanClass属性是FactoryBean类型的实现类,会调用getObject()方法进行实例化。我们在RPCClientFactoryBean的getObject()方法中使用JDK动态代理生成代理对象。
public class RPCClientFactoryBean<T> implements FactoryBean<T> {@Autowiredprivate LoadBalanceClient loadBalanceClient;private Class<T> type;public T getObject() throws Exception {// JDK动态代理生成代理对象// InvocationHandler是RPCInvocationHandler// RPCInvocationHandler中持有LoadBalanceClient,用于做负载均衡return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{type}, new RPCInvocationHandler(type, loadBalanceClient));}...}
RPCInvocationHandler
当我们调用代理对象的接口方法时,就会进入RPCInvocationHandler的invoke()方法。invoke()方法会进行url拼接、负载均衡重写url、收集方法参数、OkHttp发送请求、解析返回结果等处理。
由于我们这里直接服用了SpringMVC的注解,因此会涉及到很多SpringMVC的注解比如@RequestMapping、@GetMapping等的解析操作。
public class RPCInvocationHandler<T> implements InvocationHandler {private static final Logger LOGGER = LoggerFactory.getLogger(RPCInvocationHandler.class);private Class<T> type;private LoadBalanceClient loadBalanceClient;public RPCInvocationHandler(Class<T> type, LoadBalanceClient loadBalanceClient) {this.type = type;this.loadBalanceClient = loadBalanceClient;}public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 处理接口上的@RPCClient注解,得到服务名serviceNameRPCClient rpcClient = type.getAnnotation(RPCClient.class);String serviceName = rpcClient.serviceName();// 拼接请求url用的StringBuilderStringBuilder urlBuilder = new StringBuilder();// http://拼接上服务名urlBuilder.append("http://").append(serviceName);// 处理接口上的@RequestMapping注解,得到@RequestMapping注解中的url,然后拼接到urlBuilder上RequestMapping requestMapping = type.getAnnotation(RequestMapping.class);appendUrlByRequestMapping(urlBuilder, requestMapping);// 处理方法上的@RequestMapping、@GetMapping、@PostMapping、@PutMapping、@DeleteMapping注解// 也是获取注解上的url进行拼接// 此外还会解析出RequestMethodRequestMethod requestMethod = null;if (method.getAnnotation(RequestMapping.class) != null) {requestMapping = method.getAnnotation(RequestMapping.class);appendUrlByRequestMapping(urlBuilder, requestMapping);RequestMethod[] methods = requestMapping.method();if (methods != null && methods.length > 0) {requestMethod = methods[0];}} else if (method.getAnnotation(GetMapping.class) != null) {...} else if (method.getAnnotation(PostMapping.class) != null) {...} else if (method.getAnnotation(PutMapping.class) != null) {...} else if (method.getAnnotation(DeleteMapping.class) != null) {...}// 完整的请求url(负载均衡重写前)String requestUrl = urlBuilder.toString().trim();// 调用LoadBalanceClient进行客户端负载均衡,重写url,// LoadBalanceClient是我们的微服务负载均衡组件,这里直接服用requestUrl = loadBalanceClient.reconstructUrl(serviceName, requestUrl);// 处理参数,注解包括:@PathVariable、@RequestParam、@RequestBodyMap<String, Object> requestParamMap = new HashMap<>();...// 适用OkHttp发送请求OkHttpClient okHttpClient=new OkHttpClient();...// 处理返回结果okhttp3.ResponseBody responseBody = null;...}private void appendUrl(StringBuilder urlBuilder, String[] paths) {if (paths.length > 0) {String path = paths[0];if (!path.startsWith("/")) {urlBuilder.append("/");}urlBuilder.append(path);}}private void appendUrlByRequestMapping(StringBuilder urlBuilder, RequestMapping requestMapping) {if (requestMapping != null) {String[] paths = requestMapping.value();appendUrl(urlBuilder, paths);}}
}
我们使用一个StringBuilder进行url拼接,首先是获取到@RPCClient上的服务名,http://加上服务名作为url的开头拼接到StringBuilder上;然后解析接口上的@RequestMapping注解得到注解上的url,拼接到StringBuilder上;然后是解析方法上的注解@RequestMapping、@GetMapping、@PostMapping、@PutMapping、@DeleteMapping等,解析得到注解上的url,同样的拼接到StringBuilder上。
然后调用LoadBalanceClient进行负载均衡操作,重写url,LoadBalanceClient是直接复用了我们的微服务负载均衡组件。
然后就是收集方法参数,使用OkHttp发送请求,处理返回结果。都是一些简单但是繁琐的代码,因此直接省略掉了。
代码已上传至https://gitee.com/huang_junyi/simple-microservice,如果想看详细代码的话可以到上面去下载。
相关文章:

【深入理解SpringCloud微服务】深入理解微服务中的远程调用,并手写一个微服务RPC框架
【深入理解SpringCloud微服务】深入理解微服务中的远程调用,并手写一个微服务RPC框架 远程过程调用微服务中的RPC框架如何实现一个微服务中的RPC框架接口扫描生成代理对象代理对象处理逻辑 手写一个微服务RPC框架RPCClientEnableRPCClientMicroServiceRPCClientRegi…...

数据结构----二叉树
小编会一直更新数据结构相关方面的知识,使用的语言是Java,但是其中的逻辑和思路并不影响,如果感兴趣可以关注合集。 希望大家看完之后可以自己去手敲实现一遍,同时在最后我也列出一些基本和经典的题目,可以尝试做一下。…...

通过python管理mysql
打开防火墙端口: 使用 firewall-cmd 命令在防火墙的 public 区域中永久添加 TCP 端口 7500(FRP 控制台面板端口)、7000(FRP 服务端端口)以及端口范围 6000-6100(一组客户端端口)。这些端口是 FR…...

Run the OnlyOffice Java Spring demo project in local
Content 1.Download the sample project in java2.Run the project3.Test the example document 1.Download the sample project in java Link: download the sample code in official website document properties setting spring 项目所在的服务器 server. Address192.168…...

11. Rancher2.X部署多案例镜像
**部署springboot项目 : ** **部署中间件Mysql8.0 : ** 名称:service-mysql 端口 :3306:3306 镜像:mysql:8.0 环境变量: MYSQL_ROOT_PASSWORDxdclass.net168路径映射 /home/data/mysql/data /var/lib/mysql:rw /etc/localtime…...

探索Linux世界之Linux环境开发工具的使用
目录 一、yum -- Linux软件包管理器 1、什么是yum 2、yum的使用 2.1 yum一些经常见的操作 1.查看软件包 2. 安装软件包 3. 删除软件包 3、yum的周边知识 3.1 yum的软件包都是从哪里来的?是从哪里能下载到这些软件包? 3.2 yum的拓展软件源 二、…...

探索Spring Boot微服务架构的最佳实践
目录 引言 一、Spring Boot简介 二、微服务架构的关键要素 三、Spring Boot在微服务中的最佳实践 3.1 清晰的服务边界 3.2 自动化配置与依赖管理 3.3 服务注册与发现 3.4 配置管理 3.5 安全与认证 3.6 监控与日志 3.7 分布式事务 四、总结 引言 在当今快速迭代的软…...

[论文泛读]zkLLM: Zero Knowledge Proofs for Large Language models
文章目录 介绍实验数据实验数据1实验数据2实验数据3 介绍 这篇文章发在CCS2024,CCS是密码学领域的顶会。作者是来自加拿大的University of Waterloo。文章对大语言模型像GPT和LLM等大语言模型实现了零知识可验证执行,但不涉及零知识可验证训练。个人觉得…...

vscode插件中的图标怎么设置
首先在ts文件目录下和package.json同级的目录下加入一张图片,后缀是jpg、png、jpeg都可以。 然后package.json中加入该行 重新 vsce package即可 如果出现报错 The specified icon xxx/xxx/icon.jpg wasnt found in the extension. 那就是没有放正确文件夹的位…...

Study--Oracle-08-oracle数据库的闪回技术
一、闪回恢复区(Flash Recovery Area) 1、什么是闪回恢复区 闪回恢复区是Oracle数据库中的一个特殊存储区域,用于集中存放备份和恢复数据库所需的所有文件,包括归档日志和闪回日志。这个区域可以帮助数据库在遇到介质故障时进行完全恢复。通过将备份数…...

获取客户端真实IP
出于安全考虑,近期在处理一个记录用户真实IP的需求。本来以为很简单,后来发现没有本来以为的简单。这里主要备忘下,如果服务器处于端口回流(hairpin NAT),keepalived,nginx之后,如何取得客户端的…...

韩式告白土味情话-柯桥生活韩语学习零基础入门教学
你们韩国人别太会告白了! 1、너 얼굴에 뭐가 조금 묻었어! 你的脸上有点5376东西! 뭐가 조금 묻었1585757는데? 有点什么? 이쁨이 조금 묻었네. 有点漂亮。 2、돌잡이 때 뭐 잡았어요? 你抓周的时候抓了什么? 쌀 잡았…...

Linux安全与高级应用(一)深入探讨Linux安全与高级应用
文章目录 深入探讨Linux安全与高级应用引言目录一、Linux安全与应用概述1.1 Linux的应用现状1.2 Linux的安全需求 二、构建LAMP企业网站平台2.1 LAMP平台简介2.2 安装和配置Apache服务器2.2.1 安装Apache2.2.2 配置Apache 2.3 安装和管理MySQL数据库2.3.1 安装MySQL2.3.2 配置M…...

【nginx 第二篇章】各个环境安装 nginx
一、Windows环境安装 1、下载 Nginx 访问Nginx官网(http://nginx.org/en/download.html)下载稳定版本的 Nginx 压缩包,如 nginx-1.xx.x.zip。下载后解压到指定的目录,例如 D:\nginx。 2、启动 Nginx 直接双击解压目录下的 ngi…...

在Spring Boot和MyBatis-Plus项目中,常见的错误及其解决方法2.0
1. org.springframework.beans.factory.BeanCreationException: Error creating bean with name requestMappingHandlerMapping 现象 在创建bean时发生错误,通常是因为存在重复的URL映射。 解决方法 检查所有控制器方法上的URL映射注解,确保没有重复…...

招聘信息数据清洗
文章目录 前言代码示例如下 前言 相关知识 为了完成本关任务,你需要掌握: 1.Spark 清洗数据的相关方法, 2.空值列怎么删除; 3.怎么数据切分才能达到想要的数据。 Spark清洗数据相关方法 一、将含有空值的数据删除 1.将含有空值的数据删除&a…...

机器学习——支持向量机(SVM)(1)
目录 一、认识SVM 1. 基本介绍 2. 支持向量机分类器目标 二、线性SVM分类原理(求解损失) 三、重要参数 1. kernel(核函数) 2 .C(硬间隔与软间隔) 四、sklearn中的支持向量机(自查&#…...

Elastic Observability 8.15:AI 助手、OTel 和日志质量增强功能
作者:来自 Elastic Alex Fedotyev, Tom Grabowski, Vinay Chandrasekhar, Miguel Luna Elastic Observability 8.15 宣布了几个关键功能: 新的和增强的原生 OpenTelemetry 功能: OpenTelemetry Collector 的 Elastic 分发:此版本…...

Unity3D ECS架构的优缺点详解
前言 Unity3D作为一款强大的游戏开发引擎,近年来在性能优化和架构设计上不断进化,其中ECS(Entity-Component-System)架构的引入是其重要的里程碑之一。ECS架构通过重新定义游戏对象的组织和处理方式,为开发者带来了诸…...

理解Go语言中多种并发模式
Go 的同步原语使实现高效的并发程序成为可能,并且选择合适的同步原语和并发模式可以更加容易地实现并发的可能,减少错误的发生。这里谈论的并发模式是只在 Go 语言中常见的并发的“套路” ,一种可解决某一类通用场景和问题的惯用方法。 1. 并发模式概述 我们先来回顾下同步…...

C++ primer plus 第17 章 输入、输出和文件:文件输入和输出03:文件模式:二进制文件
系列文章目录 17.4.5 文件模式 程序清单17.18 append.cpp 程序清单17.19 binary.cpp 文章目录 系列文章目录17.4.5 文件模式程序清单17.18 append.cpp程序清单17.19 binary.cpp17.4.5 文件模式1.追加文件来看一个在文件尾追加数据的程序。程序清单17.18 append.cpp2.二进制文…...

网络安全之sql靶场(11-23)
sql靶场(11-23) 目录 第十一关(post注入) 第十二关 第十三关 第十四关 第十五关 第十六关 第十七关 第十八关 第十九关 第二十关 第二十一关 第二十二关 第二十三关 第十一关(post注入) 查看…...

WordPress网站被入侵,劫持收录事件分析
7.15,网站被入侵,但是直到7月17日,我才发现被入侵。 16日,17日正常更新文章,17日查询网站收录数据时,在站长资源平台【流量与关键词】查询上,我发现了比较奇怪的关键词。 乱码关键词排名 起初…...

原生js: 实现三个水平tab按钮, 默认第一个上面有class, 点击另外的实现class=‘cur‘的切换的效果
问: <ul><li class"cur">热门问题</li><li>订阅问题</li><li>使用问题</li></ul> 这是我的代码, 这是我的代码: // 遍历 helpInfoClass 数组helpInfoClass.forEach((item, index) > {var itemId item[0];var i…...

C#语言基础速成Day07
“知止而后有定,定而后能静,静而后能安,安而后能虑,虑而后能得。” 目录 前言文章有误敬请斧正 不胜感恩!||Day07 C#常见数据结构:1. 集合(Collection)1.1 **List<T>**1.2 **H…...

jvm运行时常量池溢出的原因
Java虚拟机(JVM)的运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储类和接口的常量池表,包括字面量和对类型、字段和方法的符号引用。运行时常量池溢出通常指的是常量池的内存使用达到了JVM设…...
floyd算法详解
算法是一种用于求解所有顶点对之间的最短路径问题的算法,特别适用于稠密图。下面是一个使用C实现的算法示例: #include <iostream> #include <climits> // For INT_MAXusing namespace std;const int V 4; // 图的顶点数// 定义一个函数来…...

Web前端性能优化的方向
减少dom渲染复杂列表优化缓存优化首页背景图片加载慢,可以放在服务器上,读取绝对路径900k的图片大小有些大,可以对图片进行压缩,tinypng网站压缩、熊猫压缩、bing域名下的图片url后面带参数w、h、qlt剪裁下拉框数据较多进行懒加载…...

【面试题】设计模式-责任链模式
设计模式-责任链模式 前言责任链简历案例代码小结 前言 我们知道,设计模式是面试时经常被问到的问题之一,这是因为设计模式能够体现出代码设计的美感,且在很多框架的底层也都会使用到各种设计模式,所以对设计模式的考察ÿ…...

JavaEE 第8节 单例模式详解
目录 概念 饿汉模式 懒汉模式 懒汉模式在多线程环境下的优化 1.线程安全问题 2.效率问题 3.指令重排序导致的问题 1)为什么要进行指令重排序? 2)指令重排序在上述代码为什么会构成问题? 导读: 单例模式是一种…...