开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

java实现短视频推荐打散算法

发表于 2021-11-13

这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

“上次写的思路还在上次的文章中”

之前写了短视频推荐打散算法的思路:【图解】短视频推荐打散算法
,为了保证推荐视频的层次感,不让相同作者的视频堆积在一起,使用springboot+java简单的实现了单轮询打散算法。

以下只是自己进行打散的逻辑,可能有一堆坑😓,只是个实现思路。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
java复制代码@Slf4j
@Service
public class FirmVideoService {

@Autowired
private RedisList redisList;
@Autowired
private RedisHash redisHash;
@Autowired
private FixSortMapper fixSortMapper;


/**
* 查询推荐视频ID列表
* @param videoId 游标:视频ID,默认从头(0)开始。视频游标可以每次获取视频时都能拿到还未看过的视
频。可以解决分页查询视频时,会出现新增视频,旧的视频被挤到下一页,导致重复视频问题。
* @param isFix 判断是否插入固定视频
* @param size 每页大小
*/
public List<VideoInfo> queryVideo(Integer videoId,int size,boolean isFix) {
List<VideoInfo> videos = new ArrayList<>();
LinkedHashSet dataSet = new LinkedHashSet<>();
// 查询视频id对应作者id
Map<Object, Object> objectMap = redisHash.hmGet("authors");

// 将固定视频放入集合中
List<Integer> fixList = new ArrayList<>();
LinkedHashSet<Integer> authors = new LinkedHashSet<>();

// videoId默认值或有固定标识,就优先显示固定视频
if(0 == videoId || isFix){
this.queryfixIds(fixList,objectMap,authors,size);
}else{
Integer[] fixIds = this.initVideo(size);
fixList = Arrays.asList(fixIds);
}

// 查询计算后的视频id集合
dataSet.addAll(this.queryFirmByRedis(videoId,size));

// 构建结果集
List<Integer> result = this.buildData(dataSet,fixList,authors,objectMap);
result.forEach(s->{
VideoInfo videoInfo = new VideoInfo();
videoInfo.setVideoId(s.longValue());
videos.add(videoInfo);
});

return videos;
}

/**
* 查询固定视频id
* @param fixList 视频ID列表
* @param objectMap 视频作者对应关系
* @param authors 作者记录set
* @param size 每页大小
*/
private void queryfixIds(List<Integer> fixList,Map<Object, Object> objectMap, LinkedHashSet<Integer> authors,int size){
Integer[] fixIds = this.initVideo(size);
this.addFixSorts(fixIds);
fixList.addAll(Arrays.asList(fixIds));

// 将固定视频放入结果集,并记录作者信息
fixList.forEach(h->{
AtomicInteger userIdAto = new AtomicInteger(0);
Optional.ofNullable(objectMap.get(h.toString())).ifPresent(u->{
userIdAto.set(Integer.parseInt(objectMap.get(h.toString()).toString()));
});
int authorId = userIdAto.get();
// 记录作者信息
authors.add(authorId);
});
}

/**
* 查询计算后的视频
* @param videoId 上次看的视频ID
* @param size 每页大小
* @return
*/
private List queryFirmByRedis(Integer videoId,int size){
// 查询全部视频id
Object msi = redisList.lRange("videos",0,-1);

try {
Optional.ofNullable(msi).orElseThrow(()->new Exception("firm video is null"));
} catch (Exception e) {
log.error("firm video is null");
return new ArrayList();
}
ArrayList videoList = JSON.parseObject(msi.toString(), ArrayList.class);

int index = 1;
if(0 != videoId){
// 获取上次查看视频的索引
index += videoList.indexOf(videoId);
}

// 分页获取视频列表
return videoList.subList(index,index+size);
}


/**
* 固定热门视频添加
* @param fixIds 返回视频ID列表
*/
private void addFixSorts(Integer[] fixIds){
// 配置强插,则其余视频按照优先级取余下视频,补足到 4 个
List<FixSorts> fixSorts = fixSortMapper.select();

// 强插数据不为空,顶替原视频
if(!CollectionUtils.isEmpty(fixSorts)){
fixSorts.forEach(h->{
int index = h.getMediafixSort().intValue() - 1;
int videoId = h.getTargetId().intValue();
fixIds[index] = videoId;
});
}
}

/**
* 递归构建视频列表
* @param unused 未放到结果集中的视频ID
* @param result 结果列表
* @param tmp 临时表,保存作者ID
* @param objectMap 视频对应作者映射
* @return
*/
private List<Integer> buildData(LinkedHashSet<Integer> unused,List<Integer> result,LinkedHashSet<Integer> tmp,Map<Object, Object> objectMap){

// 只要有未放入结果集的视频就继续走插入逻辑
Optional.ofNullable(unused).filter(u->u.size()>0).ifPresent(s->{
s.forEach(videoId->{
AtomicInteger userIdAto = new AtomicInteger(0);
// 根据videoId查找作者ID
Optional.ofNullable(objectMap.get(videoId.toString())).ifPresent(u->{
userIdAto.set(Integer.parseInt(objectMap.get(videoId.toString()).toString()));
});
int userId = userIdAto.get();
// 判断是否这次作者ID没有出现过
if(!tmp.contains(userId)){
// 往无数据的索引中插入视频id
for (int i = 0; i < result.size(); i++) {
if(0 == result.get(i)){
result.set(i,videoId);
tmp.add(userId);
break;
}
}
}
});
if(tmp.isEmpty()){
return;
}
// 清空作者信息
tmp.clear();
log.info("buildData result:{}",result);

// 过滤,只保留还未放到结果集中的id
LinkedHashSet<Integer> next = unused.stream().filter(e->!result.contains(e)).collect(Collectors.toCollection(LinkedHashSet::new));
// 继续下一次放入结果
buildData(next,result,tmp,objectMap);
});
return result;
}

/**
* 初始化列表
* @param size 每页大小
* @return
*/
private Integer[] initVideo(int size){
Integer[] arr = new Integer[size];
for (int i = 0; i < size; i++) {
arr[i] = 0;
}
return arr;
}

}

代码逻辑解释

queryVideo是获取推荐视频的入口方法,排序好的视频id列表(videos)和视频与作者id(authors)关系映射缓存在redis中。

首先判断是否显示固定位的视频,如果显示,就将fixList结果列表中对应槽放入固定位视频,并记录作者在authors集合中。

查询出全部准备推荐的视频,放到集合dataSet中。

准备好数据后进入buildData中递归将未放到结果集中的视频ID放入结果集,递归退出条件是没有临时记录的作者信息tmp,tmp为空就意味着result已经满了。

参考

  • redis工具类参考:SpringBoot 操作 Redis 详解

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Cloud Alibaba 入门学习笔记第四篇

发表于 2021-11-13

什么是Gateway路由网关

关于Gateway路由网关,这篇文章介绍的非常详细:Gateway网关简介及使用

Spring Cloud Gateway 特性:(官方文档机翻)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
markdown复制代码	基于 Spring Framework 5、Project Reactor 和 Spring Boot 2.0

能够匹配任何请求属性的路由。

谓词和过滤器特定于路由。

断路器集成。

Spring Cloud DiscoveryClient 集成

易于编写谓词和过滤器

请求速率限制

路径重写

Spring Cloud Gateway: 官方文档地址

总的来说就是一个牛皮的基于Spring WebFlux的请求处理框架

简单使用

引入JAR

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码    <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- nacos 注册中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

配置路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
yml复制代码server:
port: 8000
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 开启路由网关服务发现
lower-case-service-id: true # 服务名转为小写
url-expression: 'lb://'+serviceId # 服务调用方式 serviceId表示注册中心的服务名称
routes:
# 服务转发
- id: test-service #必填 路由id
uri: lb://service-a # 必填 对应上面的url-expression 表示调用注册中心里面的名为service-a的服务
predicates: # 必填 至少有一种匹配方式
- Path=/api/** # 匹配所有api开头的路径 (其他匹配方式见官方文档)
filters: # 非必填
- StripPrefix=1 # 将路径的第一个值去掉后转发 例如 /api/getUser 会变成 /getUser
# 路由转发
- id: cookie_route
uri: http://hjljy.cn/ # 转发到http://hjljy.cn/
predicates:
- Cookie=name, hjljy # 如果携带cookie,参数名为name,值为hjljy,则转发
nacos:
server-addr: 127.0.0.1:8848 # 注册中心地址
discovery:
service: gateway

使用测试

1 启动nacos注册中心

2 启动路由网关服务(需要先保证注册中心存在一个名为service-a的服务,并且有个接口 /getUser )
3 访问 127.0.0.1:8000/api/getUser 查看是否返回正确的数据

注意事项

  • 需要去掉对spring-boot-starter-web 的依赖,不能同时存在
  • 服务名避免使用下划线链接

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Cloud Alibaba 入门学习笔记第三篇

发表于 2021-11-13

Spring Cloud Alibaba 入门学习笔记第二篇:Nacos注册中心+Loadbalancer负载均衡
学习完使用Spring Cloud Loadbalancer进行的负载均衡调用后,会发现调用的代码不是很优雅,使用OpenFeign能够让调用代码变得如调用本地服务一样!!!

OpenFeign简介

OpenFeign是一个声明式 Web 服务客户端。它使编写 Web 服务客户端变得更容易。要使用 Feign 创建一个接口并对其进行注释。它具有可插入的注释支持,包括 Feign 注释和 JAX-RS 注释。Feign 还支持可插拔的编码器和解码器。Spring Cloud 添加了对 Spring MVC 注解的支持,并支持使用HttpMessageConvertersSpring Web 中默认使用的注解。Spring Cloud 集成了 Eureka、Spring Cloud CircuitBreaker 和 Spring Cloud LoadBalancer,在使用 Feign 时提供负载均衡的 http 客户端。

在Spring Cloud 2020版之前是基于Ribbon进行的优化处理(因为Feign当时内置的Ribbon),在Spring Cloud 2020版后基于Spring Cloud LoadBalancer进行的优化处理。

官方文档:docs.spring.io/spring-clou…

核心注解

@EnableFeignClients

主要作用:在springboot启动类上添加,告诉程序去识别@FeignClient注解。
主要属性如下:

1
2
3
4
5
6
7
8
9
scss复制代码String[] value() default {};  //指定扫描的包

String[] basePackages() default {}; //指定扫描的包

Class<?>[] basePackageClasses() default {}; //指定扫描的包

Class<?>[] defaultConfiguration() default {}; //针对FeignClient的默认配置信息

Class<?>[] clients() default {}; //指定扫描的FeignClient

通常是无需配置各项属性,直接使用即可。

@FeignClient

标注用于声明Feign客户端可访问的Web服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scss复制代码@AliasFor("name")
String value() default ""; //和value等价,必填 通常填需要调用的服务名,例如:在注册中心存在一个名为Pay的服务,这里填写Pay就会去自动调用Pay服务

String contextId() default ""; //针对同一个服务的调用(即name相同)写在多个类当中,就用contextId进行区分

@AliasFor("value")
String name() default "";

String[] qualifiers() default {}; //别名

String url() default ""; //Pay服务存在 192.168.1.1 192.168.1.2等多个地址时,指定具体地址

boolean decode404() default false; // 调用服务出现404,是否抛出异常,还是解析数据

Class<?>[] configuration() default {}; // feignClient相关配置,例如: 链接超时,日志级别,响应解码等等

Class<?> fallback() default void.class; //出现异常,回滚

Class<?> fallbackFactory() default void.class; //异常回滚工厂处理

String path() default ""; //请求统一前缀

boolean primary() default true; //存在多个相同的bena,定义为首选的bean

参考文章:那天晚上和@FeignClient注解的深度交流

代码实现

第一步 引入JAR包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
xml复制代码<!-- spring cloud 版本管理 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2020.0.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- spring cloud 注册中心 ,负载均衡,openfeign -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- spring boot web 相关jar-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

第二步 代码实现

创建一个名为feign-provider的服务注册到注册中心

1
2
3
4
5
6
yml复制代码spring:
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
service: feign-provider
1
2
3
4
5
6
7
8
9
java复制代码@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApplication {

public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码@RestController
public class ProviderController {

/**
* 获取name
*
* @param name
* @return {@link String}
*/
@RequestMapping("/provider/{name}")
public String getName(@PathVariable String name) {
return "Hello Im provider" + name;
}
}

创建调用服务用来调用feign-provider

1
2
3
4
5
6
7
8
9
java复制代码@SpringBootApplication
@EnableDiscoveryClient //开启服务注册与发现
@EnableFeignClients //开启FeignClien的支持
public class ConsumeApplication {

public static void main(String[] args) {
SpringApplication.run(ConsumeApplication.class, args);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@FeignClient("feign-provider")   //指定调用注册中心里面名为feign-provider的服务
@Service //注入为bean
public interface OpenFeignService {

/**
* 获取name
*
* @param name
* @return {@link String}
*/
@RequestMapping("/provider/{name}")
String getName(@PathVariable String name);
}
1
2
3
4
5
6
7
8
9
10
11
java复制代码@RestController
public class ConsumeController {

@Autowired
OpenFeignService openFeignService; //注入feignService 进行调用

@RequestMapping("/consume/{name}")
public String getName(@PathVariable String name) {
return openFeignService.getName(name);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
yml复制代码server:
port: 8102
spring:
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
service: feign-consume
# feign 完整配置信息见文档:https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#netflix-feign-starter
feign:
client:
config:
feign-provider: #为 feign-provider这个FeignClient配置属性 如果是default 则所有FeignClient都生效
connectTimeout: 3000 # 请求链接时间 默认是1s
readTimeout: 3000 # 请求响应超时时间 默认是1s
loggerLevel: full # 请求日志打印详情 要配合下面的 debug一起使用
logging:
level:
cn.hjljy.springcloud.openfeign.consume.OpenFeignService: debug

启动测试

1 启动Nacos注册中心
2 启动feign-privoder服务
3 启动feign-consume服务
4 浏览器输入:http://127.0.0.1:8102/consume/123456

image.png

最后

OpenFeign 在使用上,让调用者感觉不出是在进行远程调用,还是挺不错的。同时OpenFeign还能够支持对调用结果的解压,例如通常返回的数据结构是:

1
2
3
4
5
java复制代码   private int code;

private String msg;

private T data;

常常数据是在data里面,就可以通过自定义的encoder进行处理,还有重试机制等等!!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

第九届蓝桥杯Java省赛B组 第九届蓝桥杯Java省赛B组—

发表于 2021-11-13

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

第九届蓝桥杯Java省赛B组——方格计数

题目描述

如图p1.png的图片所示,在二维平面上有无数个1x1大小的小方格。

我们以某个小方格的一个顶点为圆心画一个半径为1000的圆。

你能计算出这个圆里有多少个完整的小方格吗?

注意:需要提交的是一个整数,不要填写任何多余内容。

解题思路

我最开始有一个错误的想法…就是求出圆的面积然后再向下取整。

实际上这个结果算出来并不是完整小方格的个数,结果会比实际正确的答案少很多,因为这种做法求出来会认为两个半圆是一个圆。

后来查了一些别人的解析,这题还是很容易的……

这一题主要是要知道题目的大致意思,所求的圆内的方格需要是完整的方格而不是实际面积上有多少方格。所以只要理解了这一点,这题的算大就很简单了。

image.png
我们从圆心出建立直角坐标系。

只要判断这个点到圆心的距离是否小于圆的半径就能确定这个方格是否是完整的在这个圆中。

程序代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
js复制代码package day_01;

public class 方格计数 {

public static void main(String[] args) {
/*
我们以某个小方格的一个顶点为圆心画一个半径为1000的圆。
你能计算出这个圆里有多少个完整的小方格吗?
*/

int r=1000;
int sum=0;

for(int x=1;x<=r;x++) {
for(int y=1;y<=r;y++) {
if((x*x+y*y)<=r*r) {
sum++;
}
}
}

System.out.print(sum*4);
}

}

代码关键步骤

两个for循环,分别判断x轴与y轴的关系是否符合。

1
2
3
4
5
6
7
js复制代码for(int x=1;x<=r;x++) {
for(int y=1;y<=r;y++) {
if((x*x+y*y)<=r*r) {
sum++;
}
}
}

image.png

结果演示

结果为:3137548

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java的JVM运行时栈结构和方法调用详解 1 运行时栈结构

发表于 2021-11-13

「这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战」。

详细介绍了Java 的JVM的运行时栈结构,以及和方法调用详解,包括解析调用和分派调用。

JVM对于方法的执行是基于栈的,方法调用——入栈,方法调用完毕——出栈,了解JVM的运行时栈结构,有助于我们更加深入的分析、理解字节码和方法调用的执行过程。

而对于方法调用的学习,可以帮助我们从字节码层面了解方法的重载和重写调用的规则。

1 运行时栈结构

1.1 栈帧

**栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。**栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

在编译程序代码时,栈帧需要最大多大的局部变量表、最深多深的操作数栈都已经完全确定,并写入方法表Code属性中,因此一个栈帧需要分配多少内存,不会受程序运行期数据的影响。

一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。但是对于执行引擎来说,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧关联的方法称为当前方法,定义这个方法的类称为当前类。对局部变量表和操作数栈的各种操作,通常都指的是对当前栈帧的对局部变量表和操作数栈进行的操作。
如果当前方法调用了其他方法,或者当前方法执行结束,那这个方法的栈帧就不再是当前栈帧了。当一个新的方法被调用,一个新的栈帧也会随之而创建,并且随着程序控制权移交到新的方法而成为新的当前栈帧。当方法返回的之际,当前栈帧会传回此方法的执行结果给前一个栈帧,在方法返回之后,当前栈帧就随之被丢弃,前一个栈帧就重新成为当前栈帧了。

栈帧是线程私有的数据,不可能在一个栈帧之中引用另外一条线程的栈帧。

典型的虚拟机栈帧结构如下图所示:

在这里插入图片描述

在这里插入图片描述

1.2 局部变量表

局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表中的变量只在当前方法调用中有效, 当方法调用结束后, 随着方法栈帧的销毁, 局部变量表也会随之销毁。

在class类编译的时候,某个方法的局部变量表的最大容量,就在方法的 Code 属性的 max_locals 数据项中确定了下来,而局部变量则是保存在Code属性内的LocalVariableTable属性表中。

局部变量表的容量以变量槽(Variable Slot,下称 Slot)为最小单位,虚拟机规范中并没有明确指明一个 Slot 应占用的内存空间大小,只是很有导向性地说到每个 Slot 都应该能存储一个 boolean、byte、char、short、int、float、reference 或 returnAddress 类型的数据,这 8 种数据类型,都可以使用 32 位或更小的物理内存来存放,在 Java 虚拟机的数据类型中,64 位的数据类型只有 long 和 double 两种,关于这几种局部变量表中的数据有两点需要注意:

  1. reference 数据类型,虚拟机规范并没有明确指明它的长度,也没有明确指明它的数据结构,但是虚拟机通过 reference数据可以做到两点:1. 通过此 reference 引用,可以直接或间接的查找到对象在 Java 堆上的其实地址索引;2. 通过此reference 引用,可以直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息。
  2. 对于 64 位的 long 和 double 数据,虚拟机会以高位对齐的方式为其分配两个连续的 Slot 空间。

在方法执行时,虚拟机是使用局部变量表完成参数变量列表的传递过程,如果是实例方法,那么局部变量表中的每 0 位索引的 Slot 默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字 “this” 来访问这个隐藏的局部变量,其余参数则按照参数列表的顺序来排列,占用从 1 开始的Slot位置,参数表分配完毕后,再跟进方法体内部定义的变量顺序和作用域来分配其余的 Slot。

需要注意的是局部变量并不存在如类变量的”准备”阶段,类变量会在类加载的时候经过“准备”和“初始化”阶段,即使程序员没有为类变量在 “初始化” 赋予初始值,也还是会在”准备”阶段赋予系统的类型默认值,但是局部变量不会这样,局部变量表没有”准备”阶段,所以需要程序员手动的为局部变量赋予初始值。

1.2.1 局部变量表对方法调用的影响

由于局部变量表在栈帧之中,会占用栈空间内存, 因此,如果方法的参数和局部变量较多,使得局部变量膨胀,从而每一次方法数调用就会占用更多的栈空间,最终导致方法数的嵌套调用(比如递归)次数减少。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码public class TestStackDeep {
private static int count = 0;

/**
* 该方法内部有更多的局部变量,方法的最大递归调用次数将会更少
* @param a
* @param b
* @param c
*/
public static void recursion(long a, long b, long c) {
long e = 1, f = 2, g = 3, h = 4, i = 5, k = 6, q = 7, x = 8, y = 9, z = 10;
count++;
recursion(a, b, c);
}

/**
* 该方法内部有更少的局部变量,方法的最大递归调用次数将会更多
*/
public static void recursion() {
count++;
recursion();
}

public static void main(String[] args) {
try {
//recursion(); //切换分别注释这两个方法,运行,观察count的值
recursion(0, 0, 0);
} finally {
System.out.println(count);
}
}
}

运行后,可以看出来,局部变量更少的方法的递归调用深度可以更深。

1.2.2 局部变量表的Solt的复用

每一个局部变量都有自己的作用范围(作用字节码范围),为了尽可能节省栈帧空间, 局部变量表中的变量所在的Slot是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值巳经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class SoltReuse {
public static void solt1() {
//a、b变量作用域都是该方法
int a = 1;
System.out.println(a);
int b = 1;
}

public static void solt2() {
//a变量作用域在该方法的代码块之中
{
int a = 1;
System.out.println(a);
}
//b变量在a变量作用域之后
int b = 1;
}

public static void main(String[] args) {

}
}

使用jclasslib打开class文件,找到两个方法的局部变量表:

在这里插入图片描述

在这里插入图片描述

可以看到,solt2方法的局部变量表Solt实现了复用。

局部变量表的变量也被垃圾回收器作为根节点来判断,只要被局部变量表直接或间接引用到的对象都不会被回收。在某些情况下,Slot的复用会直接影响到系统的垃圾收集行为。

如下案例,vm参数设置为 -XX:+PrintGC,分别运行下面几个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码public class SoltGC {

public void SoltGC0() {
System.gc();
}

public void SoltGC1() {
byte[] b = new byte[5 * 1024 * 1024];
System.gc();
}

public void SoltGC2() {
byte[] b = new byte[5 * 1024 * 1024];
b = null;
System.gc();
}

public void SoltGC3() {
{
byte[] b = new byte[5 * 1024 * 1024];
}
System.gc();
}

public void SoltGC4() {
{
byte[] b = new byte[5 * 1024 * 1024];
}
int c = 10;
System.gc();
}

public void SoltGC5() {
SoltGC1();
System.gc();
}

public static void main(String[] args) {
new SoltGC().SoltGC5();
}
}

其中solt0()方法用作对照。

运行solt0(),本人GC信息为:

[GC (System.gc()) 5202K->848K(249344K), 0.0011430 secs] [Full GC
(System.gc()) 848K->651K(249344K), 0.0046617 secs]

在空方法时,Young GC回收大约5000k,以此作为对照。后面的例子需要排除5000k

运行solt1(),本人GC信息为:

[GC (System.gc()) 10322K->6000K(249344K), 0.0029231 secs] [Full GC
(System.gc()) 6000K->5776K(249344K), 0.0044659 secs]

可以看到,Young GC后还剩下6000k,说明byte数组所占用的内存没有被回收,因为byte数组被局部变量b引用,因此没有回收内存。

运行solt2(),本人GC信息为:

[GC (System.gc()) 10322K->912K(249344K), 0.0011081 secs] [Full GC
(System.gc()) 912K->680K(249344K), 0.0048601 secs]

在垃圾回收前,先将变量b置为null,这样byte就没有了引用。

可以看到,Young GC后还剩下1000k左右,Young GC时把byte数组回收了。

运行solt3(),本人GC信息为:

[GC (System.gc()) 10322K->6000K(249344K), 0.0036167 secs] [Full GC
(System.gc()) 6000K->5800K(249344K), 0.0049001 secs]

我们在变量b的作用域之后进行了垃圾回收,由于变量b的作用域已经结束了,按理说GC应该会回收数组的内存,但是发现byte数组的内存并没有被回收,这是为什么呢?

代码虽然已经离开了变量b的作用域,但在此之后,没有任何对屁部变量表的读写操作——变量b原本所占用的Slot还没有被其他变量复用,所以作为Gc Roots 根节点一部分的局部变量表仍然保持对它的关联,这种关联没有被及时打断,因此内存没有被回收。

运行solt4(),本人GC信息为:

[GC (System.gc()) 10322K->848K(249344K), 0.0014418 secs] [Full GC
(System.gc()) 848K->656K(249344K), 0.0048550 secs]

可以看到内存被回收了,因为垃圾回收时在变量b的作用域之外,并且声明了新变量c,此时变量c会复用变量b的槽位,对数组的引用此时被测底清除,因此随后的GC可以回收数组的内存。

运行solt5(),本人GC信息为:

[GC (System.gc()) 10322K->6000K(249344K), 0.0030734 secs] [Full GC
(System.gc()) 6000K->5800K(249344K), 0.0046043 secs]
[GC (System.gc()) 5800K->5800K(249344K), 0.0006343 secs] [Full GC
(System.gc()) 5800K->680K(249344K), 0.0041057 secs]

可以看到内存在外部方法调用GC方法时被回收了,虽然SoltGC1()犯法不会回收内存,但是SoltGC1()方法返回后,它对应的栈帧也被销毁了,自然局部变量表的的局部变量也不存在了,因此在第二个GC时,数组的内存可以被回收了。

1.3 操作数栈

操作数栈也常被称为操作栈,它也是一个后入先出的栈结构。许多的字节码都需要通过操作数栈进行参数传递,因此它主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

当一个方法刚刚开始执行的时候,操作数栈是空的,在方法执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈、入栈操作。操作数栈中的数据类型必须与字节码指令序列匹配,在编译程序代码时,编译时必须严格保证这一点,在类校验阶段的数据流分析中还要在此验证这一点。

例如,整数加法的字节码指令 iadd 在运行的时候,需要保证操作数栈栈顶两个元素存入int 类型的值。iadd 会取出栈顶两个元素,然后相加,之后把结果再存入操作数栈。

同局部变量表一样,操作数栈的最大深度也是在编译时期就写入到方法表的 Code 属性的 max_stacks 数据项中。操作数栈的每一个元素可以是可以是任意 Java 数据类型,包括 long 和 double,long 和 double 的数据类型占用两个单位的栈深度,其他数据类型则占用以个单位的栈深度。

虚拟机的执行引擎又被称为“基于栈的执行引擎”,其中的“栈”就是操作数栈。

两个栈帧作为虚拟机栈的元素,理论上是完全相互独立的。但在大多数虚拟机的实现里,都会做一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的操作数栈和上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时可以共用一部分数据,无需进行额外的参数复制传递,重叠过程如下图所示:

在这里插入图片描述

1.4 栈帧信息

除了局部变量表和操作数栈外,Java 栈帧还需要一些数据来支持动态链接、方法返回地址等信息,他们统称为栈帧信息。

1.4.1 动态链接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接(Dynamic Linking)。Class 文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候就转化为直接引用,这种转化称为静态解析。另一部分在每次运行期间转化为直接引用,这部分称为动态链接。

1.4.2 方法返回地址

一个方法开始执行后,只有两种方式可以退出这个方法:

  1. 当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的PC计数器可以作为返回地址。
  2. 当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常表来确定。

方法退出时,需要返回到方法被调用的位置,程序才能继续执行。方法正常退出时,调用者的 PC 计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值;而方法异常退出时,返回地址是要通过异常器表来确定的,栈帧中一般不会保存这部分信息,在返回调用方法中后会在调用方法中抛出相同的异常,并尝试查找调用方法的异常表,来解决这个异常。

方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:

  1. 恢复上层方法的局部变量表和操作数栈。
  2. 把返回值压入调用者栈帧的操作数栈。
  3. 调整 PC 计数器的值以指向方法调用指令后面的一条指令。

2 方法调用

方法调用即指确认调用哪个方法的过程,并不是指执行方法的过程。 由于在 java 代码编译成 class 文件之后,在 class 文件中存储的是方法的符号引用(方法在常量池中的符号),并不是方法的直接引用(方法在内存布局中的入口地址),所以需要在加载或运行阶段才会确认目标方法的直接引用。

2.1 解析调用

在类加载的解析阶段,会将一部分方法符号引用转化为直接引用,这种解析成立的前提是:方法在程序运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期间不可变。换句话说,调用目标在程序写好,编译器编译时就可以确定下来了。这类方法调用称为解析。

在 Java 语言中符合“编译期可知,运行期不可变”的方法主要包含静态方法和私有方法两大类。前者与类型直接关联,后者在外部不可见,这两种方法各自的特点决定了他们不可能通过继承或别的方式重写其他版本,因此适合在类加载阶段进行解析。

Java 虚拟机规范里提供了 5 条方法调用字节码指令:

  1. invokestatic:调用静态方法。
  2. invokespecial:调用实例构造器方法、私有方法和父类方法。
  3. invokevirturl:调用(实例)虚方法。
  4. invokeinterface:调用接口方法,会在运行时确定一个实现此接口的对象。
  5. invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的 4 条调用指令,分派逻辑是固化在 Java 虚拟机内部的。而invokedynamic 的分派逻辑由用户设定的引导方法决定。

只要是被 invokestatic 和 invokespecial 指令调用的方法,都可以在解析阶段确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器方法、父类方法,他们在类加载的时候就会把符号引用转化为直接引用。这一类方法被称为非虚方法,相对的其他方法就是虚方法(final 方法除外)。

除了使用 invokestatic 和 invokespecial 调用的方法之外,还有一种就是被 final 修饰的方法。虽然 final 方法是使用 invokevirtual 来调用的,但由于它无法被覆盖,又没有其他版本,所以无需对方法接收者进行多态选择。在 Java 虚拟机规范中,明确说明了 final 方法是非虚方法。

解析调用是一个静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期才去完成。而分派 (Dispatch )调用则可能是静态的,也可能是动态的,根据分派的宗量数又可以分为单分派、多分派。这两类分派方式的两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派 4 种分派组合情况。

2.1 分派调用

Java 是一门面向对象的程序语言,因为 Java 具备面向对象的基本特征:继承、封装、多态。分派调用将会揭示多态性特征的一些最基本的体现,比如“重载”和“重写”在 Java 虚拟机之中是如何实现的。

2.1.2 静态分派

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码public class StaticDispatch {
/*几个重载方法*/
public void sayHello(Human guy) {
System.out.println("hello guy");
}
public void sayHello(Man guy) {
System.out.println("hello gentleman");
}
public void sayHello(Woman guy) {
System.out.println("hello lady");
}

public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
//hello guy
sr.sayHello(man);
//hello guy
sr.sayHello(woman);
}

static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
}

该程序运行结果如下:

hello, guy
hello, guy

为什么会选择参数类型为 Human 的重载方法呢?在解决这个问题前,我们先看两个重要的概念。

Human man = new Man();

对于上面的代码,Human 是变量的静态类型,而 Man 是变量的实际类型。 变量的静态类型是编译期就可以确定的,而实际类型需要等到运行时才能确定。虚拟机(准确的说是编译器)在重载时是通过参数的静态类型而非实际类型来作为判定依据的。因为静态类型是编译期可知的,javac 编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了 sayHello(Human) 作为调用目标,并把这个方法的符号引用写到了 invokevirtual 的参数中。

利用 javap -v StaticDispatch.class查看字节码文件可以验证这一点:

在这里插入图片描述

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,静态分派的典型应用是方法重载。静态分派发生在编译期间,因此确定静态分派的动作实际上不是由虚拟机来执行的。

另外,编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是“唯一的”,往往只能确定一个“更加合适的”版本。 这种模糊的结论在由0和1构成的计算机世界中算是比较“稀罕”的事情,产生这种模糊结论的主要原因是字面量不需要定义,所以字面量没有显示的静态类型,它的静态类型只能通过语言上的规则去理解和推断。

下面代码演示了何为“更加合适的”版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
java复制代码/**
* 重载方法匹配优先级
*
* @author lx
*/
public class Overload {

public static void sayHello(Object arg) {
System.out.println("hello Object");
}

public static void sayHello(int arg) {
System.out.println("hello int");
}

public static void sayHello(long arg) {
System.out.println("hello long");
}

public static void sayHello(Character arg) {
System.out.println("hello Character");
}

public static void sayHello(char arg) {
System.out.println("hello char");
}

public static void sayHello(char... arg) {
System.out.println("hello char...");
}

public static void sayHello(long... arg) {
System.out.println("hello Character...");
}

public static void sayHello(Serializable arg) {
System.out.println("hello Serializable");
}

public static void main(String[] args) {
sayHello('a');
}
}

直接运行代码,上面会输出: hello char

因为‘a’是一个char类型的数据,自然会寻找参数类型为char的重载方法。

如果注释掉sayHello(char arg)方法,那输出会变为: hello int

这时发生了一次自动类型转化,‘a’除了可以代表一个字符串,还可以代表数字97(字符‘a’的Unicode数值为十进制数字97),因此参数类型为int的重载也是合适的。

继续注释掉sayHello(int arg)方法,那输出会变为: hello long

这时发生了两次自动类型转换,‘a’转型为整数97之后,进一步转型为长整数97L,匹配了参数类型为long的重载。笔者在代码中没有写其他的类型如float、double等的重载,不过实际上自动转型还能继续发生多次,按照char->int->long->float->double的顺序转型进行匹配。但不会匹配到byte和short类型的重载,因为char到byte或short的转型是不安全的。

继续注释掉sayHello(long arg)方法,那输出会变为: hello Character

这时发生了一次自动装箱,‘a’被包装为它的封装类型java.lang.Character,所以匹配到了参数类型为Character的重载。

继续注释掉sayHello(Character arg)方法,那输出会变为: hello Serializable

出现hello Serializable,是因为java.lang.Serializable是java.lang.Character类出现的一个接口,当自动装箱之后,发现还是找不到装箱类,但是找到了装箱类实现了的接口类型,所以紧接着又发生一次自动转型。char可以转型成int,但是Character是绝对不会转型为Integer的,它只能安全地转型为它实现的接口或父类。Character还实现了另外一个接口java.lang.Comparable,如果同时出现两个参数分别为Serializable和Comparable的重载方法,那它们在此时的优先级是一样的。编译器无法确定要自动转型为哪种类型,会提示类型模糊,拒绝编译。程序必须在调用时显示地指令字面量的静态类型,如:sayHello((Comparable)’’a),才能编译通过。

下面继续注释掉sayHello(Serializable arg)方法,输出会变为: hello Object

这时是char装箱后转型为父类了,如果有多个父类,那将在继承关系中从下往上开始搜索,越接近上层的优先级越低。即使方法调用传入的参数值为null时,这个规则仍然适用。

把sayHello(Object arg)也注释掉,输出将会变为: hello char…

7个重载方法已经被注释得只剩一个了,可见变长参数的重载优先级是最低的,这时候字符‘a’被当做一个数组元素。

静态方法会在类加载期就进行解析,而静态方法显然也是可以拥有重载版本的,选择重载版本的过程也是通过静态分派完成的。

补充: 使用idea开发的工作者,如果不知道具体使用的那个一版本的方法,可以将鼠标放在调用方法上,然后按住ctrl,然后左键点击该方法,就会自动跳转到具体调用的方法处。

2.1.3 动态分派

动态分派和多态性的另一个重要体现:重写 Override 有着密切的关联。 先看一下动态分派的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
java复制代码/**
* 动态分派
*/
public class DynamicDispatch {
static abstract class Human {
/**
* 父类方法
*/
protected abstract void sayHello();
}

static class Man extends Human {

/**
* 重写方法
*/
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}

static class Woman extends Human {
/**
* 重写方法
*/
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}

public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
//man say hello
man.sayHello();
//woman say hello
woman.sayHello();
//改变实际类型不改变静态类型
man = new Woman();
//woman say hello
man.sayHello();
}
}

执行程序,输出如下所示:

hello man
hello woman
hello woman

虚拟机是怎样去调用哪个方法的?显然这里是不能根据静态类型来决定的,因为静态类型同样都是Human的两个变量man和woman在调用sayHello()方法时执行了不同的行为,并且变量man在两次调用中执行了不同的方法。导致这个现象的原因很明显,是这两个变量的实际类型不同,Java虚拟机是如何根据实际类型来分派方法执行版本的呢?使用javap -v DynamicDispatch.class命令输出这段代码的字节码,尝试从中寻找答案:

在这里插入图片描述

可以看到,字节码中执行 DynamicDispatch$Human.sayHello 的是 invokevirtual 指令,执行之前通过 aload_1 和 aload_2 把相关对象从局部变量表复制到了操作栈栈顶。invokevirtual 指令的运行时解析过程大致分为以下几个步骤:

  1. 找到操作数栈栈顶第一个元素所指向的对象的实际类型,记作 C;
  2. 如果在类型 C 中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验。如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常;
  3. 否则,按照继承关系从下往上依次对 C 的各个父类进行第 2 步的搜索和验证过程。
  4. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常。

由于 invokevirtual 指令的第一步就是在运行期确定接收者的实际类型,所以两次调用中的 invokevirtual 指令把相同的类符号引用解析到了不同的直接引用上,这个过程就是 Java 语言中方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的过程称为动态分派。

2.1.4 单分派与多分派

方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。 单分派是根据一个宗量对目标方法进行选择,多分派是根据多余一个宗量对目标方法进行选择。

在 Java 语言中静态分派要同时考虑实际类型和方法参数,所以 Java 语言中的静态分派属于多分派类型。而在执行 invokevirtual 指令时,唯一影响虚拟机选择的只有实际类型,所以 Java 语言中的动态分派属于单分派类型。

2.1.5 虚拟机动态分派的实现

由于动态分派是非常频繁的动作,而动态分派在方法版本选择过程中又需要在方法元数据中搜索合适的目标方法,虚拟机实现出于性能的考虑,通常不直接进行如此频繁的搜索,而是采用优化方法。

其中一种“稳定优化”手段是:在类的方法区中建立一个虚方法表(Virtual Method Table, 也称vtable, 与此对应,也存在接口方法表——Interface Method Table,也称itable)。使用虚方法表索引来代替元数据查找以提高性能。其原理与C++的虚函数表类似。

虚方法表中存放的是各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类中该方法相同,都指向父类的实现入口。虚方法表一般在类加载的连接阶段进行初始化。

相关文章:

  1. 《Java虚拟机规范 JavaSE8版》
  2. 《深入理解Java虚拟机》
  3. 《实战Java虚拟机》

如果有什么不懂或者需要交流,各位可以留言。另外,希望点赞、收藏、关注一下,我将不间断更新Java各种教程!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

C++-多线程数据共享问题和互斥锁 线程数据共享问题 不变式

发表于 2021-11-13

@[toc]

线程数据共享问题

多线程的优势之一就是线程之间可以共享数据,但我们需要一套规则规定哪个线程可以访问哪部分数据,什么时候可以访问,以及怎么通知其他关心该数据的线程已经更新了数据,如果不能处理好数据共享的问题,多线程的这个优势也会变为劣势。
线程间共享数据的所有问题都是因为对数据的修改,一个只读的数据不会造成任何问题。

不变式与竞争条件

这里要提到一个概念:不变式(invariant),意思是关于特定数据结构的陈述始终正确。例如:”x变量保存着列表中元素的个数”,但这些不变式常常在数据更新的时候被破坏。

考虑一个双向链表,节点保存着指向上一个和下一个节点的指针,这里的不变式之一就是”如果你沿着A节点的下一个指针找到了B节点,那么通过B节点的上一个指针也能找到A“,当我们执行删除节点操作时,经历以下步骤:

  1. 确定删除的节点
  2. 更新删除节点的上一节点的下一个指针
  3. 更新删除节点的下一个节点的上一个指针
  4. 删除节点
    在2和3步骤,这个不变式就会破坏(此时关于该数据结构的陈述是错误的),直到删除操作完成时才为真。
    假如现在有三个节点a,b,c,当线程在删除b节点时执行完步骤2,此时另一个线程在执行删除c节点的操作,此时c通过指针找到的上一个节点是b,在访问和修改b节点指针或其他相关数据的时候,b节点可能已经被删除,从而导致程序崩溃。
    这是竞争条件(race condition)的一个典型例子,在并发领域中,竞争条件的含义是结果依赖于线程执行的顺序,有时这些不同的结果都可以接受,但有时这些结果可能破坏不变式,那么就会造成问题。我们说的竞争条件通常是后者。竞争条件可以细分很多种,如数据竞争(多个线程对同一对象的修改)。
    竞争条件经常出现在执行一个操作,该操作包含修改多个独立数据的情况下,当部分数据被修改,另一个线程可能会访问该数据。
    竞争条件通常和比较难以发现和复现,在调试环境下由于改变了程序的执行速度,因此竞争条件通常没法调试中复现。

使用并发技术的编写程序的复杂性主要来自于避免竞争条件。

避免竞争条件

解决竞争条件的最简单方法就是把数据通过某种保护机制保护起来,保证只有正在修改数据的线程可以看到操作的中间过程(2,3步骤)。
另一种方式是改变数据结构设计和不变式,使得每次修改数据都是不可再分的操作,写每次修改都能保持不变式,这通常意味着lock-free programming(后面介绍)。
还有一种方式是把修改数据看作事务,即software transactional memory,有兴趣的自行了解一下。

互斥锁

C++标准库提供了std::mutex,有一个lock和unlock方法,但我们通常不直接调用这两个方法,而是使用std::lock_guard,其实现了互斥锁的RAII,构建时锁定互斥锁,析构时解锁互斥锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cpp复制代码#include <list>
#include <mutex>
#include <algorithm>
std::list<int> some_list;
std::mutex some_mutex;
void add_to_list(int new_value)
{
std::lock_guard<std::mutex> guard(some_mutex);
some_list.push_back(new_value);
}
bool list_contains(int value_to_find)
{
std::lock_guard<std::mutex> guard(some_mutex);
return std::find(some_list.begin(),some_list.end(),value_to_find)
!= some_list.end();
}

当有互斥锁已经被一个线程获取,其他线程执行到std::lock_guard<std::mutex> guard(some_mutex);或者some_mutex.lock()时,线程会阻塞在该语句,直到锁被释放。注意,同一线程不能多次获取同一个锁,否则会引发异常。
上面的代码保证了add_to_list和list_contains调用总是互斥的,通常情况下,这足以保护数据的安全,但是要注意,如果接口返回值或者参数包含了相关数据的指针或者引用,就能做到在没有互斥锁保护的情况下随意访问数据,因此这依赖于接口的设计。
除此以外,也不要使用用户的可调用对象来处理数据。如下代码足以说明这一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
cpp复制代码class some_data
{
int a;
std::string b;

public:
void do_something();
};
class data_wrapper
{
private:
some_data data;
std::mutex m;

public:
template <typename Function>
void process_data(Function func)
{
std::lock_guard<std::mutex> l(m);
func(data);//危险!
}
};
some_data *unprotected;
void malicious_function(some_data &protected_data)//被保护数据的引用
{
unprotected = &protected_data;//保存数据地址
}
data_wrapper x;
void foo()
{
x.process_data(malicious_function);
unprotected->do_something();//随意访问
}

总之,不要将受保护数据的指针和引用传递到锁保护范围之外的,无论是通过从函数返回值、还是将其存储在外部可见的内存中,还是将其作为参数传递给用户提供的可调用对象中。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

二叉树刷题总结:二叉搜索树的属性

发表于 2021-11-13

「这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战」

二叉搜索树中的搜索

题目

给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。

解题思路

二叉搜索树是一棵有序树,它有着以下的这些特征:

  1. 左子树上的所有节点的值小于根节点的值;
  2. 右子树上的所有节点的值大于根节点的值;

于是,我们可以用递归的方式来求解。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码class Solution {
// 递归,利用二叉搜索树特点
public TreeNode searchBST(TreeNode root, int val) {
if (root == null || root.val == val) {
return root;
}
// 如果小于根节点
if (val < root.val) {
return searchBST(root.left, val);
} else {
// 反之
return searchBST(root.right, val);
}
}
}

时间复杂度:O(H), H 为二叉搜索树的高度。

空间复杂度:O(H), 递归栈的深度为 H。

是不是二叉搜索树

题目

给定一个二叉树,判断其是否是一个有效的二叉搜索树。

解题思路

二叉搜索树有一特性是:在中序遍历下,输出的二叉搜索树节点的数值是一个有序序列。

于是,我们可以通过中序遍历的方式,来判断该序列是否是有序的,从而来判断该二叉树是否是一个有效的二叉树。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public long pre = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
if (root == null) {
return true;
}

if (!isValidBST(root.left)){
return false;
}
if (root.val <= pre) {
return false;
}
pre = root.val;
return isValidBST(root.right);
}
}

时间复杂度 : O(n),其中 n 为二叉树的节点个数;

空间复杂度 : O(n),其中 n 为二叉树的节点个数;

求二叉搜索树的最小绝对差

题目

给你一棵所有节点为非负值的二叉搜索树,请你计算树中任意两节点的差的绝对值的最小值。

解题思路

因为二叉搜索树是一棵有序树,通过中序遍历,我们可以得到一个有序数组,这就转化为在有序数组中求最值。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int ans;
int pre;
public int getMinimumDifference(TreeNode root) {
ans = Integer.MAX_VALUE;
pre = -1;
inOrder(root);
return ans;
}

public void inOrder(TreeNode node){
if (node == null) {
return;
}
inOrder(node.left);
if (pre == -1) {
pre = node.val;
} else {
ans = Math.min(ans, node.val - pre);
pre = node.val;
}
inOrder(node.right);
}
}

时间复杂度:O(n),其中 n 为二叉搜索树节点的个数。

空间复杂度:O(n), 递归栈的深度为节点数 n。

求二叉搜索树的众数

题目

给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。

解题思路

  1. 对二叉搜索树进行中序遍历,这样就得到了一个有序数组;
  2. 利用哈希表,来存储有序数组每个元素出现的次数,并记录次数最高的数值 maxLen;
  3. 遍历哈希表,根据 maxLen,求得出现频率最高元素的合集;

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
java复制代码/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
private List<Integer> list;
public int[] findMode(TreeNode root) {
list = new ArrayList();
inOrder(root);

Map<Integer, Integer> map = new HashMap();
int maxLen = Integer.MIN_VALUE;
for (int value :list) {
map.put(value, map.getOrDefault(value, 0) + 1);
if (maxLen < map.get(value)) {
maxLen = map.get(value);
}
}

if (maxLen > 0) {
List<Integer> ans = new ArrayList<>();
for (int b:map.keySet()) {
if (map.get(b).equals(maxLen)) {
ans.add(b);
}
}
int answ[] = new int[ans.size()];
for (int i = 0; i < ans.size(); i++) {
answ[i] = ans.get(i);
}
return answ;
}

return new int[0];
}

public void inOrder(TreeNode node){
if (node == null) return;
inOrder(node.left);
list.add(node.val);
inOrder(node.right);
}
}

时间复杂度:O(n), 即遍历这棵树的复杂度。

空间复杂度:O(n), 即递归的栈空间的空间代价。

最后

好了,关于二叉搜索树的属性到这里就结束了。相信看完这篇文章,你会对二叉搜索树的属性有一定的了解,感谢你的阅读。

往期文章:

  • StoreKit2 有这么香?嗯,我试过了,真香
  • 看完这篇文章,再也不怕面试官问我如何构造二叉树啦!
  • 那帮做游戏的又想让大家氪金,太坏了!
  • 手把手带你撸一个网易云音乐首页 | 适配篇
  • 手把手带你撸一个网易云音乐首页(三)
  • 手把手带你撸一个网易云音乐首页(二)
  • 手把手带你撸一个网易云音乐首页(一)
  • 代码要写注释吗?写你就输了
  • Codable发布这么久我就不学,摸鱼爽歪歪,哎~就是玩儿
  • iOS 优雅的处理网络数据,你真的会吗?不如看看这篇
  • UICollectionView 自定义布局!看这篇就够了

请你喝杯 ☕️ 点赞 + 关注哦~

  1. 阅读完记得给我点个赞哦,有👍 有动力
  2. 关注公众号— HelloWorld杰少,第一时间推送新姿势

最后,创作不易,如果对大家有所帮助,希望大家点赞支持,有什么问题也可以在评论区里讨论😄~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

springboot整合pagehelper

发表于 2021-11-13

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

在日常工作中怎么分页呢?咱们可以在sql里面拼接limit,但是这样是不是有点low,所以我又访问了一下伟大的百度,又知道了一个插件pagehelper,那么下面介绍一下pagehelper。

1.整合

1.添加pom文件

首先在pom文件中加入依赖。同理版本方面可以访问官网。pagehelper.github.io/ 同时里面还有官方文档,想仔细研究的大佬可以去瞧瞧。

1
2
3
4
5
js复制代码       <dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<pagehelper.version>1.2.5</pagehelper.version>
</dependency>

2.编写controller

我们在controller中实现分页,首先判断前台是否传过来分页的入参,如果没有直接查询,如果PageNum,PageSize传了,直接使用 page = PageHelper.startPage()分页查询。也可以使用Long total = page.getTotal();获取总条数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
js复制代码public Result<*>(*VO *VO) {
Result<*> resultVO = new Result<>();
// 分页
Page page = new Page();
if (!CommonUtil.isEmpty(*VO.getPageNum()) && !CommonUtil.isEmpty(*VO.getPageSize())) {
page = PageHelper.startPage(*VO.getPageNum(), *VO.getPageSize());
resultVO.setPageNum(*VO.getPageNum());
resultVO.setPageSize(*VO.getPageSize());
}
// ASC是根据id 正向排序,DESC是反向排序
if (!CommonUtil.isEmpty(*VO.getOrder())) {
PageHelper.orderBy(*VO.getOrder());
}
// 业务查询 只有这一句是业务查询!!!
List<*VO> result = *Service.query(*VO);
// 分页总数封装
Long total = page.getTotal();
resultVO.setTotal(total);
// 实体封装
resultVO.setData(result );
return resultVO;
}

这里有个坑,如果controller中有两个查询语句,那么第二个查询也会加上第一个的查询条件,如果有该情况,可以建立service,然后分别调用。

3.service层

很简单,没啥说的,普通的增删改查即可,这里不加以展示。#

4.配置类

需要在配置类中加入以下配置,helperDialect根据数据库类型修改。

1
2
3
4
5
js复制代码pagehelper:
helperDialect: mysql
reasonable: true
supportMethodsArguments: true
params: count=countSql

然后我们直接调用接口就ok了,顺便说一下pagehelper是一种物理分页方式(拼接liimit到sql),会直接在数据库中查询出想要的数据,而不是全部查询出来,然后在内存中再次截取,所以效率是比较好的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Security 自动登录与注销登录

发表于 2021-11-13

「这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战」。

一、自动登录

自动登录实质上是指将用户的登录信息保存在用户浏览器cookie中,当用户下次访问时,自动实现校验并建立登录状态的一种机制。

处于安全考虑会将用户信息先加密,再存于cookie中。

Spring Security提供了两种非常好的令牌

  • 用散列算法加密用户登录信息并生成令牌
  • 数据库等持久性数据存储机制用的持久化令牌

1. 散列加密方案

修改配置configure()(基于过滤器实现图形验证码代码修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码@Autowired
private MyUserDetailsService myUserDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/api/**").hasRole("USER")
.antMatchers("/app/api/**", "/captcha.jpg").permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/myLogin.html")
// 指定处理登录请求的路径,修改请求的路径,默认为/login
.loginProcessingUrl("/mylogin").permitAll()
.failureHandler(new MyAuthenticationFailureHandler())
.and()
//增加自动登录功能,默认为散列加密
.rememberMe()
.userDetailsService(myUserDetailsService)
.key("autologin")
.and()
.csrf().disable();
// 将过滤器添加在UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(new VerificationCodeFilter(), UsernamePasswordAuthenticationFilter.class);
}

在自定义页面中添加remember-me checkbox

注意: name必须为remember-me

1
2
3
4
5
html复制代码<div>
<label>
<input name="remember-me" type="checkbox" value="true"> 记住我
</label>
</div>
  • 启动项目
  • 访问api:http://localhost:8080/user/api/hi
    image-20201021142423275.png
  • 勾选记住我
  • 登录
  • F12打开控制台查看cookie

image-20201021142818523.png

在源码中可以找到与之对应代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
public static final String SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY = "remember-me";
public static final String DEFAULT_PARAMETER = "remember-me";
public static final int TWO_WEEKS_S = 1209600;
private static final String DELIMITER = ":";
protected final Log logger = LogFactory.getLog(this.getClass());
protected final MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserDetailsService userDetailsService;
private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private String cookieName = "remember-me";
private String cookieDomain;
private String parameter = "remember-me";
private boolean alwaysRemember;
private String key;
private int tokenValiditySeconds = 1209600;
private Boolean useSecureCookie = null;
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
...

Spring Security每次登录成功之后会更新令牌

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = this.retrieveUserName(successfulAuthentication);
String password = this.retrievePassword(successfulAuthentication);
if (!StringUtils.hasLength(username)) {
this.logger.debug("Unable to retrieve username");
} else {
if (!StringUtils.hasLength(password)) {
UserDetails user = this.getUserDetailsService().loadUserByUsername(username);
password = user.getPassword();
if (!StringUtils.hasLength(password)) {
this.logger.debug("Unable to obtain password for user: " + username);
return;
}
}

int tokenLifetime = this.calculateLoginLifetime(request, successfulAuthentication);
long expiryTime = System.currentTimeMillis();
expiryTime += 1000L * (long)(tokenLifetime < 0 ? 1209600 : tokenLifetime);
String signatureValue = this.makeTokenSignature(expiryTime, username, password);
this.setCookie(new String[]{username, Long.toString(expiryTime), signatureValue}, tokenLifetime, request, response);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Added remember-me cookie for user '" + username + "', expiry: '" + new Date(expiryTime) + "'");
}

}
}

加密部分

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码 protected String makeTokenSignature(long tokenExpiryTime, String username, String password) {
String data = username + ":" + tokenExpiryTime + ":" + password + ":" + this.getKey();

MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException var8) {
throw new IllegalStateException("No MD5 algorithm available!");
}

return new String(Hex.encode(digest.digest(data.getBytes())));
}

key默认情况下是随机生成的

1
2
3
4
5
6
7
8
9
10
java复制代码private String getKey() {
if (this.key == null) {
if (this.rememberMeServices instanceof AbstractRememberMeServices) {
this.key = ((AbstractRememberMeServices) rememberMeServices).getKey();
} else {
this.key = UUID.randomUUID().toString();
}
}
return this.key;
}
  • 这样会导致每次重启项目都会重新生成key,使之前浏览器保存的全部失效。
  • 另外当多实例部署时由于实例之间的key都不相同,所以当访问另一个实例时自动登录策略也会失效
  • 合理的做法是指定key

这种方式实现简单,无需花费服务器资源存储自动登录的相关信息。但是存在cookie被盗风险。

2. 持久化令牌方案

相比于散列加密方案采用了更加严谨的安全设计

核心的两个值(都是采用MD5加密过的随机字符串)

  • series 仅在用户使用密码重新登录时更新
  • token 在每一个新的session中都重新生成

这样做的的好处

  • 解决了一个令牌在多端登录的情况
  • 同时校验series和token,如果令牌被盗,当非法登录时,会刷新token的值,这样合法用户自动登录的时候,校验series一样,但是token不一样,可以推测令牌被盗用。

Spring Security使用PersistentRememberMeToken来表明一个验证实体

1
2
3
4
5
6
7
8
java复制代码public class PersistentRememberMeToken {
private final String username;
private final String series;
private final String tokenValue;
//最后一次自动登录的时间
private final Date date;
...
}
  • 在数据库中建一张persistent_logins表(存储自动登录信息的表)
1
2
3
4
5
6
7
mysql复制代码CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  • 定制tokenRepository

配置中需要传入一个PersistentTokenRepository实例,此接口定义了持久化令牌的一些必要方法

1
2
3
4
5
6
java复制代码public interface PersistentTokenRepository {
void createNewToken(PersistentRememberMeToken var1);
void updateToken(String var1, String var2, Date var3);
PersistentRememberMeToken getTokenForSeries(String var1);
void removeUserTokens(String var1);
}

可以利用自带的实现类JdbcTokenRepositoryImpl实现持久化,为其指定DataSource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码@Autowired
private DataSource dataSource;
@Autowired
private MyUserDetailsService myUserDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
http.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/api/**").hasRole("USER")
.antMatchers("/app/api/**", "/captcha.jpg").permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/myLogin.html")
// 指定处理登录请求的路径,修改请求的路径,默认为/login
.loginProcessingUrl("/mylogin").permitAll()
.failureHandler(new MyAuthenticationFailureHandler())
.and()
//增加自动登录功能,默认为散列加密
.rememberMe()
.userDetailsService(myUserDetailsService)
.tokenRepository(jdbcTokenRepository)
.and()
.csrf().disable();
// 将过滤器添加在UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(new VerificationCodeFilter(), UsernamePasswordAuthenticationFilter.class);
}
  • 启动项目
  • 访问api:http://localhost:8080/user/api/hi
  • 登录
  • 查看表中自动插入了验证数据

image-20201021164453481.png

  • 重启项目
  • 访问api:http://localhost:8080/user/api/hi
  • 自动登录

image-20201021164636053.png

和我们预料的一样,series不变,验证自动登录成功后刷新token的值
这种方式同样存在令牌被盗的风险

二、注销登录

1. 系统自带

Spring Security自带注销登录逻辑,默认请求 /logout,注销后
HttpSession失效、清空已配置的Remember-me验证,以及清空SecurityContextHolder

2.自行配置

根据自己需要,编写退出登录逻辑,清除登录的一系列信息。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何使用Python基于接口编程

发表于 2021-11-13

软件行业,唯一不变的就是变化。产品经理会变,产品需求会变,代码同样要跟着变。

不同的代码设计,变化所带来的工作量更是不同,有的每改一次需求,近乎一次重构,而有的只需要修改一个配置文件,或者在类里添加一行代码。当然比较好的代码设计,由于有着良好的可扩展性,高内聚,低耦合,因而易维护, 以少变应万变。如果才能有好的代码设计,就需要我们学习设计模式。今天为你分享的是在Python中,如何基于接口编程。

1994 年 GoF 的《设计模式》一书中有一个重要的原则就是:基于接口而非实现编程,英文源文是「Program to an interface,not an implementaion」,这里的所说的 interface,并不是特定编程语言中的接口,它是语言无关的,是指开发者提供给使用者的一个功能列表,理解了这一点非常重要。接口在 java 语言中是有关键字 interface 来实现的,java 不支持类的多重继承,但支持接口的多重继承,所在 java 开发者对接口非常熟悉了,Python 其实完全不需要 Java 那样的设计,但可以借鉴接口的优点。

先通过一个实例来了解下接口到底解决什么问题。

比如你正在实现一个图片上传功能,目前采用七牛云来存储,你的类可能是这样的。

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码class QnyImageStore(object):

def getToken():
pass

def upload_qny(self,image):
print("Qny upload.")
#do something

def download_qny(self,image):
print("Qny download.")
#do something

实际的开发中,代码会有很多行,函数也不止三个,它被成百上千个地方被调用,分散在好几百个文件中。 过了一段时间,公司自建了私有云,要求不能再使用七牛云了,要改成自己的云存储,于是你不得不重新写一个类:

1
2
3
4
5
6
7
8
9
python复制代码class OwnImageStore(object):

def upload_own(self,image):
print("Qny upload.")
#do something

def download_own(self,image):
print("Qny download.")
#do something

然后你在使用七牛去的地方,都进行替换,还要替换函数的名称,最后还要多次测试,生活哪一处没有考虑到,运行报错。好不容易改好了,突然有一天,需求又变了,由于自己的云服务经常出问题,于是要换阿里云。经过上次的一翻痛苦折腾,看到这次又要改,直接吐血。

其实问题就在于你写的代码不够通用,命名不够抽象。假如你的类的函数命名使用 upload,download 这样,那么你修改的代码量可能会减少到一半,只替换一下类名就可以了。实际上,我们可以使用接口来减少代码的改动量:通过接口和实现相分离的模式,封装不稳定的实现,暴露稳定的接口。上下游系统在使用我们开发的功能时,只需要使用接口中声明的函数列表,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低耦合性,提高扩展性。下面就该问题,提供一种基于接口的代码实现方式。

定义一个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码from abc import ABCMeta, abstractmethod

class ImageStore(metaclass = ABCMeta):

@abstractmethod
def upload(self,image):
pass
#raise NotImplementedError

@abstractmethod
def download(self,image):
pass
#raise NotImplementedError

定义了该接口之后,任何继承该接口的类要想正确的使用,必须重写 upload 和 download 方法,否则均会抛出异常,这里我们不需要自己在接口中抛出异常,标准库 abc 已经为我们做好了这些工作。

####定义类,继承接口

目的其实是是为了强制约束,也就是说必须实现 upload 和 download 方法,在编译时进行检查,确保程序的健壮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码class OwnImageStore2(ImageStore):

def upload(self,image):
print("Own upload.")
#do something

def download(self,image):
print("Own download.")
#do something



class QnyImageStore2(ImageStore):

def getToken():
pass

def upload(self,image):
print("Qny upload.")

def download(self,image):
print("Qny download.")
#do something

接下来,我们定义一个接口,可以自动的根据类型来选择调用对应对象的方法。

1
2
3
4
5
6
7
8
9
10
11
python复制代码class UsedStore(object):

def __init__(self, store):
if not isinstance(store, ImageStore): raise Exception('Bad interface')
self._store = store

def upload(self):
self._store.upload('image')

def download(self):
self._store.download('image')

最后,我们可以在配置文件中指明我们使用的是哪个具体的接口:

1
2
3
4
5
6
7
python复制代码
#在其他文件中,应该这样调用
img = QnyImageStore2()
# img = OwnImageStore2() 把这些放在配置文件中,只需要更新配置文件就可以替换
store = UsedStore(img)
store.upload()
store.download()

这样,后面再增加新的图片存储,我们只需要添加相应的类,继承接口,并修改下配置文件即可,减轻大量的代码修改工作。

Python 抽象基类的介绍 (PEP3119)

python 标准库 abc,全称是Abstract Base Classes,它提供以下功能:

  • 一种重载isinstance()和issubclass()的方法
  • 一个新的模块abc作为“Abstract Base Classes支持框架”。它定义了一个用于abc的元类和一个可以用来定义抽象方法的装饰器
  • 容器和迭代器的特定抽象基类,将被添加到 collections 模块

基本原理:

在面向对象程序设计领域,与对象交互的设计模式可以分为两个基本类别,即“调用”和“检查”。

调用是指通过调用对象的方法与对象进行交互。 通常,这会与多态性结合使用,因此调用给定方法可能会根据对象的类型运行不同的代码。

检查是指外部代码(在对象的方法之外)检查该对象的类型或属性,并根据该信息来决定如何处理该对象的能力。

两种使用模式均服务于相同的通用目的,即能够以统一的方式支持处理多种多样且可能新颖的对象,但同时允许为每种不同类型的对象定制处理决策。

在经典的 OOP 理论中,调用是首选的设计模式,并且不鼓励检查,因为检查被认为是较早的过程编程风格的产物。 但是,实际上,这种观点过于教条和僵化,导致了某种设计僵化,与诸如 Python 之类的语言的动态特性大相径庭。

特别是,通常需要以对象类的创建者无法预期的方式处理对象。 内置到满足该对象的每个可能用户需求的每个对象方法中,并非总是最佳的解决方案。 而且,有许多强大的调度哲学与严格地封装在对象中的经典OOP行为要求形成鲜明对比,例如规则或模式匹配驱动的逻辑

另一方面,经典的 OOP 理论家对检查的批评之一是缺乏形式主义和被检查内容的特殊性质。 在诸如 Python 这样的语言中,几乎可以通过外部代码反映并直接访问对象的任何方面,有很多不同的方法来测试对象是否符合特定的协议。 例如,如果询问“此对象是否是可变序列容器?”,则可以寻找“列表”的基类,或者可以寻找名为“ getitem”的方法。 但是请注意,尽管这些测试看似显而易见,但它们都不正确,因为其中一个会产生假阴性,而另一个会产生假阳性。

普遍同意的补救措施是对测试进行标准化,并将其分组为正式形式。 通过继承机制或其他某种方式,通过与每个类关联一组标准的可测试属性,最容易做到这一点。 每个测试都带有一组承诺:它包含有关类的一般行为的承诺,以及有关其他可用的类方法的承诺。

PEP为组织这些测试提出了一种特殊的策略,称为抽象基类(ABC)。 ABC只是添加到对象的继承树中的Python类,以将对象的某些功能发送给外部检查器。 使用isinstance()完成测试,并且特定ABC的存在意味着测试已通过。

此外,ABC定义了建立该类型特征行为的最少方法集。 根据对象的ABC类型区分对象的代码可以相信,这些方法将始终存在。 这些方法中的每一个都附带有ABC文档中描述的广义抽象语义定义。 这些标准的语义定义不是强制性的,但强烈建议使用。

像Python中的所有其他内容一样,这些承诺属于绅士协议的性质,在这种情况下,这意味着尽管该语言确实执行了ABC中做出的某些承诺,但具体类的实现者必须确保 剩下的保留下来。

看完上面的描述,你可以简单的理解为,ABC 是一个基类,继承它,你可以写一个类似于 java 的接口,接口中的方法将始终存在,可以放心使用,不需要再进行探测。

PEP3119 还给了样例代码让你理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码from abc import ABCMeta, abstractmethod

class A(metaclass=ABCMeta):
@abstractmethod
def foo(self): pass

A() # raises TypeError

class B(A):
pass

B() # raises TypeError

class C(A):
def foo(self): print(42)

C() # works

多的不说了,希望你可以正确地使用 ABC,同时强烈推荐,学习 Python,就看 Python 的官方文档和 PEP 提案,这里有最权威的讲解。

此外,设置模式也是非常重要的编程之术和编程之道,它是基本功,基本功如果不够,把一台战斗机放你面前,你都不知道如何欣赏和品味。

掌握了设计模式,再看别人的代码,你会拥有火眼金睛,哪些是战斗机,哪些是拖拉机,对自己的学习和提升也非常有帮助,写的代码也会更加具有可维护性,可读性,可扩展性,灵活性。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…352353354…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%