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

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


  • 首页

  • 归档

  • 搜索

LeetCode通关:求次数有妙招,位运算三连 基础知识 求

发表于 2021-08-03

分门别类刷算法,坚持,进步!

刷题路线参考: github.com/chefyuan/al…

大家好,我是刷题困难户老三,这一节我们来刷几道很有意思的求次数问题,它们都有同一类非常巧妙的解法。

这种解法是什么呢?往下看吧!

基础知识

在开始之前,我们最好先了解一些位运算的基础知识。

原反补码

先简单说一下,原码、反码、补码。

一个数在计算机中的二进制表示形式, 叫做这个数的机器数。机器数是带符号的,在计算机用一个数的最高位存放符号, 正数为0, 负数为1.

比如,十进制中的数 +3 ,假如计算机字长为8位,转换成二进制就是00000011。如果是 -3 ,就是 10000011 。

  • 原码

原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值. 比如如果是8位二进制:

[+1]原 = 0000 0001

[-1]原 = 1000 0001

  • 反码

正数的反码是其本身

负数的反码是在其原码的基础上, 符号位不变,其余各个位取反。

[+1] = [00000001]原 = [00000001]反

[-1] = [10000001]原 = [11111110]反

  • 补码

正数的补码就是其本身

负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)

[+1] = [00000001]原 = [00000001]反 = [00000001]补

[-1] = [10000001]原 = [11111110]反 = [11111111]补

补码是人脑认识里不太直观的一种表示方式,之所以发明补码,是为了让机器以一种一致的方式来处理加法运算。

原反补码

更多知识建议阅读《j计算机组成原理》。

与或非异或运算

在处理整型数值时,位运算符可以直接对组成整型数值的各个位进行操作。这些位运算符在位模式下工作。位运算符包括:&、|、~、^

  • 与(&)

对应位都为1,结果为1,否则结果为0

1
2
3
4
5
java复制代码int a=129;
int b=128;
System.out.println("a与b的结果:"+(a&b));
# 输出
a与b的结果:128

计算过程如下:

1
2
3
java复制代码10000001 &
10000000 =
10000000
  • 或(|)

对应位只要有一个为1,结果是1,否则就为0

1
2
3
4
5
java复制代码int a=129;
int b=128;
System.out.println("a或b的结果:"+(a|b));
# 输出
a或b的结果是:129

计算过程如下:

1
2
3
java复制代码10000001 |
10000000 =
10000001
  • 非(~)

位为0,结果是1;位为1,结果是0

1
2
3
4
java复制代码 int a = 8;
System.out.println("非a的结果:"+(~a));
# 输出
非a的结果:-9

计算过程如下

1
2
3
4
5
6
7
8
java复制代码        //8转换为二进制
1000
// 补符号位
01000
// 取反
10111 (补码)
// 转源码除符号位取反+1
11001
  • 异或(^)

对应位相同,结果是0,否则结果是1

1
2
3
java复制代码1111 ^
0010 =
1101

移位运算

移位运算见名知意,是数字二进位的移动,我们这里只讨论int型的移位运算。

  • << 左移运算符

数值的补码全部左移若干位,符号位和高位丢弃,低位补 0。

  • >> 右移运算符

数值的补码全部右移若干位,符号位不变。

假如int是8位二进制,两个例子如下:

10的补码为0000 1010,左移一位变成20(0001 0100),右移一位变成5(0000 0101)

5的补码为0000 0101,左移一位变成10(0000 1010),右移一位变成2(0000 0010)

求次数问题

LeetCode136. 只出现一次的数字

☕ 题目:136. 只出现一次的数字 (leetcode-cn.com/problems/si…)

❓ 难度:简单

📕 描述:

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

说明:

你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

题目示例

💡思路:

哈希法

用哈希表存储每一个元素出现的次数,最后找到出现一次的元素。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    public int singleNumber(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
//存储元素出现的次数
for (int i = 0; i < nums.length; i++) {
map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
}
//遍历获取出现次数为1的情况
for (int k : map.keySet()) {
if (map.get(k) == 1) {
return k;
}
}
return -1;
}

⏰ 时间复杂度:O(n)

🏠 空间复杂度:O(n)

位运算

题中要求空间复杂度O(1),哈希法明显是不合要求的。

这里有一个全新的方法:位运算。

异或运算有如下特点:

  • 一个数和 0 做异或运算等于本身:a⊕0 = a
  • 一个数和其本身做 异或 运算等于 0:a⊕a = 0
  • 异或 运算满足交换律和结合律:a⊕b⊕a = (a⊕a)⊕b = 0⊕b = b

可以重复分利用异或运算的特性,异或数组所有元素,最后留下的那个就是只出现一次的元素。

1
2
3
4
5
6
7
8
java复制代码    public int singleNumber(int[] nums) {
int ans = 0;
for (int i = 0; i < nums.length; i++) {
//异或运算
ans ^= nums[i];
}
return ans;
}

⏰ 时间复杂度:O(n)

🏠 空间复杂度:O(1)

LeetCode137. 只出现一次的数字 II

☕ 题目:137. 只出现一次的数字 II (leetcode-cn.com/problems/si…)

❓ 难度:中等

📕 描述:

给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 **三次 。**请你找出并返回那个只出现了一次的元素。

题目示例

这道题和 剑指 Offer 56 - II. 数组中数字出现的次数 II 是一样的。

💡思路:

哈希法

第一反应还是哈希法,不用多说了,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码    public int singleNumber(int[] nums) {
if (nums.length == 1) {
return nums[0];
}
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
}
for (int k : map.keySet()) {
if (map.get(k) == 1) {
return k;
}
}
return -1;
}

⏰ 时间复杂度:O(n)

🏠 空间复杂度:O(n)

位运算

好了,又到了我们的主角出场。

将我们的数的二进制位每一位相加,然后对每一位的和与3取余:

位运算

这个原理是什么呢?

如果其他数都出现 3 次,只有目标数出现 1 次,那么每一位的 1 的个数无非有这两种情况,

  • 为 3 的倍数(全为出现三次的数)
  • 3 的倍数 +1(包含出现一次的数)

这个 3 的倍数 +1 的情况也就是我们的目标数的那一位。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码    public int singleNumber(int[] nums) {
int res = 0;
for (int i = 0; i < 32; i++) {
int count = 0;
for (int num : nums) {
//检查第i位是否为1
if ((num >> i & 1) == 1) {
count++;
}
}
if (count % 3 != 0) {
//将第i位设为1
res = res | 1 << i;
}
}
return res;
}

🚗时间复杂度:O(n)

🏠 空间复杂度:O(1)

LeetCode260. 只出现一次的数字 III

☕ 题目:260. 只出现一次的数字 III (leetcode-cn.com/problems/si…)

❓ 难度:中等

📕 描述:

给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。

题目示例

这道题和 剑指 Offer 56 - I. 数组中数字出现的次数 是一模一样的。

💡思路:

这次不是一个重复的元素了,是两个。还是先上我们朴素的哈希法。

哈希法

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码    public int[] singleNumber(int[] nums) {
int[] res = new int[2];
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
}
int index = 0;
for (int k : map.keySet()) {
if (map.get(k) == 1) {
res[index] = k;
index++;
}
}
return res;
}

🚗时间复杂度:O(n)

🏠 空间复杂度:O(n)

位运算[5]

我们在 LeetCode136. 只出现一次的数字 里只用了一个异或就找出了那个出现一次的数字。

这道题怎么办呢?

要是我们能把它分成两组就好了。

怎么分呢?

大家都知道异或运算对应位相同,结果是0,否则结果是1

我们可以根据两个数某一位是否是0和1来把数组分为两组。

例如数组: [12,13,14,17,14,12]

异或的结果是:13^17。

获取分组位

分组位找到了。

那么怎么借助分组位进行分组呢?

13、17的异或值,可以仅保留异或值的分组位,其余位变为 0,例如 11100变成00100。

为什么要这么做呢?在第二题提到,我们可以根据 a & 1 来判断 a 的最后一位为 0 还是为 1,所以我们将 11100变成00100之后,然后数组内的元素 x & 001 即可对 x 进行分组 。

那么我们如何才能仅保留分组位,其余位变为 0 呢?

可以利用 x & (-x) 来保留最右边的 1。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码    public int[] singleNumber(int[] nums) {
int bitMask = 0;
//把数组中的所有元素全部异或一遍
for (int num : nums) {
bitMask ^= num;
}
//保留最右边的1
bitMask &= -bitMask;
int[] res = {0, 0};
for (int num : nums) {
//将数组分成两部分,每部分分别异或
if ((num & bitMask) == 0) {
res[0] ^= num;
} else {
res[1] ^= num;
}
}
return res;
}

总结

三道求次数问题就这么做完了。

求次数问题的朴素做法是Hash法,使用Hash存储元素出现次数。

但是Hash法空间复杂度是O(n),如果要求O(1)的空间复杂度就不行了。

这时候就要灵活利用位运算的方法,位运算的关键在于充分了解位运算的相关应用。

简单的事情重复做,重复的事情认真做,认真的事情有创造性地做。

点赞、关注不迷路,咱们下期见!


博主算法练习生一枚,刷题路线和思路主要参考如下!
参考:

[1]. github.com/chefyuan/al…

[2]. leetcode-cn.com/problems/si…

[3]. blog.csdn.net/White_Idiot…

[4].blog.csdn.net/qq_30374549…

[5].leetcode-cn.com/problems/si…

[6]. www.cnblogs.com/zhangziqiu/…

本文转载自: 掘金

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

Go Web 入门与实战系列:构建 Go Web 服务器

发表于 2021-08-03

这是我参与8月更文挑战的第2天,活动详情查看: 8月更文挑战

Web 应用程序是一种可以通过 Web 访问的应用程序,Web 程序的最大好处是用户很容易访问应用程序,用户只需要有浏览器即可,不需要再安装其他软件。Web 应用对于身处互联网时代的我们来说太普遍。无论哪一种语言,只要它能够开发出与人类交互的软件,它就必然会支持 Web 应用开发。
本系列文章将会介绍 Go Web 的应用与实践。欢迎关注。

上一篇文章介绍了访问 Web 站点的过程。用户在浏览器地址栏输入请求 URL,发起请求。通过 DNS 获取到主机的实际地址,这样就可以访问对应的服务器。服务器根据请求的类型返回给浏览器。了解了我们敲击回车之后,Web 应用访问所涉及的一系列步骤。接下来我们来具体构建 Go Web 服务器。

使用 Go 构建服务器

Go 在标准库中提供 HTTP 协议的支持,通过它可以快速简单的开发一个 Web 服务器。同时,Go 语言为开发者提供了很多便利。在介绍了 Web 相关的工作过程后,我们将会搭建一个 Go 语言版本的 Web 应用程序。

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
go复制代码package main

import (
"fmt"
"log"
"net/http"
"strings"
)

func sayHello(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm() //3 解析参数,默认是不会解析的
fmt.Println(r.Form) //4 输出到服务器端的打印信息
fmt.Println("Path: ", r.URL.Path)
fmt.Println("Host: ", r.Host)
for k, v := range r.Form {
fmt.Println("key:", k)
fmt.Println("val:", strings.Join(v, ""))
}
_, _ = fmt.Fprintf(w, "Hello Web, %s!", r.Form.Get("name")) //5 写入到 w 的是输出到客户端的内容
}

func main() {
http.HandleFunc("/", sayHello) //1 设置访问的路由
err := http.ListenAndServe(":8080", nil) //2 设置监听的端口
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}

如上代码实现了一个简单的 Go Web 程序。在程序中配置了 Web 监听的端口为:8080,通过访问 http://localhost:8080/hello?name=aoho,得到如下图所示的响应结果。

image.png

同时在控制台中,输出了如下的日志信息:

1
2
3
4
5
makefile复制代码map[name:[aoho]]
Path: /hello
Host: localhost:8080
key: name
val: aoho

主函数中的注释 2 用于设置 Web 服务监听的端口号。注释 1 是设置访问的路由,所有的请求都由 sayHello 处理。在 sayHello 方法中,首先会解析参数,默认是不会解析的。我们将部分信息在服务器打印出来(包括请求的路径,请求的 Host 地址,Form 表单中的键值对),并且在注释 5 将指定的表单信息作为输出到客户端的内容,如示例中 的 name 对应的值。

小结

本文主要介绍使用 Go 构建服务器。Go 在标准库中提供了 HTTP 协议的支持,我们通过 net/http 可以很方便地实现一个 Go Web 程序。只要调用 Http 包的两个函数就可以了。下面的文章将会深入实现原理,具体讲解 Go 是如何接收和处理请求。

阅读最新文章,关注公众号:aoho求索

本文转载自: 掘金

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

mybatis源码分析

发表于 2021-08-03

mybatis源码分析

mybatis的流程分析

首先mybatis的源码分两种情况:

  1. 单独的mybatis
  2. 和spring整合的mybatis源码
    这两种情况下的源码分析会有点不同,比如如果是分析mybatis+spring的模式,那么mybatis的开始就是从spring的初始化的时候开始。

在整合spring的情况下,mybatis从哪里开始

首先分析得知mybatis和spring整合的情况下,主要是提供两处关联

  • @MapperScan
  • @Bean—->SqlSessionFactoryBean
    通过源码得知MapperScan其实主要利用spring的Import技术和spring的扩展点ImportBeanDefinitionRegistrar,
    而@Bean仅仅是利用了javaconfig技术配置了一个对象。如果你精通spring技术就可以知道spring最先处理的import也就是MapperScan

@MapperScan

如果你熟悉spring,对spring的初始化流程精通的话,那么就知道spring的初始化大概可以分为两大部分

  1. springBean的实例化之前的工作
  2. spring实例之中和之后的工作
springBean实例化之前的工作

通过分析源码可以得出@MapperScan主要做了3个事情

  1. 扫描出所有的mapper所对应的BeanDefinition。
  2. 把mapper变成FactoryBean,MapperFactoryBean的BeanDefinition
  3. 为BeanDefinition添加一个构造方法的值,因为mybatis的MapperFactoryBean有一个有参构造方法,
    spring在实例化这个对象的时候需要一个构造方法的值,这个值是一个class,后面spring在实例化过程中
    根据这个class返回我们的代理对象
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
java复制代码# 1.把mapper变成FactoryBean
MapperScannerRegistrar.registerBeanDefinitions()
ClassPathMapperScanner.doScan()
ClassPathMapperScanner.processBeanDefinitions()

//这里就是将这个class传递到有参构造方法
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
//将所有的mapper接口改为MapperFactoryBean类
definition.setBeanClass(this.mapperFactoryBean.getClass());


public MapperFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}

public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}

# 2.spring实例化该BeanDefinition

ConstructorResolver.autowireConstructor()

//将传递给构造器的参数,获取到传递过来的mappeer接口
ConstructorArgumentValues cargs = mbd.getConstructorArgumentValues();
spring实例化之中和之后的工作

mybatis主要利用spring的初始方法扩展点来完成对mapper信息的初始化,比如sql语句的初始化,这里的
spring扩展点主要就是afterPropertiesSet,如果你精通spring就会知道afterPropertiesSet这种机制
是如何工作的,说白就是利用MapperFactoryBean去实现initialzingBean,由于MapperFactoryBean是一个
FactoryBean,我们理解为就是一个mapper,所以又可以理解他就是自己获得自己的信息,然后把信息缓存起来,
缓存到一个map当中。mappedStatements

1
2
3
4
5
6
7
8
9
java复制代码org.apache.ibatis.session.Configuration#mappedStatements
org.springframework.dao.support.DaoSupport#afterPropertiesSet
org.mybatis.spring.mapper.MapperFactoryBean#checkDaoConfig
org.apache.ibatis.session.configuration#addMapper(this.mapperInterface);
org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parse();
org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parseStatement()

关键代码
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);

相关流程图

在这里插入图片描述

mybatis一级缓存的各种问题

1.为什么mybatis在整合spring一级缓存就失效了?

因为mybatis和spring的集成包当中扩展了一个类sqlSessionTemplate这个类在spring容器启动的时候被注入给了mapper。
这个类替代了原来的DefaultSqlSession,SqlSessionTemplate当中所有的查询方法不是直接查询,而是经过一个代理对象,代理对象增强了查询方法,主要是关闭了session。

  • 底层原理分析:
  • 在跟踪源码,可以看到这个sqlSession已经被改成了sqlSessionTemplate
    *在这里插入图片描述
  • 进入到sqlSessionTemplate类中,可以发现原来的DefaultSqlSession已经被增强了。
  • 在这里插入图片描述
  • 接着再跟踪到下一步,可以看到已经到DefaultSqlSession代理类的invocationHandler(jdk动态代理)
  • 在这里插入图片描述

和mybatis原生的mapper查询比较,可以看到原生的是使用DefaultSqlSession对象进行查询

  • 在这里插入图片描述

总结:通过源码分析可以看到mybatis一级缓存失效的原理,是sqlSessionTemplate的增强。
思考:为什么这里需要关闭sqlSession?

因为mybatis和spring整合后,将mybatis管理的mapper直接交给了spring管理,但是mybatis也没有将sqlSession暴露给spring,那么当spring执行完代码之后,就不能再拿到sqlSession对象,所以这里spring就只能在执行完之后,就将sqlSession关闭了,这也是没有办法的办法吧。

spring和mybatis整合之后,mybatis是怎么初始化的?

1.mybatis是怎么进行mapper接口扫描?

在上面已经谈论过了,mybatis和spring整合之后,是将mybatis所有的mapper接口,都交给了MapperFactoryBean处理。MapperFactoryBean是一个FactoryBean对象,实际返回的对象是getObject()方法返回的对象。

  • 进入源码分析阶段:
  • 1.通过@MapperScan扫描mybatis所有的mapper接口。原理是使用spring的import的扩展点(ImportBeanDefinitionRegistrar)
  • 在这里插入图片描述
  • 2.实现了ImportBeanDefinitionRegistrar,重写registerBeanDefinitions方法,拿到registry对象,可以往spring容器注册bean
  • 在这里插入图片描述
  • 在这里插入图片描述
  • 3.开始扫描mapper接口
  • 在这里插入图片描述
  • 在这里插入图片描述
  • 4.对扫描得到的mapper接口,进行处理。例如:添加属性,设置自动装配模式,改变beanClass,添加构造方法参数等。
  • 在这里插入图片描述

总结:这里主要是对mapper接口进行初始化的工作,最要是有3个作用。
1.通过@MapperScan注解,应用import技术,进行mapper扫描。
2.将扫描得到的mapper,改变beanClass,使用FactoryBean统一处理。
3.改变beanDefinition的自动装配模式,可以对MapperFactoryBean对象的属性进行自动装配,而不需要使用 @Autowired,避免对spring的强制依赖。

2.mybatis的Mapper接口怎么进行实例化的?
  • 1.mapper接口的MapperFactoryBean对象属性赋值。
  • 在这里插入图片描述
  • 在这里插入图片描述

通过查看MapperFactoryBean,可以看到相关属性,暂且估计以下属性是需要自动装配的:
1.mapperInterface
2.addToConfig
3.setSqlSessionFactory()
4.setSqlSessionTemplate()

  • 下面通过追踪源码进行分析:
  • 在这里插入图片描述
  • 在这里插入图片描述
  • 在这里插入图片描述
  • 接着跟踪到autowireByType方法。
  • 在这里插入图片描述
  • 在这里插入图片描述

总结:MapperFactoryBean需要自动装配的属性:sqlSessionFactory,addToConfig

  • 2.MapperFactoryBean接口实例化之后,怎么扫描该mapper接口对应的sql.xml或注解?

实现了InitializingBean接口,重写了afterPropertiesSet()方法,然后在这个方法进行初始化操作。
问题:为什么不使用@PostConstruct,而使用实现InitializingBean接口?

因为实现InitializingBean接口,重写了afterPropertiesSet()方法,afterPropertiesSet顾名思义,就是要等到所有Properties属性初始化,才执行这个方法。所以mybatis的初始化就是要等到所有属性都初始化了,才初始化mybatis的内容,例如:扫描mapper等操作。

查看MapperFactoryBean的结构有:
MapperFactoryBean — > SqlSessionDaoSupport —> DaoSupport —> impl InitializingBean
所有可以得到MapperFactoryBean ,实现了InitializingBean接口。

InitializingBean是spring的技术,实现了该类,需要重写afterPropertiesSet()方法。
afterPropertiesSet(),顾名思义就是在类所有属性property自动装配后执行。

  • 跟踪源码分析:
  • 在这里插入图片描述
  • 在这里插入图片描述
  • 在这里插入图片描述

spring和mybatis整合总结

到此,mybatis整合到spring所有的关键点就已经分享到这里了。关键点总结一下吧:
1.@MapperSacn,import,ImportBeanDefinitionRegistrar
2.FactoryBean,InitializingBean,AutowireMode

本文转载自: 掘金

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

负载均衡算法

发表于 2021-08-03

负载均衡算法

负载均衡算法说明
  • 负载均衡介绍
  • 负载均衡,英文名称为Load Balance,指由多台服务器以对称的方式组成一个服务器集合,每台服务器都具有等价的地位,都可以单独对外提供服务而无须其他服务器的辅助。
  • 通过某种负载分担技术,将外部发送过来的请求均匀分配到对称结构中的某一台服务器上,而接收到请求的服务器独立地回应客户的请求。
  • 负载均衡能够平均分配客户请求到服务器阵列,借此提供快速获取重要数据,解决大量并发访问服务问题,这种集群技术可以用最小的投资获得接近于大型主机的性能。
  • 负载均衡方式

软件负载和硬件负载

  • 软件负载均衡
  • 常见的负载均衡软件有:nginx,LVS,HAproxy
  • 资料:

这三大软件负载均衡器优缺点:www.ha97.com/5646.html
这三大软件负载均衡器的对比:www.21yunwei.com/archives/58…

  • 硬件负载均衡
  • 常见的负载均硬件件有:Array,F5
  • 负载均衡算法

随机算法,加权轮询,一致性hash,最小活跃数算法

负载均衡算法模拟
  • 数据支持
  • 在这里插入图片描述
(1) 随机算法
1、简单随机算法
  • 在这里插入图片描述

这个简单随机算法使用与每天机器的性能差不多的时候,实际上,生产中可能某些机器的性能更高一点,它可以处理更多的情况,所以,我们可以对每台服务器设置一个权重。

2、加权随机算法——V1

image.png

这个V1版本的加权随机算法的思路比较简单,每个服务器按它所对应的的权重进行复制。

这种实现方法在遇到权重之和特别大的时候就会比较消耗内存,因为需要对ip地址进行复制,权重之和越大那么上文中的ips就需要越多的内存。

3、加权随机算法——V2

下面,介绍另一种实现思路。

假设有一组服务器servers=[A,B,C],对应的权重weights=[5,3,2],权重总和为10。

(1)现在把这些权重平铺在一维坐标上,那么就会有[0,5]区间属于服务器A,[5,8]区间属于服务器B,[8,10]区间属于服务器C。

(2)接下来通过随机数生成一个范围在[0,10]之间的随机数,然后计算该随机数落在哪个区间上即可。

  • 在这里插入图片描述
(2) 轮询算法
1、简单轮询算法
  • 在这里插入图片描述

这种简单轮询算法,很公平,每台服务器轮流来进行服务,但是有的机器性能好,所以能者多劳,和随机算法一样,所以,我们可以对每台服务器设置一个权重。

2、加权轮询算法
  • 在这里插入图片描述

加权轮询算法:
思想:

  1. 例如有服务器servers=[A,B,C],对应权重weights=[2,5,1],总权重为8。
  2. 我们可以理解为有8台服务器,2台A,5台B,1台C,一次调用过来的时候,需
  3. 要按顺序访问,如有10次调用,调用顺序为AABBBBBCAA。

步骤:

  1. 因为调用次数会越来越大,而服务器是固定,需要将调用次数“缩小”,取余
  2. 将权重值平铺在一维坐标值上:[0,2]为A,[2,7]为B,[7,8]为C
  3. 接下来获取该次是第几次请求,需要对总权重做取余运算,获取offset

这种算法有一个缺点:一台服务器的权重特别大的时候,他需要连续的处理请求,但是实际上我们想达到的效果是,对于100次请求,只要有有100*8/50=16次就够了,这16次不一定要连续的访问,比如假设我们有三台服务器servers=[A,B,C],对应权重weights=[2,5,1],总权重为7,那么上述这算法的结果是AAAAABC,那么如果能够是这么一个结果呢:AABACAA,把B和C平均插入到5个A中间,这样是比较均衡的。

3、平滑加权轮询算法

那么就引出了平滑加权轮询
思路:

  1. 每个服务器对应两个权重,分别为weight和currentWeight。其中weight是固定的,currentWeight会动态调整,初始值为0
  2. 当有新的请求进来时,遍历服务器列表,让它的currentWeight加上自身权重,遍历完成后,找到最大的currentWeight。
  3. 并将最大的currentWeight减去权重总和,然后返回相应的服务器即可。

假设:
测试数据:

WEIGHT_LIST.put(“A”, 5);
WEIGHT_LIST.put(“B”, 1);
WEIGHT_LIST.put(“C”, 1);

运算过程如下:

次数 当前currentWeight数组 (currentWeight+=weight) 选择结果max(currentWeight) 减去权重总和后的currentWeight数组 (max(currentWeight)-=sum(weight))
1 [5,1,1] A [-2,1,1]
2 [3,2,2] A [-4,2,2]
3 [1,3,3] B [1,-4,3]
4 [6,-3,4] A [-1,-3,4]
5 [4,-2,5] C [4,-2,-2]
6 [9,-1,-1] A [2,-1,-1]
7 [7,0,0] A [0,0,0]
8 [5,1,1] A [-2,1,1]

如表所示,经过平滑行处理后,得到的服务器序列为[A,A,B,A,C,A,A],相比之前的序列[A,A,A,A,A,B,C],分布性要好一些。初始情况下currentWeight=[0,0,0] ,在第7个请求处理完后,currentWeight再次变回[0,0,0]。
你会惊讶的发现在第8次的时候,当前currentWeight数组又变回了[5,1,1] !!!

  • 具体代码实现如下图:
  • 在这里插入图片描述
(3) 一致性哈希算法

服务器集群接收到一次请求调用时,可以根据请求的信息,比如客户端的ip地址,或请求路径与参数等信息进行哈希,可以得出一个哈希值,特点是对于相同的ip地址,或请求路径和请求参数哈希出来的值是一样,只要能再增加一个算法,能够把这个哈希值映射成一个服务端ip的地址,就可以使相同的请求落到同一服务器上。

因为客户端发起的请求情况是无穷大的,所以对于哈希值也是无穷大的,所以不能把所有的哈希值都进行映射到服务器ip上,所以这里需要用到哈希环。如下图:

  • 在这里插入图片描述

上面的情况是比较均匀,如果出现ip4服务器宕机了,那就是这样的了:

  • 在这里插入图片描述

会发现ip3和ip1直接的范围是比较大的,会有更多的请求落到ip1上,这是不公平的,解决这个问题需要加入虚拟节点:

  • 在这里插入图片描述

其中ip2-1,ip3-1就是虚拟结点,并不能处理节点,而是等同于对应的ip2和ip3服务器。

实际上,这只是处理这种不均衡性的一种思路,实际上就算哈希环本身是均衡的,你也可以增加更多的虚拟节点来使这个环更加平衡,比如:

  • 在这里插入图片描述

这个彩环也是公平的,并且只有ip1,ip2,ip3,ip4是实际的服务器ip,其他的都是虚拟ip。

那么我们怎么实现呢?

  1. 对于我们的服务器ip地址,我们肯定是知道共有多少个,需要多少个虚拟节点也是我们自己控制,虚拟节点多则流量越均衡,另外哈希算法也是很关键的,哈希算法越散列流量也将越均衡。
  2. 这种环,可以使用TreeMap来存储;当一次请求过来,获取该请求的hash值,根据hash值从TreeMap中,拿大于该hash值的子树。
  3. 再从得到的子树中,拿第一个元素即可。
  • 具体代码实现:
  • 在这里插入图片描述
(4) 最小活跃数算法

前面几种方法主要目标是使服务端分配到的调用次数尽量均衡,但是实际情况是这样吗?

调用次数相同,服务器的负载就均衡吗?

当然不是,这里还要考虑每次调用的时间,而最小活跃数算法则是解决这种问题的。

活跃调用数越小,表明该服务提供者效率越高,单位时间内可处理更多的请求。此时应优先将请求分配给该服
务提供者。在具体实现中,每个服务提供者对应一个活跃数。初始情况下,所有服务提供者活跃数均为0。每
收到一个请求,活跃数加1,完成请求后则将活跃数减1。在服务运行一段时间后,性能好的服务提供者处理请
求的速度更快,因此活跃数下降的也越快,此时这样的服务提供者能够优先获取到新的服务请求、这就是最小
活跃数负载均衡算法的基本思想。除了最小活跃数,最小活跃数算法在实现上还引入了权重值。所以准确的来
说,最小活跃数算法是基于加权最小活跃数算法实现的。举个例子说明一下,在一个服务提供者集群中,有两
个性能优异的服务提供者。某一时刻它们的活跃数相同,则会根据它们的权重去分配请求,权重越大,获取到
新请求的概率就越大。如果两个服务提供者权重相同,此时随机选择一个即可。

实现:
因为活跃数是需要服务器请求处理相关逻辑配合的,- -次调用开始时活跃数+1,结束是活跃数-1, 所以这里就
不对这部分逻辑进行模拟了,直接使用一个map来进行模拟。

  • 具体代码实现:
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
java复制代码//最小活跃算法
public class LeastActive {
private static String getServer() {
//找出当前活跃数最小的服务器
Optional<Integer> minValue = ServerIps.ACTIVITY_LIST.values().stream().min(Comparator.naturalOrder());
if (minValue.isPresent()) {
List<String> minActivityIps = new ArrayList<>();
ServerIps.ACTIVITY_LIST.forEach((ip, activity) -> {
if (activity.equals(minValue.get())) {
minActivityIps.add(ip);
}
});
//最小活跃数的ip有多个,则根据权重来选,权重大的优先
if (minActivityIps.size() > 1) {
//过滤出对应的ip和权重
Map<String, Integer> weightList = new LinkedHashMap<>();
ServerIps.WEIGHT_LIST.forEach((ip, weight) -> {
if (minActivityIps.contains(ip)) {
weightList.put(ip, ServerIps.WEIGHT_LIST.get(ip));
}
});
int totalWeight = 0;
boolean sameWeight = true;
Object[] weights = weightList.values().toArray();
//计算出总的权重,判断所有权重是否一样
for (int i = 0; i < weights.length; i++) {
Integer weight = (Integer) weights[i];
totalWeight += weight;
if (sameWeight && i > 0 && !weight.equals(weights[i - 1])) {
sameWeight = false;
}
}
//生成一个在[0,totalWeight]区间内的随机数
java.util.Random random = new java.util.Random();
int randomPos = random.nextInt(totalWeight);
if (!sameWeight) {
for (String ip : weightList.keySet()) {
Integer weight = weightList.get(ip);
if (randomPos < weight) {
return ip;
}
randomPos = randomPos - weight;
}
}
//如果所有权重都一样,就使用随机算法
randomPos = random.nextInt(weightList.size());
return (String) weightList.keySet().toArray()[randomPos];
} else {
return minActivityIps.get(0);
}
} else {
java.util.Random random = new java.util.Random();
int randomPos = random.nextInt(ServerIps.WEIGHT_LIST.size());
return (String) ServerIps.WEIGHT_LIST.keySet().toArray()[randomPos];
}
}
public static void main(String[] args) {
for (int i=0; i<10; i++){
System.out.println(getServer());
}
}
}

负载均衡总结:

  • 在这里插入图片描述
  • 参考资料:dubbo.apache.org/zh-cn/docs/…

本文转载自: 掘金

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

SpringBoot开发javaFX桌面程序-----环境配

发表于 2021-08-03

前言

最近在学习SpringBoot和javaFX,准备做个小项目练练手,本文章记录一下使用SpringBoot开发javaFX的环境配置。

准备

  • IDEA
  • JDK11以上(在JDK11里面将Java FX独立开了,所以要使用JDK11开发JavaFX应用就要将相关的库导入)
  • javaFX库:

gluonhq.com/products/ja…

1

  • JavaFX Scene Builder 2.0(可视化工具,加速JavaFX图形界面的开发,强烈建议安装)

JavaFX Scene Builder 2.0

1

步骤

1.创建SpringBoot应用

1

1

然后。。。默认就行

完成:

1

调整目录

这里我把src设为源文件夹,创建view和controller包

1

2.配置JavaFX Scene Builder 2.0

文件>设置>语言和框架>javaFX

填入你的安装路径:

1

3.Maven依赖引入

pom.xml

推荐一个查询最新Maven的网站:mvnrepository.com/

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
xml复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
<relativePath/>
</parent>
<groupId>com.rightstar</groupId>
<artifactId>javafx</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>javafx</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>12</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>de.roskenet</groupId>
<artifactId>springboot-javafx-support</artifactId>
<version>2.1.6</version>
</dependency>


<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>16-ea+5</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
<version>16-ea+5</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>16-ea+5</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>16-ea+5</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-web</artifactId>
<version>16-ea+5</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId>
<version>16-ea+5</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-media</artifactId>
<version>16-ea+5</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.1</version>
</dependency>



</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>


</plugins>
</build>

</project>

4.编写第一个界面

  • 使用SceneBuilder 编写InitScene.fxml

resource文件夹下创建InitScene.fxml文件,打开

1

  • 添加一个Pane和Button

1

ctrl+s保存

1

  • 编写绑定的InitSceneView类(继承AbstractFxmlView)

1

1
2
3
4
5
6
7
8
9
scala复制代码package com.rightstar.view;

import de.felixroske.jfxsupport.AbstractFxmlView;
import de.felixroske.jfxsupport.FXMLView;

@FXMLView("/InitScene.fxml")
public class InitSceneView extends AbstractFxmlView {

}
  • 编写启动类JavafxApplication(继承AbstractJavaFxApplicationSupport)

1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scala复制代码package com.rightstar;

import com.rightstar.view.InitSceneView;
import de.felixroske.jfxsupport.AbstractJavaFxApplicationSupport;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


@SpringBootApplication
public class JavafxApplication extends AbstractJavaFxApplicationSupport {

public static void main(String[] args) {

launch(JavafxApplication.class, InitSceneView.class,args);
}

}
  • 启动(重点需要配置VM)

1

–module-path换成你安装的javaFXSDK路径,(示例):

1
arduino复制代码--module-path "D:\javaTools\javafx-sdk-11.0.2\lib" --add-modules=javafx.controls,javafx.fxml
  • 启动

1

  • ok

1

后言

欢迎继续关注作者后面的博文更新,后面我将更新如何使用SpringBoot+javaFX编写一个简单的文件树显示程序

本文转载自: 掘金

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

【收藏】Java 8的 Stream 玩法大全

发表于 2021-08-03

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

前言

  Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。让程序员写出高效率、干净、简洁的代码。

什么是 Stream?

  Stream(流)是一个来自数据源的元素队列并支持聚合操作,流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。
Stream(流)的组成包含:元素、数据源、聚合操作、内部迭代、Pipelining等。

  • 元素:特定类型的对象
  • 数据源:流的来源,元素的集合,数组等。
  • 聚合操作:类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等。
  • 内部迭代:在流中内部迭代,与for外部处理不同。
  • Pipelining: 中间操作都会返回流对象本身。

Stream 基本操作

初始化两组数据

1
2
ini复制代码List<String> stringList = Arrays.asList("juejin", "zijie", "toutiao", "feidian", "join","", "juelimanman");
List<Integer> integerList = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7,8, 9, 10,11,12);

Stream 创建

stream()

  使用stream()创建串行流。

1
ini复制代码Stream<String> stream = stringList.stream();

parallelStream()

  parallelStream() 方法创建并行流

1
ini复制代码Stream<String> stringStream = stringList.parallelStream();

Arrays.stream()

  使用Arrays 中的 stream() 方法

1
arduino复制代码Stream<String> stream1 = Arrays.stream(new String[10]);

Stream.of

  Stream.of创建流,创建的是有限流

1
arduino复制代码Stream<String> streamOf = Stream.of("juejin", "zijie", "toutiao", "feidian", "join","", "juelimanman");

Stream.iterate

  Stream.iterate创建流,创建一个有序无限的数据流

1
ini复制代码Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 3).limit(5);

Stream.generate

  Stream.generate创建流,创建一个无限数据流

1
arduino复制代码Stream<Double> stream3 = Stream.generate(Math::random).limit(2);

reader.lines

  使用 BufferedReader.lines() 方法,将每行内容转成流

1
ini复制代码 Stream<String> lineStream = reader.lines();

Stream 操作

forEach

  forEach来迭代流中的每个数据,输出所有元素

1
arduino复制代码stringList.forEach(System.out::println);

map

  map 方法用于映射每个元素到对应的结果

1
c复制代码stringList.stream().map(i->i.equals("juejin"));

flatMap

  flatMap:将流中的每个值都换成另一个流,然后把所有流连接成一个流。

1
css复制代码stream.flatMap(String::toUpperCase).collect(Collectors.toList());

filter

  filter 方法用于通过设置的条件过滤出元素,过滤流中的某些元素

1
csharp复制代码stringList.stream().filter(i->i.equals("juejin"));

peek

   如同于map,能得到流中的每一个元素。但map接收的是一个Function表达式,有返回值;而peek接收的是Consumer表达式,没有返回值。

1
csharp复制代码stream.peek(s -> s.equals("juejin"));

limit

  limit(n) 方法用于获取指定数量的流。例如:limit(n):获取n个元素

1
scss复制代码integerList.stream().limit(3);

skip

  skip(n):跳过n元素,配合limit(n)可实现分页

1
scss复制代码integerList.stream().skip(5).limit(3);

distinct

  distinct:通过流中元素的 hashCode() 和 equals() 去除重复元素

1
scss复制代码integerList.stream().distinct().collect(Collectors.toList());

sorted

  sorted sorted 方法用于对流进行排序,自然排序。

1
scss复制代码integerList.stream().sorted();

sorted(Comparator com)

  sorted(Comparator com):定制排序,自定义Comparator排序器

1
less复制代码integerList.stream().sorted(Comparator.comparing(Integer::intValue));

parallel

  parallelStream 是流并行处理程序的代替方法。

1
scss复制代码integerList.parallelStream().sorted();

Collectors

  Collectors 类实现了很多归约操作,例如将流转换成集合和聚合元素。

1
less复制代码stringList.stream().filter(s -> s.equals("juejin")).collect(Collectors.toList());

count

  count 统计结果的收集器也非常有用,流的终止操作,用在流的最后。

1
scss复制代码integerList.stream().count();

allMatch

  allMatch:接收一个函数,当流中每个元素都符合该断言时才返回true,否则返回false,流的终止操作,用在流的最后

1
bash复制代码integerList.stream().allMatch(integer -> integer>0);

noneMatch

  noneMatch:接收一个函数,当流中每个元素都不符合该断言时才返回true,否则返回false,流的终止操作,用在流的最后。

1
bash复制代码integerList.stream().noneMatch(integer -> integer>100);

anyMatch

  anyMatch:接收一个函数,只要流中有一个元素满足该断言则返回true,否则返回false,流的终止操作,用在流的最后。

1
bash复制代码integerList.stream().anyMatch(integer -> integer>2);

findFirst

  findFirst:返回流中第一个元素,流的终止操作,用在流的最后。

1
scss复制代码stringList.stream().findFirst();

findAny

  findAny:返回流中的任意元素,流的终止操作,用在流的最后。

1
scss复制代码stringList.stream().findAny();

max

  max:返回流中元素最大值,流的终止操作,用在流的最后。

1
less复制代码userList.stream().max(Comparator.comparingInt(user::score));

min

  min:返回流中元素最小值,流的终止操作,用在流的最后。

1
less复制代码userList.stream().min(Comparator.comparingInt(user::score));

collect

  collect:接收一个Collector实例,将流中元素收集成另外一个数据结构。

1
scss复制代码integerList.stream().filter(u->user.getScore>60).collect(Collectors.toList());

总结

  Stream API 提供了一种高效且易于使用的处理数据的方式。让程序员写出高效率、干净、简洁的代码。在Stream流中都是使用复杂的表达式去处理数据逻辑,当然大家可以先了解这些方法,在项目中使用到的时候,再进行深入了解,进行更复杂的组合操作。

  作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

  好了,感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

本文转载自: 掘金

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

王者并发课-钻石2:分而治之-如何从原理深入理解ForkJo

发表于 2021-08-03

欢迎来到《王者并发课》,本文是该系列文章中的第25篇,砖石中的第2篇。

在上一篇文章中,我们学习了线程池ThreadPoolExecutor,它通过对任务队列和线程的有效管理实现了对并发任务的处理。然而,ThreadPoolExecutor有两个明显的缺点:一是无法对大任务进行拆分,对于某个任务只能由单线程执行;二是工作线程从队列中获取任务时存在竞争情况。这两个缺点都会影响任务的执行效率,要知道高并发场景中的每一毫秒都弥足珍贵。

针对这两个问题,本文即将介绍的ForkJoinPool给出了可选的答案。在本文中,我们将首先从分治算法开始介绍,接着体验ForkJoinPool中自定义任务的实现,最后再深入到Java中去理解ForkJoinPool的原理和用法。

本文大约2万字,篇幅较长,在阅读时建议先看目录再看内容或先收藏。

一、分治算法与Fork/Join模式

在并发计算中,Fork/Join模式往往用于对大任务的并行计算,它通过递归的方式对任务不断地拆解,再将结果进行合并。如果从其思想上看,Fork/Join并不复杂,其本质是分治算法(Divide-and-Conquer) 的应用。

分治算法的**基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。**分治算法的步骤如下:

  • (1)分解:将要解决的问题划分成若干规模较小的同类问题;
  • (2)求解:当子问题划分得足够小时,用较简单的方法解决;
  • (3)合并:按原问题的要求,将子问题的解逐层合并构成原问题的解。

Fork/Join对任务的拆分和对结果合并过程也是如此,可以用下面伪代码来表示:

1
2
3
4
5
6
7
8
9
10
11
java复制代码solve(problem):
if problem is small enough:
// 如果任务足够小,执行任务
solve problem directly (sequential algorithm)
else:
// 拆分任务
for part in subdivide(problem)
fork subtask to solve(part)
// 合并结果
join all subtasks spawned in previous loop
return combined results

所以,理解Fork/Join模型和ForkJoinPool线程池,首先要理解其背后的算法的目的和思想,因为后文所要详述的ForkJoinPool不过只是这种算法的一种的实现和应用。

二、Fork/Join应用场景与体验

按照王者并发课所提倡的思想->实现->源码的思路,在了解了Fork/Join思想之后,我们先通过一个场景手工实现一个RecursiveTask,这样可以更好地体验Fork/Join的用法。

场景:给定两个自然数,计算两个两个数之间的总和。比如1~n之间的和:1+2+3+…+n

为了解决这个问题,我们创建了TheKingRecursiveSumTask这个核心类,它继承于RecursiveTask. RecursiveTask是ForkJoinPool中的一种任务类型,你暂且不必深入了解它,后文会有详细描述。

TheKingRecursiveSumTask中定义了任务计算的起止范围(sumBegin和sumEnd)和拆分阈值(threshold),以及核心计算逻辑compute().

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
java复制代码public class TheKingRecursiveSumTask extends RecursiveTask<Long> {
private static final AtomicInteger taskCount = new AtomicInteger();
private final int sumBegin;
private final int sumEnd;
/**
* 任务拆分阈值,当任务尺寸大于该值时,进行拆分
*/
private final int threshold;

public TheKingRecursiveSumTask(int sumBegin, int sumEnd, int threshold) {
this.sumBegin = sumBegin;
this.sumEnd = sumEnd;
this.threshold = threshold;
}

@Override
protected Long compute() {
if ((sumEnd - sumBegin) > threshold) {
// 两个数之间的差值大于阈值,拆分任务
TheKingRecursiveSumTask subTask1 = new TheKingRecursiveSumTask(sumBegin, (sumBegin + sumEnd) / 2, threshold);
TheKingRecursiveSumTask subTask2 = new TheKingRecursiveSumTask((sumBegin + sumEnd) / 2, sumEnd, threshold);
subTask1.fork();
subTask2.fork();
taskCount.incrementAndGet();
return subTask1.join() + subTask2.join();
}
// 直接执行结果
long result = 0L;
for (int i = sumBegin; i < sumEnd; i++) {
result += i;
}
return result;
}

public static AtomicInteger getTaskCount() {
return taskCount;
}
}

在下面的代码中,我们设置的计算区间值0~10000000,当计算的个数超过100时,将对任务进行拆分,最大并发数设置为16.

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
java复制代码 public static void main(String[] args) {
int sumBegin = 0, sumEnd = 10000000;
computeByForkJoin(sumBegin, sumEnd);
computeBySingleThread(sumBegin, sumEnd);
}

private static void computeByForkJoin(int sumBegin, int sumEnd) {
ForkJoinPool forkJoinPool = new ForkJoinPool(16);
long forkJoinStartTime = System.nanoTime();
TheKingRecursiveSumTask theKingRecursiveSumTask = new TheKingRecursiveSumTask(sumBegin, sumEnd, 100);
long forkJoinResult = forkJoinPool.invoke(theKingRecursiveSumTask);
System.out.println("======");
System.out.println("ForkJoin任务拆分:" + TheKingRecursiveSumTask.getTaskCount());
System.out.println("ForkJoin计算结果:" + forkJoinResult);
System.out.println("ForkJoin计算耗时:" + (System.nanoTime() - forkJoinStartTime) / 1000000);
}

private static void computeBySingleThread(int sumBegin, int sumEnd) {
long computeResult = 0 L;
long startTime = System.nanoTime();
for (int i = sumBegin; i < sumEnd; i++) {
computeResult += i;
}
System.out.println("======");
System.out.println("单线程计算结果:" + computeResult);
System.out.println("单线程计算耗时:" + (System.nanoTime() - startTime) / 1000000);
}

运行结果如下:

1
2
3
4
5
6
7
8
9
shell复制代码======
ForkJoin任务拆分:131071
ForkJoin计算结果:49999995000000
ForkJoin计算耗时:207
======
单线程计算结果:49999995000000
单线程计算耗时:40

Process finished with exit code 0

从计算结果中可以看到,ForkJoinPool总共进行了131071次的任务拆分,最终的计算结果是49999995000000,耗时207毫秒。

不过,细心的你可能已经发现了,ForkJoin的并行计算的耗时竟然比单程程还慢?并且足足慢了近5倍!先别慌,关于ForkJoin的性能问题,我们会在后文有讲解。

三、ForkJoinPool设计与源码分析

在Java中,ForkJoinPool是Fork/Join模型的实现,于Java7引入并在Java8中广泛应用。ForkJoinPool允许其他线程向它提交任务,并根据设定将这些任务拆分为粒度更细的子任务,这些子任务将由ForkJoinPool内部的工作线程来并行执行,并且工作线程之间可以窃取彼此之间的任务。

在接口实现和继承关系上,ForkJoinPool和ThreadPoolExecutor类似,都实现了Executor和ExecutorService接口,并继承了AbstractExecutorService抽类。而在任务类型上,ForkJoinPool主要有两种任务类型:RecursiveAction和RecursiveTask,它们继承于ForkJoinTask. 相关关系如下图所示:

解读ForkJoinPool的源码并不容易,虽然它的思想较为简单,但在实现上要考虑的显然更多,加上部分代码可读性一般,所以讲解它的全部源码是不现实的,当然也是没必要的。在下文中,我们将主要介绍其核心的任务提交和执行相关的部分源码,其他源码有兴趣的可以自行阅读。

1. 构造ForkJoinPool的几种不同方式

ForkJoinPool中有四个核心参数,用于控制线程池的并行数、工作线程的创建、异常处理和模式指定等。各参数解释如下:

  • int parallelism:指定并行级别(parallelism level)。ForkJoinPool将根据这个设定,决定工作线程的数量。如果未设置的话,将使用Runtime.getRuntime().availableProcessors()来设置并行级别;
  • ForkJoinWorkerThreadFactory factory:ForkJoinPool在创建线程时,会通过factory来创建。注意,这里需要实现的是ForkJoinWorkerThreadFactory,而不是ThreadFactory. 如果你不指定factory,那么将由默认的DefaultForkJoinWorkerThreadFactory负责线程的创建工作;
  • UncaughtExceptionHandler handler:指定异常处理器,当任务在运行中出错时,将由设定的处理器处理;
  • boolean asyncMode:从名字上看,你可能会觉得它是异步模式设置,但其实是设置队列的工作模式:asyncMode ? FIFO_QUEUE : LIFO_QUEUE. 当asyncMode为true时,将使用先进先出队列,而为false时则使用后进先出的模式。

围绕上面的四个核心参数,ForkJoinPool提供了三种构造方式,使用时你可以根据需要选择其中的一种。

(1)方式一:默认无参构造

在该构造方式中,你无需设定任何参数。ForkJoinPool将根据当前处理器数量来设置并行数量,并使用默认的线程构造工厂。不推荐。

1
2
3
4
java复制代码 public ForkJoinPool() {
this(Math.min(MAX_CAP, Runtime.getRuntime().availableProcessors()),
defaultForkJoinWorkerThreadFactory, null, false);
}

(2)方式二:通过并行数构造

在该构造方式中,你可以指定并行数量,以更有效地平衡处理器数量和负载。建议在设置时,并行级别应低于当前处理器的数量。

1
2
3
java复制代码 public ForkJoinPool(int parallelism) {
this(parallelism, defaultForkJoinWorkerThreadFactory, null, false);
}

(2)方式三:自定义全部参数构造

以上两种构造方式都是基于这种构造,它允许你配置所有的核心参数。为了更有效地管理ForkJoinPool,建议你使用这种构造方式。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
boolean asyncMode) {
this(checkParallelism(parallelism),
checkFactory(factory),
handler,
asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
"ForkJoinPool-" + nextPoolId() + "-worker-");
checkPermission();
}

2. 按类型提交不同任务

任务提交是ForkJoinPool的核心能力之一,在提交任务时你有三种选择,如下面表格所示:

从非fork/join线程调用 从fork/join调用
提交异步执行 execute(ForkJoinTask) ForkJoinTask.fork()
等待并获取结果 invoke(ForkJoinTask) ForkJoinTask.invoke()
提交执行获取Future结果 submit(ForkJoinTask) ForkJoinTask.fork() (ForkJoinTasks are Futures)

(1)第一类核心方法:invoke

invoke类型的方法接受ForkJoinTask类型的任务,并在任务执行结束后,返回泛型结果。如果提交的任务是null,将抛出空指针异常。

1
2
3
4
5
6
java复制代码 public <T> T invoke(ForkJoinTask<T> task) {
if (task == null)
throw new NullPointerException();
externalPush(task);
return task.join();
}

(2)第二类核心方法:execute

execute类型的方法在提交任务后,不会返回结果。另外要注意的是,ForkJoinPool不仅允许提交ForkJoinTask类型任务,还允许提交Callable或Runnable任务,因此你可以像使用现有Executors一样使用ForkJoinPool。

当然,Callable或Runnable类型任务时,将会转换为ForkJoinTask类型,具体可以查看任务提交的相关源码。那么,这类任务和直接提交ForkJoinTask任务有什么区别呢?还是有的。区别在于,由于任务是不可切分的,所以这类任务无法获得任务拆分这方面的效益,不过仍然可以获得任务窃取带来的好处和性能提升。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码 public void execute(ForkJoinTask<?> task) {
if (task == null)
throw new NullPointerException();
externalPush(task);
}

public void execute(Runnable task) {
if (task == null)
throw new NullPointerException();
ForkJoinTask<?> job;
if (task instanceof ForkJoinTask<?>) // avoid re-wrap
job = (ForkJoinTask<?>) task;
else
job = new ForkJoinTask.RunnableExecuteAction(task);
externalPush(job);
}

(3)第三类核心方法:submit

submit类型的方法支持三种类型的任务提交:ForkJoinTask类型、Callable类型和Runnable类型。在提交任务后,将返回ForkJoinTask类型的结果。如果提交的任务是null,将抛出空指针异常,并且当任务不能按计划执行的话,将抛出任务拒绝异常。

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复制代码   public < T > ForkJoinTask < T > submit(ForkJoinTask < T > task) {
if (task == null)
throw new NullPointerException();
externalPush(task);
return task;
}

public < T > ForkJoinTask < T > submit(Callable < T > task) {
ForkJoinTask < T > job = new ForkJoinTask.AdaptedCallable < T > (task);
externalPush(job);
return job;
}

public < T > ForkJoinTask < T > submit(Runnable task, T result) {
ForkJoinTask < T > job = new ForkJoinTask.AdaptedRunnable < T > (task, result);
externalPush(job);
return job;
}

public ForkJoinTask < ? > submit(Runnable task) {
if (task == null)
throw new NullPointerException();
ForkJoinTask < ? > job;
if (task instanceof ForkJoinTask < ? > ) // avoid re-wrap
job = (ForkJoinTask < ? > ) task;
else
job = new ForkJoinTask.AdaptedRunnableAction(task);
externalPush(job);
return job;
}

3. ForkJoinTask

ForkJoinTask是ForkJoinPool的核心之一,它是任务的实际载体,定义了任务执行时的具体逻辑和拆分逻辑,本文前面的示例代码就是通过继承它实现。作为一个抽象类,ForkJoinTask的行为有点类似于线程,但它更为轻量,因为它不维护自己的运行时堆栈或程序计数器等。

在类的设计上,ForkJoinTask继承了Future接口,所以也可以将其看作是轻量级的Future,它们之间的关系如下图所示。

(1)fork与join

fork()/join()是ForkJoinTask甚至是ForkJoinPool的核心方法,承载着主要的任务协调作用,一个用于任务提交,一个用于结果获取。

fork-提交任务

fork()方法用于向当前任务所运行的线程池中提交任务,比如上文示例代码中的subTask1.fork(). 注意,不同于其他线程池的写法,任务提交由任务自己通过调用fork()完成,对此不要感觉诧异,fork()内部会将任务与当前线程进行关联。

从源码中看,如果当前线程是ForkJoinWorkerThread类型,将会放入该线程的任务队列,否则放入common线程池的任务队列中。关于common线程池,后续会有介绍。

1
2
3
4
5
6
7
8
java复制代码    public final ForkJoinTask<V> fork() {
Thread t;
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
((ForkJoinWorkerThread)t).workQueue.push(this);
else
ForkJoinPool.common.externalPush(this);
return this;
}

join-获取任务执行结果

前面,你已经知道可以通过fork()提交任务。那么现在,你则可以通过join()方法获取任务的执行结果。

调用join()时,将阻塞当前线程直到对应的子任务完成运行并返回结果。从源码看,join()的核心逻辑由doJoin()负责。doJoin()虽然很短,但可读性较差,阅读时稍微忍一下。

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
java复制代码public final V join() {
int s;
// 如果调用doJoin返回的非NORMAL状态,将报告异常
if ((s = doJoin() & DONE_MASK) != NORMAL)
reportException(s);
// 正常执行结束,返回原始结果
return getRawResult();
}

private int doJoin() {
int s;
Thread t;
ForkJoinWorkerThread wt;
ForkJoinPool.WorkQueue w;
//如果已完成,返回状态
return (s = status) < 0 ? s :
 //如果未完成且当前线程是ForkJoinWorkerThread,则从该线程中取出workQueue,并尝试将当前task取出执行。如果执行的结果是完成,则返回状态;否则,使用当前线程池awaitJoin方法进行等待
((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
(w = (wt = (ForkJoinWorkerThread) t).workQueue).
tryUnpush(this) && (s = doExec()) < 0 ? s :
wt.pool.awaitJoin(w, this, 0 L):
//当前线程非ForkJoinWorkerThread,调用externalAwaitDone方法等待
externalAwaitDone();
}

final int doExec() {
int s;
boolean completed;
if ((s = status) >= 0) {
try {
completed = exec();
} catch (Throwable rex) {
return setExceptionalCompletion(rex);
}
// 执行完成后,将状态设置为NORMAL
if (completed)
s = setCompletion(NORMAL);
}
return s;
}

(2)RecursiveAction与RecursiveTask

在ForkJoinPool中,常用的有两种任务类型:返回结果的和不返回结果的,这方面和ThreadPoolExecutor等线程池是一致的,对应的两个类分别是:RecursiveAction和RecursiveTask. 从类图中可以看到,它们均继承于ForkJoinTask.

RecursiveAction:无结果返回

RecursiveAction用于递归执行但不需要返回结果的任务,比如下面的排序就是它的典型应用场景。在使用RecursiveAction时,你需要继承并实现它的核心方法compute().

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
java复制代码static class SortTask extends RecursiveAction {
final long[] array;
final int lo, hi;
SortTask(long[] array, int lo, int hi) {
this.array = array;
this.lo = lo;
this.hi = hi;
}
SortTask(long[] array) {
this(array, 0, array.length);
}
// 核心计算方法
protected void compute() {
if (hi - lo < THRESHOLD)
// 直接执行
sortSequentially(lo, hi);
else {
// 拆分任务
int mid = (lo + hi) >>> 1;
invokeAll(new SortTask(array, lo, mid),
new SortTask(array, mid, hi));
merge(lo, mid, hi);
}
}
// implementation details follow:
static final int THRESHOLD = 1000;
void sortSequentially(int lo, int hi) {
Arrays.sort(array, lo, hi);
}
void merge(int lo, int mid, int hi) {
long[] buf = Arrays.copyOfRange(array, lo, mid);
for (int i = 0, j = lo, k = mid; i < buf.length; j++)
array[j] = (k == hi || buf[i] < array[k]) ?
buf[i++] : array[k++];
}
}

RecursiveTask:返回结果

RecursiveTask用于递归执行需要返回结果的任务,比如前面示例代码中的求和或下面这段求斐波拉契数列求和都是它的典型应用场景。在使用RecursiveTask时,你也需要继承并实现它的核心方法compute().

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码 class Fibonacci extends RecursiveTask<Integer> {
final int n;
Fibonacci(int n) { this.n = n; }
Integer compute() {
if (n <= 1)
return n;
Fibonacci f1 = new Fibonacci(n - 1);
f1.fork();
Fibonacci f2 = new Fibonacci(n - 2);
return f2.compute() + f1.join();
}
}

(3)ForkJoinTask使用限制

虽然在某些场景下,ForkJoinTask可以通过任务拆解的方式提高执行效率,但是需要注意的是它并非适合所有的场景。ForkJoinTask在使用时需要谨记一些限制,违背这些限制可能会适得其反甚至引来灾难。

为什么这么说呢?

这是因为,ForkJoinTask最适合用于纯粹的计算任务,也就是纯函数计算,计算过程中的对象都是独立的,对外部没有依赖。你可以想象,如果大量的任务或被拆分的子任务之间彼此依赖或对外部存在严重阻塞依赖,那将是怎样的画面…用千丝万缕来形容也不为过,外部依赖会带来任务执行和问题排查方面的双重不确定性。

所以,在理想情况下,提交到ForkJoinPool中的任务应避免执行阻塞I/O,以免出现不可控的意外情况。当然,这也并非是绝对的,在必要时你也可以定义和使用可阻塞的ForkJoinTask,只不过你需要付出更多的代价和考虑,使用时应当慎之又慎,本文对此不作叙述。

4. 工作队列与任务窃取

前面已经说到,ForkJoinPool与ThreadPoolExecutor有个很大的不同之处在于,ForkJoinPool存在引入了任务窃取设计,它是其性能保证的关键之一。

关于任务窃取,简单来说,就是允许空闲线程从繁忙线程的双端队列中窃取任务。默认情况下,工作线程从它自己的双端队列的头部获取任务。但是,当自己的任务为空时,线程会从其他繁忙线程双端队列的尾部中获取任务。这种方法,最大限度地减少了线程竞争任务的可能性。

ForkJoinPool的大部分操作都发生在工作窃取队列(work-stealing queues ) 中,该队列由内部类WorkQueue实现。其实,这个队列也不是什么神奇之物,它是Deques的特殊形式,但仅支持三种操作方式:push、pop和poll(也称为窃取)。当然,在ForkJoinPool中,队列的读取有着严格的约束,push和pop仅能从其所属线程调用,而poll则可以从其他线程调用。换句话说,前两个方法是留给自己用的,而第三种方法则是为了方便别人来窃取任务用的。任务窃取的相关过程,可以用下面这幅图来表示,这幅图建议你收藏:

看到这里,不知你是否会有疑问:为什么工作线程总是从自己的头部获取任务?为什么要这样设计?首先处理队列中等待时间较长的任务难道不是更有意义吗?

答案当然不会是“更有意义”。这样做的主要原因是为了提高性能,通过始终选择最近提交的任务,可以增加资源仍分配在CPU缓存中的机会,这样CPU处理起来要快一些。而窃取者之所以从尾部获取任务,则是为了降低线程之间的竞争可能,毕竟大家都从一个部分拿任务,竞争的可能要大很多。

此外,这样的设计还有一种考虑。由于任务是可分割的,那队列中较旧的任务最有可能粒度较大,因为它们可能还没有被分割,而空闲的线程则相对更有“精力”来完成这些粒度较大的任务。

5. ForkJoinPool监控

对于一个复杂框架来说,实时地了解ForkJoinPool的内部状态是十分必要的。因此,ForkJoinPool提供了一些常用方法。通过这些方法,你可以了解当前的工作线程、任务处理等情况。

(1)获取运行状态的线程总数

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public int getRunningThreadCount() {
int rc = 0;
WorkQueue[] ws;
WorkQueue w;
if ((ws = workQueues) != null) {
for (int i = 1; i < ws.length; i += 2) {
if ((w = ws[i]) != null && w.isApparentlyUnblocked())
++rc;
}
}
return rc;
}

(2)获取活跃线程数量

1
2
3
4
5
java复制代码
public int getActiveThreadCount() {
int r = (config & SMASK) + (int)(ctl >> AC_SHIFT);
return (r <= 0) ? 0 : r; // suppress momentarily negative values
}

(3)判断ForkJoinPool是否空闲

1
2
3
java复制代码public boolean isQuiescent() {
return (config & SMASK) + (int)(ctl >> AC_SHIFT) <= 0;
}

(4)获取任务窃取数量

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public long getStealCount() {
AtomicLong sc = stealCounter;
long count = (sc == null) ? 0 L : sc.get();
WorkQueue[] ws;
WorkQueue w;
if ((ws = workQueues) != null) {
for (int i = 1; i < ws.length; i += 2) {
if ((w = ws[i]) != null)
count += w.nsteals;
}
}
return count;
}

(5)获取队列中的任务数量

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public long getQueuedTaskCount() {
long count = 0;
WorkQueue[] ws;
WorkQueue w;
if ((ws = workQueues) != null) {
for (int i = 1; i < ws.length; i += 2) {
if ((w = ws[i]) != null)
count += w.queueSize();
}
}
return count;
}

(6)获取已提交的任务数量

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public int getQueuedSubmissionCount() {
int count = 0;
WorkQueue[] ws;
WorkQueue w;
if ((ws = workQueues) != null) {
for (int i = 0; i < ws.length; i += 2) {
if ((w = ws[i]) != null)
count += w.queueSize();
}
}
return count;
}

四、警惕ForkJoinPool#commonPool

在上文中所示的源码中,你可能已经在多处注意到commonPool的存在。在ForkJoinPool中,commonPool是一个共享的、静态的线程池,并且在实际使用时才会进行懒加载,Java8中的CompletableFuture和并行流(Parallel Streams)用的就是它。不过,使用CompletableFuture时你可以指定自己的线程池,但是并行流在使用时却不可以,这也是我们要警惕的地方。为什么这么说呢?

ForkJoinPool中的commonPool设计初衷是为了降低线程池的重复创建,让一些任务共用同一个线程池,毕竟创建线程池和创建线程都是昂贵的。然而,凡事都有两面性,commonPool在某些场景下确实可以达到线程池复用的目的,但是,如果你决定与别人分享自己空间,那么当你想使用它的时候,它可能不再完全属于你。也就是说,当你想用commonPool时,它可能已经其他任务填满了。

提交到ForkJoinPool中的任务一般有两类:计算类型和阻塞类型。考虑一个场景,应用中多处都在使用这个共享线程池,有人在某处做了个不当操作,比如往池子里丢入了阻塞型任务,那么结果会怎样?结果当然是,整个线程池都有可能被阻塞!如此,整个应用都面临着被拖垮的风险。看到这里,对于Java8中的并行流的使用,你就应该高度警惕了。

那怎么避免这种情况发生呢?答案是尽量避免使用commonPool,并且在需要运行阻塞任务时,应当创建独立的线程池,和系统的其他部分保持隔离,以免风险扩散。

五、ForkJoinPool性能评估

为了测试ForkJoinPool的性能,我做了一组简单的、非正式实验。实验分三组进行,为了尽可能让每组的数据客观,每组实验均运行5次,取最后的平均数。

  • 实验代码:本文第一部分的示例代码;
  • 实验环境:Mac;
  • JDK版本:8;
  • 任务分隔阈值:100

实验结果如下方表格所示:

实验次数 1000量级耗时(毫秒) 1000000量级耗时(毫秒) 1000000000量级耗时(毫秒)
Fork/Join 单线程 Fork/Join 单线程 Fork/Join 单线程
1 4 0 34 5 1157 313
2 3 0 34 6 848 344
3 5 0 16 9 1069 325
4 4 0 35 8 955 307
5 5 0 30 22 922 385
平均 4.2 0 29.8 10 990.2 334.8

从实验结果(0表示不到1毫秒)来看,ForkJoinPool的性能竟然不如单线程的效率高!这样的结果,似乎很惊喜、很意外…然而,为什么会这样?

不要惊讶,之所以会出现这个令你匪夷所思的结果,其原因在于任务拆分的粒度过小!在上面的测试中,任务拆分阈值仅为100,导致Fork/Join在计算时出现大量的任务拆分动作,也就是任务分的太细,大量的任务拆分和管理也是需要额外成本的。

以0~1000000求和为例,当把阈值从100调整为100000时,其结果结果如下。可以看到,Fork/Join的优势就体现出来了。

1
2
3
4
5
6
7
shell复制代码======
ForkJoin任务拆分:16383
ForkJoin计算结果:499999999500000000
ForkJoin计算耗时:143
======
单线程计算结果:499999999500000000
单线程计算耗时:410

那么,问题又来了,哪些因素会影响Fork/Join的性能呢?

根据经验和实验,任务总数、单任务执行耗时以及并行数都会影响到性能。所以,当你使用Fork/Join框架时,你需要谨慎评估这三个指标,最好能通过模拟对比评估,不要凭感觉冒然在生产环境使用。

小结

以上就是关于ForkJoinPool的全部内容。Fork/Join是一种基于分治算法的模型,在并发处理计算型任务时有着显著的优势。其效率的提升主要得益于两个方面:

  • 任务切分:将大的任务分割成更小粒度的小任务,让更多的线程参与执行;
  • 任务窃取:通过任务窃取,充分地利用空闲线程,并减少竞争。

在使用ForkJoinPool时,需要特别注意任务的类型是否为纯函数计算类型,也就是这些任务不应该关心状态或者外界的变化,这样才是最安全的做法。如果是阻塞类型任务,那么你需要谨慎评估技术方案。虽然ForkJoinPool也能处理阻塞类型任务,但可能会带来复杂的管理成本。

而在性能方面,要认识到Fork/Join的性能并不是开箱即来,而是需要你去评估和验证一些重要指标,通过数据对比得出最佳结论。

此外,ForkJoinPool虽然提供了commonPool,但出于潜在的风险考虑,不推荐使用或谨慎使用。

夫子的试炼

  • 动手:使用ForkJoinPool实现List数组排序。

延伸阅读与参考资料

  • 《王者并发课》大纲与更新进度总览:juejin.cn/post/696727…
  • A Java Fork/Join Framework(Doug Lea):gee.cs.oswego.edu/dl/papers/f…
  • Java Fork and Join using ForkJoinPool:tutorials.jenkov.com/java-util-c…
  • Introduction to the Fork/Join Framework:www.pluralsight.com/guides/intr…
  • The Unfairly Unknown ForkJoinPool:medium.com/swlh/the-un…
  • A Java? Fork-Join Calamity:coopsoft.com/ar/Calamity…

关于作者

从业近十年,先后从事敏捷与DevOps咨询、Tech Leader和管理等工作,对分布式高并发架构有丰富的实战经验。热衷于技术分享和特定领域书籍翻译,掘金小册《高并发秒杀的设计精要与实现》作者。


关注公众号【MetaThoughts】,及时获取文章更新和文稿。

如果本文对你有帮助,欢迎点赞、关注、监督,我们一起从青铜到王者。

本文转载自: 掘金

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

MySQL进阶系列: 一文详解explain各字段含义

发表于 2021-08-03

explain有何用处呢:为了知道优化SQL语句的执行,需要查看SQL语句的具体执行过程,以加快SQL语句的执行效率。

可以使用explain+SQL语句来模拟优化器执行SQL查询语句,从而知道mysql是如何处理sql语句的。通过查看执行计划了解执行器是否按照我们想的那样处理SQL。

explain执行计划中包含的信息如下:

  • id: 查询序列号
  • select_type: 查询类型
  • table: 表名或者别名
  • partitions: 匹配的分区
  • type: 访问类型
  • possible_keys: 可能用到的索引
  • key: 实际用到的索引
  • key_len: 索引长度
  • ref: 与索引比较的列
  • rows: 估算的行数
  • filtered: 按表条件筛选的行百分比
  • Extra: 额外信息

下面说下具体每一列的表示的含义和对应sql.

测试使用mysql版本5.7, 使用的3个表结构如下

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
sql复制代码CREATE TABLE `demo`.`emp`  (
`emp_id` bigint(20) NOT NULL,
`name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '姓名',
`empno` int(20) NOT NULL COMMENT '工号',
`deptno` int(20) NOT NULL COMMENT '部门编号',
`sal` int(11) NOT NULL DEFAULT 0 COMMENT '销售量',
PRIMARY KEY (`emp_id`) USING BTREE,
INDEX `u1`(`deptno`) USING BTREE,
UNIQUE INDEX `u2`(`empno`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

CREATE TABLE `demo`.`dept` (
`id` bigint(20) NOT NULL,
`deptno` int(20) NOT NULL COMMENT '部门编码',
`dname` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '部门名称',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `dept_u1`(`deptno`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;


CREATE TABLE `demo`.`salgrade` (
`id` bigint(20) NOT NULL,
`losal` int(20) NULL DEFAULT NULL,
`hisal` int(20) NULL DEFAULT NULL,
`emp_id` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

一 id列

select查询的序列号(一组数字),表示查询中执行select子句或者操作表的顺序。

id列分为三种情况:

1、如果id相同,那么执行顺序从上到下

1
csharp复制代码mysql> explain select * from emp e join dept d on e.deptno = d.deptno join salgrade sg on e.sal between sg.losal and sg.hisal;

2、如果id不同,如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行\

1
csharp复制代码mysql> explain select * from emp e where e.deptno = (select d.deptno from dept d where d.dname = 'SALES');

3、id相同和不同的,同时存在:相同的可以认为是一组,从上往下顺序执行,在所有组中,id值越大,优先级越高,越先执行

1
csharp复制代码mysql> explain select * from emp e join dept d on e.deptno = d.deptno join salgrade sg on e.sal between sg.losal and sg.hisal wheree.deptno = (select d.deptno from dept d where d.dname = 'SALES');

二,select_type列

1
复制代码主要用来分辨查询的类型,是普通查询还是联合查询还是子查询
  1. sample: 简单的查询,不包含子查询和union
1
csharp复制代码mysql> explain select * from emp;

  1. primary: 查询中若包含任何复杂的子查询,最外层查询则被标记为Primary
1
csharp复制代码mysql> explain select * from emp e where e.deptno = (select d.deptno from dept d where d.dname = 'SALES');

  1. union: 在union,union all和子查询中的第二个和随后的select被标记为union
1
sql复制代码mysql> explain select * from emp where deptno = 10 union select * from emp where sal >2000;

  1. dependent union: 在包含UNION或者UNION ALL的大查询中,如果各个小查询都依赖于外层查询的话,那除了最左边的那个小查询之外,其余的小查询的select_type的值就是DEPENDENT UNION。
1
sql复制代码mysql> explain select * from emp e where e.empno  in ( select empno from emp where deptno = 10 union select empno from emp where sal >2000)

  1. union result: 从union表获取结果的select。
1
sql复制代码mysql> explain select * from emp where deptno = 10 union select * from emp where sal >2000;

  1. subquery: 在select或者where列表中包含子查询(不在from子句中)
1
csharp复制代码mysql> explain select * from emp where sal > (select avg(sal) from emp) ;

  1. dependent subquery: 子查询中的第一个select(不在from子句中),而且取决于外面的查询。
1
ini复制代码mysql> explain select e1.* from emp e1 WHERE e1.deptno = (SELECT deptno FROM emp e2 WHERE e1.empno = e2.empno);

  1. derived: ****在FROM列表中包含的子查询被标记为DERIVED,也叫做派生类
1
csharp复制代码mysql> explain select * from ( select emp_id,count(*) from emp group by emp_id ) e;

  1. UNCACHEABLE SUBQUERY:一个子查询的结果不能被缓存,必须重新评估外链接的第一行对于外层的主表,子查询不可被物化,每次都需要计算(耗时操作)
1
less复制代码mysql> explain select * from emp where empno = (select empno from emp where deptno=@@sort_buffer_size);

  1. uncacheable union: 表示union的查询结果不能被缓存:没找到具体的sql语句验证.

三,table列

对应行正在访问哪一个表,表名或者别名,可能是临时表或者union合并结果集.

1、如果是具体的表名,则表明从实际的物理表中获取数据,当然也可以是表的别名.


2、表名是derivedN的形式,表示使用了id为N的查询产生的衍生表.


3、当有union result的时候,表名是union n1,n2等的形式,n1,n2表示参与union的id.

四,type列

type显示的是访问类型,访问类型表示我是以何种方式去访问我们的数据,最容易想的是全表扫描,直接暴力的遍历一张表去寻找需要的数据,效率非常低下。

访问的类型有很多,效率从最好到最坏依次是:

system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

一般情况下,要保证查询至少达到range级别,最好能达到ref

1. **all**: 全表扫描,需要扫描整张表,从头到尾找到需要的数据行。一般情况下出现这样的sql语句而且数据量比较大的话那么就需要进行优化。
1
csharp复制代码mysql> explain select * from emp;

2. **index**:全索引扫描这个比all的效率要好,主要有两种情况,一种是当前的查询时覆盖索引,即我们需要的数据在索引中就可以索取,或者是使用了索引进行排序,这样就避免数据的重排序
1
csharp复制代码mysql> explain  select empno from emp;

3. **range**:表示利用索引查询的时候限制了范围,在指定范围内进行查询,这样避免了index的全索引扫描,适用的操作符:=, <>, >, >=, <, <=, IS NULL, BETWEEN, LIKE, or IN() 
1
sql复制代码mysql> explain select * from emp where empno between 100 and 200;

4. **index\_subquery**:利用索引来关联子查询,不再扫描全表
1
csharp复制代码mysql> explain select * from emp where deptno not in (select deptno from emp)


但是大多数情况下使用SELECT子查询时,MySQL查询优化器会自动将子查询优化为联表查询,因此 type 不会显示为 index_subquery,而是ref

5. **unique\_subquery**: 该连接类型类似于index\_subquery,使用的是唯一索引
1
csharp复制代码mysql> explain SELECT * from emp where emp_id not in (select emp.emp_id from emp );


大多数情况下使用SELECT子查询时,MySQL查询优化器会自动将子查询优化为联表查询,因此 type 不会显示为 index_subquery,而是eq_ref

6. **index\_merge**:在查询过程中需要多个索引组合使用.

mysql> 没有模拟出来

7. **ref\_or\_null**:对于某个字段即需要关联条件,也需要null值的情况下,查询优化器会选择这种访问方式.

mysql> 没模拟出来

8. **ref**:使用了非唯一性索引进行数据的查找
1
csharp复制代码mysql> explain select * from emp where  deptno=10;

9. **eq\_ref** :当进行等值联表查询使用主键索引或者唯一性非空索引进行数据查找(实际上唯一索引等值查询type不是eq\_ref而是const)
1
sql复制代码mysql> explain select * from salgrade s LEFT JOIN emp e on s.emp_id = e.emp_id;

10. **const**:最多只能匹配到一条数据,通常使用主键或唯一索引进行等值条件查询
1
csharp复制代码mysql> explain select * from emp where empno = 10;

11. **system**:表只有一行记录(等于系统表),这是const类型的特例,平时不会出现,不需要进行磁盘io
1
go复制代码mysql> explain SELECT * FROM `mysql`.`proxies_priv`;

五,possible_keys列

显示可能应用在这张表中的索引,一个或多个,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用。

六,key列

实际使用的索引,如果为null,则没有使用索引,查询中若使用了覆盖索引,则该索引和查询的select字段重叠。

七,key_len列

表示索引中使用的字节数,可以通过key_len计算查询中使用的索引长度,在不损失精度的情况下长度越短越好。

索引越大占用存储空间越大,这样io的次数和量就会增加,影响执行效率

八,ref列

显示之前的表在key列记录的索引中查找值所用的列或者常量

九,rows列

根据表的统计信息及索引使用情况,大致估算出找出所需记录需要读取的行数,此参数很重要,直接反应的sql找了多少数据,在完成目的的情况下越少越好。

十,filtered列

针对表中符合某个条件(where子句或者联接条件)的记录数的百分比所做的一个悲观估算。

十一,extra列

包含额外的信息。

1. **using filesort**: 说明mysql无法利用索引进行排序,只能利用排序算法进行排序,会消耗额外的位置
1
vbnet复制代码mysql> explain select * from emp order by sal;

2. **using temporary**: 建立临时表来保存中间结果,查询完成之后把临时表删除
1
csharp复制代码mysql> explain select name,count(*) from emp where deptno = 10 group by name;

3. **using index**: 这个表示当前的查询是覆盖索引的,直接从索引中读取数据,而不用访问数据表。如果同时出现using where 表名索引被用来执行索引键值的查找,如果没有,表面索引被用来读取数据,而不是真的查找
1
csharp复制代码mysql> explain select deptno,count(*) from emp group by deptno limit 10;

4. **using where**: 使用where进行条件过滤
1
csharp复制代码mysql> explain select * from emp where name = 1;
5. **using join buffer**: 使用连接缓存
1
csharp复制代码mysql> explain select * from emp e left join dept d on e.deptno = d.deptno;

6. **impossible where**:where语句的结果总是false
1
csharp复制代码mysql> explain select * from emp where 1=0;

文中有问题的可以给我留言,部分没有模拟出来的也可以给意见

参考《高性能MySQL》

mysql进阶系列历史回顾:

1. 基础架构篇

2. 存储引擎

3. MyISAM和InnoDB有什么区别篇

4. 表设计如何更好的选择数据类型

5. 数据库设计中的范式究竟该如何使用

本文转载自: 掘金

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

服务调用Ribbon、OpenFeign Ribbon 一、

发表于 2021-08-03

Ribbon

一、概述

Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。

简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。

官方文档:github.com/Netflix/rib…

LB负载均衡(Load Balance)是什么:
简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用),常见的负载均衡有软件Nginx,LVS,硬件 F5等。

Ribbon本地负载均衡客户端(进程内LB) VS Nginx服务端负载均衡区别(集中式LB):
Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是由服务端实现的。

Ribbon本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。

Ribbon=负载均衡+RestTemplate调用

二、Ribbon负载均衡演示

1、架构说明

image.png

Ribbon在工作时分成两步:第一步先选择 EurekaServer ,它优先选择在同一个区域内负载较少的server。

第二步再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权。

总结:Ribbon其实就是一个软负载均衡的客户端组件,他可以和其他所需请求的客户端结合使用,和eureka结合只是其中的一个实例。

2、POM

spring-cloud-starter-netflix-eureka-client自带了spring-cloud-starter-ribbon引用。

3、二说RestTemplate的使用

(1)官网

docs.spring.io/spring-fram…

(2)getForObject方法/getForEntity方法

返回对象为响应体中数据转化成的对象,基本上可以理解为Json。

返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等。

1
2
3
4
5
6
7
8
9
java复制代码@GetMapping("/consumer/payment/getForEntity/{id}")
public CommonResult<Payment> getPayment2(@PathVariable("id") Long id){
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);
if(entity.getStatusCode().is2xxSuccessful()){
return entity.getBody();
}else{
return new CommonResult<>(444,"操作失败");
}
}

(3)postForObject/postForEntity

1
2
3
4
5
java复制代码@GetMapping("/consumer/payment/create")
public CommonResult<Payment> create(Payment payment){
return restTemplate.postForObject(PAYMENT_URL+"/payment/create",payment,CommonResult.class);
//return restTemplate.postForEntity(PAYMENT_URL+"/payment/create",payment,CommonResult.class).getBody();
}

三、Ribbon核心组件IRule

1、IRule

根据特定算法中从服务列表中选取一个要访问的服务:

com.netflix.loadbalancer.RoundRobinRule:轮询

com.netflix.loadbalancer.RandomRule:随机

com.netflix.loadbalancer.RetryRule:先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务

WeightedResponseTimeRule:对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择

BestAvailableRule:会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务

AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例

ZoneAvoidanceRule:默认规则,复合判断server所在区域的性能和server的可用性选择服务器

2、如何替换

(1)修改cloud-consumer-order80

(2)注意配置细节

官方文档明确给出了警告:

这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。

(3)新建package

com.atguigu.myrule

(4)新建MySelfRule规则类

1
2
3
4
5
6
7
java复制代码@Configuration
public class MySelfRule{
@Bean
public IRule myRule(){
return new RandomRule();//定义为随机,默认是轮询
}
}

(5)主启动类添加@RibbonClient

1
java复制代码@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration=MySelfRule.class)

(6)测试

http://localhost/consumer/payment/get/31

四、Ribbon负载均衡算法

1、原理

负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标 ,每次服务重启动后rest接口计数从1开始。

1
java复制代码List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");

如: List [0] instances = 127.0.0.1:8002
   List [1] instances = 127.0.0.1:8001

8001+ 8002 组合成为集群,它们共计2台机器,集群总数为2, 按照轮询算法原理:

当总请求数为1时: 1 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8001
当总请求数位2时: 2 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8002
当总请求数位3时: 3 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8001
当总请求数位4时: 4 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8002
如此类推……

2、RoundRobinRule源码

3、手写本地负载均衡器

(1)7001/7002集群启动

(2)8001/8002微服务改造

controller

1
2
3
4
java复制代码@GetMapping(value = "/payment/lb")
public String getPaymentLB(){
return serverPort;
}

(3)80订单微服务改造

①ApplicationContextBean去掉注解@LoadBalanced

②LoadBalancer接口

1
2
3
java复制代码public interface LoadBalancer{
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}

③MyLB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Component
public class MyLB implements LoadBalancer{
private AtomicInteger atomicInteger = new AtomicInteger(0);
public final int getAndIncrement(){
int current;
int next;
do{
current = this.atomicInteger.get();
next = current >= 2147483647 ? 0 : current + 1;
} while(!this.atomicInteger.compareAndSet(current, next));
System.out.println("*****next: "+next);
return next;
}
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances){
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
}

④OrderController

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
java复制代码@RestController
public class OrderController{
//public static final String PAYMENT_SRV = "http://localhost:8001";
public static final String PAYMENT_SRV = "http://CLOUD-PAYMENT-SERVICE";

@Resource
private RestTemplate restTemplate;
//可以获取注册中心上的服务列表
@Resource
private DiscoveryClient discoveryClient;
@Resource
private LoadBalancer loadBalancer;

@GetMapping("/consumer/payment/create")
public CommonResult<Payment> create(Payment payment){
return restTemplate.postForObject(PAYMENT_SRV+"/payment/create",payment,CommonResult.class);
}

@GetMapping("/consumer/payment/get/{id}")
public CommonResult<Payment> getPayment(@PathVariable("id") Long id){
return restTemplate.getForObject(PAYMENT_SRV+"/payment/get/"+id,CommonResult.class);
}

@GetMapping("/consumer/payment/getForEntity/{id}")
public CommonResult<Payment> getPayment2(@PathVariable("id") Long id){
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_SRV+"/payment/get/"+id, CommonResult.class);
if(entity.getStatusCode().is2xxSuccessful()){
return entity.getBody();
}else{
return new CommonResult(444, "操作失败");
}
}

@Resource
private LoadBalancer loadBalancer;

@GetMapping("/consumer/payment/lb")
public String getPaymentLB(){
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if(instances == null || instances.size()<=0) {
return null;
}
ServiceInstance serviceInstance = loadBalancer.instances(instances);
URI uri = serviceInstance.getUri();
return restTemplate.getForObject(uri+"/payment/lb",String.class);
}
}

⑤测试

http://localhost/consumer/payment/lb

OpenFeign

一、概述

1、OpenFeign是什么

Feign是一个声明式的Web服务客户端,让编写Web服务客户端变得非常容易,只需创建一个接口并在接口上添加注解即可。

GitHub:github.com/spring-clou…

2、能干嘛

Feign能干什么

Feign旨在使编写Java Http客户端变得更容易。前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon时,自动封装服务调用客户端的开发量。

Feign集成了Ribbon

利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。

3、Feign和OpenFeign两者区别

Feign OpenFeign
Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端。Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务 OpenFeign是Spring Cloud 在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务
org.springframework.cloud spring-cloud-starter-feign org.springframework.cloud spring-cloud-starter-openfeign

二、OpenFeign使用步骤

接口+注解:微服务调用接口+@FeignClient

1、新建cloud-consumer-feign-order80

Feign在消费端使用

2、POM

1
2
3
4
5
6
7
8
9
10
java复制代码<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

3、YML

1
2
3
4
5
6
7
8
yaml复制代码server:
port: 80

eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/

4、主启动

1
2
3
4
5
6
7
java复制代码@SpringBootApplication
@EnableFeignClients//开启
public class OrderFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class,args);
}
}

5、业务类

业务逻辑接口+@FeignClient配置调用provider服务

(1)新建PaymentFeignService接口并新增注解@FeignClient

1
2
3
4
5
6
java复制代码@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")//使用
public interface PaymentFeignService {
@GetMapping(value = "/payment/get/{id}")
CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}

(2)控制层Controller

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Slf4j
@RestController
public class OrderFeignController {
@Resource
private PaymentFeignService paymentFeignService;

@GetMapping(value = "/consumer/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
return paymentFeignService.getPaymentById(id);
}
}

6、测试

先启动2个eureka集群7001/7002,再启动2个微服务8001/8002,启动OpenFeign启动,http://localhost/consumer/payment/get/31

Feign自带负载均衡配置项

三、OpenFeign超时控制

1、超时设置,故意设置超时演示出错情况

(1)服务提供方8001故意写暂停程序

1
2
3
4
5
6
7
java复制代码@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeOut(){
System.out.println("*****paymentFeignTimeOut from port: "+serverPort);
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
return serverPort;
}

(2)服务消费方80添加超时方法PaymentFeignService

1
2
java复制代码@GetMapping(value = "/payment/feign/timeout")
String paymentFeignTimeOut();

(3)服务消费方80添加超时方法OrderFeignController

1
2
3
4
5
java复制代码@GetMapping(value = "/consumer/payment/feign/timeout")
public String paymentFeignTimeOut(){
//openfeign-ribbon,客户端一般默认等待一秒钟
return paymentFeignService.paymentFeignTimeOut();
}

(4)测试

http://localhost/consumer/payment/feign/timeout

OpenFeign默认等待1秒钟,超过后报错

2、OpenFeign默认支持Ribbon

默认Feign客户端只等待一秒钟,但是服务端处理需要超过1秒钟,导致Feign客户端不想等待了,直接返回报错。为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制。

3、YML文件里需要开启OpenFeign客户端超时控制

1
2
3
4
5
6
yaml复制代码#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
#指的是建立连接后从服务器读取到可用资源所用的时间
ReadTimeout: 5000
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ConnectTimeout: 5000

四、OpenFeign日志打印功能

1、是什么

Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节。说白了就是对Feign接口的调用情况进行监控和输出。

2、日志级别

NONE:默认的,不显示任何日志;

BASIC:仅记录请求方法、URL、响应状态码及执行时间;

HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息;

FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。

3、配置日志bean

1
2
3
4
5
6
7
java复制代码@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}

4、YML文件里需要开启日志的Feign客户端

1
2
3
4
yaml复制代码logging:
level:
# feign日志以什么级别监控哪个接口
com.atguigu.springcloud.service.PaymentFeignService: debug

5、后台日志查看

本文转载自: 掘金

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

【细说软件工程】《软件工程》Software Enginee

发表于 2021-08-03

《软件工程》60’

一.、软件过程

1、软件过程的概念

答:

1)**软件过程描述为为了开发出客户需要的软件,什么人、在什么时候、做什么事以及怎么做这些事以实现某一种的具体目标。**ISO9000把过程定义为:“使用资源将输入转化为输出的活动所构成的系统”。(《软件工程导论》p14)

2)过程定义了运用方法的顺序、应该交付的文档资料、为保证软件质量和协调变化所需要采取的管理措施以及标志软件开发各个阶段任务完成的里程碑。(《软件工程导论》p14)

3)软件过程是软件生成周期中的一系列相关的过程。过程是活动的集合,活动是任务的集合。(《复旦大学软工PPT》)

软件过程有三层含义:

个体含义:

即指软件产品或系统在生存周期中的某一类活动的集合,如软件开发过程,软件管理过程等;

整体含义:

即指软件产品或系统在所有上述含义下的软件过程的总体;

工程含义:

即指解决软件过程的工程,它应用软件工程的原则、方法来构造软件过程模型,并结合软件产品的具体要求进行实例化,以及用户环境下的运作,以此进一步提高软件生产率,降低成本。

2、经典软件过程模型特点(瀑布模型、增量模型、演化模型、统一过程模型)

答:

瀑布模型

  • 瀑布模型将软件生命周期划分为需求分析、规格说明、设计、程序编写、软件测试和运行维护等六个基本活动,并且规定了它们自上而下、相互衔接的固定次序,如同瀑布流水,逐级下落。
  • 瀑布模型强调文档的作用,并要求每个阶段都要仔细验证。
  • 瀑布模型模型的线性过程太理想化,已不再适合现代的软件开发模式,几乎被业界抛弃,主要问题是:

1.各个阶段的划分完全固定,阶段之间生成大量的文档,极大地增加了工作量;

2.由于开发模型是线性的,用户只有等到整个过程的末期才能见到开发成果,从而增加了开发的风险;

3.早期的错误可能要等到开发后期的测试阶段才能发现,进而带来严重的后果。

增量模型

与建造大厦相同,软件也是一步一步建造起来的。

在增量模型中,软件被作为一系列的增量构件来设计、实现、集成和测试,每一个构件是由多种相互作用的模块所形成的提供特定功能的代码片段构成。

  • 增量模型在各个阶段并不交付一个可运行的完整产品,而是满足客户需求的一个子集的可运行产品。
  • 增量模型侧重于每个增量都提交一个可以运行的产品。
  • 整个产品被分解成若干个构件,开发人员逐个构件地交付产品,这样做的好处是软件开发可以较好地适应变化,客户可以不断地看到所开发的软件,从而降低开发风险。

image-20201124144633852

但是,增量模型也存在以下缺陷:

1.由于各个构件是逐渐并入已有的软件体系结构中的,所以加入构件必须不破坏已构造好的系统部分,还需要软件具备开放式的体系结构。

2.在开发过程中,需求的变化是不可避免的。增量模型的灵活性可以使其事情这种变化的能力大大优于瀑布模型和快速原型模型,但是也很容易退化为边做边改模型,从而使软件过程的控制失去整体性。

在使用增量模型时,第一个增量往往是实现基本需求的核心产品。核心产品交付用户使用后,经过评价形成下一个增量的开发计划,它包括对核心产品的修改和一些新功能的发布。每个过程在每个增量发布后不断重复,直到产生最终的完善产品。

演化模型

演化模型是一种全局的软件(或产品)生存周期模型。属于迭代开发方法。

即根据用户的基本需求,通过快速分析构造出该软件的一个初始可运行版本,这个初始的软件通常称之为原型,然后根据用户在使用原型的过程中提出的意见和建议对原型进行改进,获得原型的新版本。重复这一过程,最终可得到令用户满意的软件产品。

采用演化模型的开发过程,,实际上就是从初始的原型逐步演化成最终软件产品的过程。演化模型特别适用于对软件需求缺乏准确认识的情况。

缺点:

1.如果所有的产品需求在一开始并不完全弄清楚的话,会给总体设计带来困难以及削弱产品设计的完整性,并因而影响产品性能的优化及产品的可维护性;

2.如果缺乏严格的过程管理的话,这个生命周期模型很可能退化为一种原始的无计划的“试-错-改”模式;

统一过程模型

统一过程(RUP/UP,Rational Unified Process)是一种以用例驱动、以体系结构为核心、迭代以及增量的软件过程模型,由UML方法和工具支持,广泛应用于各类面向对象项目。

RUP是Rational公司开发并维护,和一系列软件开发工具紧密集成。RUP蕴含了大量优秀的实践方法,这些经验被称为“最佳实践”。

最佳实践包括:

  • 迭代式软件开发
  • 需求管理
  • 基于构件的架构应用
  • 建立可视化的软件模型
  • 软件质量严重
  • 软件变更控制等

RUP的静态结构包括6个核心工作流(业务建模、需求、分析设计、实现、测试、部署)和3个核心支持工作流(配置与变更管理、项目管理、环境)

image-20201124155011590

RUP把软件的生命周期划分成4个连续的阶段。每个阶段都有明确的目标,并且定义了用来评估是否达到这些目标的里程碑。每个阶段的目标通过一次或多次迭代来完成。

4个阶段的工作目标包括:初始阶段、精华阶段、构建阶段、移交阶段。

RUP模型采用迭代开发,通过多次执行不同的开发工作流,逐步确定一部分需求分析和风险,在设计、实现并确认这部分后,再去做下一部分的需求分析、设计、实现和确认工作,依次进行下去,直到整个项目完成,这样能够在逐步集成中更好地理解需求,构建一个健壮的体系结构。

3、过程评估与CMM/CMMI的基本概念

答:

1)CMM(Capability Maturity Model)即能力成熟度模型,是美国卡耐基梅隆大学软件工程研究所(SEI)在美国国防部资助下于二十世纪八十年代末建立的,用于评价软件机构的软件过程能力成熟度的模型。此模型在建立和发展之初,主要目的在于提供一种评价软件承接方能力的方法,为大型软件项目的招投标活动提供一种全面而客观的评审依据。而发展到后来,又同时被软件组织用于改进其软件过程。(《复旦大学软件工程ppt》)

CMM提供了一个成熟度等级框架:

  • 1级-初始级
  • 2级-可重复级
  • 3级-已定义级
  • 4级-已管理级
  • 5级-优化级

image-20201125143110579

2)美国国防部、美国国防工业委员会和SEI/CMU于1998年启动CMMI项目,希望CMMI是若干过程模型的综合和改进,是支持多个工程学科和领域的系统的、一致的过程改进框架,能适应现代工程的特点和需要,能提高过程的质量和工作效率。(《复旦大学软件工程ppt》)

3)CMMI模型为每个学科的组合都提供两种表示法:阶段式模型和连续式模型。

4、敏捷宣言与敏捷过程的特点

1)敏捷宣言:

(《复旦大学软件工程ppt》)

​ (1)个人和交互高于过程和工具

不是否定过程和工具的重要性,而是强调软件开发中人的作用和交流的作用。软件是由人组成的团队来开发的,与软件项目相关的各类人员通过充分的交流和有效的合作,才能成功地开发出得到用户满意的软件。

如果光有定义良好的过程和先进的工具,而人员的技能很差,又不能很好地交流和协作,软件是很难成功地开发的。

​ (2)可运行软件高于详尽的文档

通过一个可运行的软件了解软件做了什么,远比阅读厚厚的文档要容易得多。

敏捷软件开发强调不断地快速地向用户提交可运行的软件(不一定是完整的软件),以得到用户的认可。

好的必要的文档仍是需要的,它帮助我们理解软件做什么,怎么做,以及如果使用,但软件开发的主要目标时创建可运行的软件。

​ (3)与客户协作高于合同(契约)谈判

只有客户才能明确说明需要什么样的软件,然而,大量的实践表明,在开发的早期客户常常不能完整地表达他们的全部需求,有些早期确定的需求,以后也可能会改变。

要想通过合同谈判的方式,将需求固定下来常常是困难的。

敏捷软件开发强调与客户的协作,通过与客户的交流和紧密合作来F爱心那用户的需求。

(4)对变更及时做出反应高于遵循计划

任何软件项目的开发都应该制订一个项目计划,以确定开发任务的优先顺序和起止日期。然而,随着项目的进展,需求、业务环境、技术等都可能变化,任务的优先顺序和起止日期也可能因种种原因会改变。

因此,项目计划应具有可塑性,有变动的余地。当出现变化时几十做出反应,修订计划以适应变化。

2)敏捷过程的特点

(《复旦大学软件工程ppt》)

​ (1)最优先的是通过尽早地和不断地提交有价值的软件使用户满意

​ (2)欢迎变化的需求,即使该变化出现在开发后期,为了提升对客户的竞争优势,Agile过程利用变化作为动力。

​ (3)以几周到几个月为周期,尽快、不断地发布可运行软件

​ (4)在整个项目过程中,业务人员和开发人员必须天天一起工作

​ (5)以积极向上的员工为中心建立项目组,给予他们所需要的环境和支持,对他们的工作予以充分的信任

​ (6)项目组内效率最高、最有效的信息传递方式是面对面的交流

​ (7)测量项目进展的首要依据是可运行的软件

​ (8)敏捷过程提倡可持续的开发,项目发起者、开发者和用户应能长期保持恒定的速度

​ (9)应时刻关注技术上的精益求精和好的设计,以增强敏捷性

​ (10)简单化是必不可少的,这是尽可能减少不必要工作的艺术

​ (11)最好的架构、需求和设计出自于自我组织的团队

​ (12)团队要定期反思怎样才能更有效,并据此调整自己的行为

二、软件需求

1、软件需求的概念

答:

主观需求:用户解决一个问题或达到一个目标所需要的一种状态或能力;

客观需求:系统为了满足一种约定、标准、规格说明或其它正式文件而必须满足或拥有的一种状态或能力;

功能性需求:

  1. 系统需要提供的服务或功能:如图书检索;
  2. 系统对特定输入的处理方式:如对非法输入的提示;
  3. 系统在特定环境下的行为:如长时间无操作的屏保;

非功能性需求:

  1. 对系统功能或服务附加的质量约束,例如响应时间、容错性、安全性等-客户所关心的(外部质量);
  2. 从系统开发和维护角度的质量属性,例如可理解下、可扩展性等-软件开发或维护者所关心的(内部质量、软件所持有)

2、需求工程的基本过程

答:

需求获取、需求分析与协商、系统建模、需求规约、需求验证、需求管理

3、分层数据流模型

答:

分层数据流图的设计方法

第一步,画子系统的输入输出

把整个系统视为一个大的加工,然后根据数据系统从哪些外部实体接受数据流,以及系统发送数据流到哪些外部实体,就可以画出输入输出图。这张图称为顶层图。

第二步,画子系统的内部

把顶层图的加工分解成若干个加工,并用数据流将这些加工连接起来,使得顶层图的输入数据经过若干加工处理后,变成顶层图的输出数据流。这张图称为0层图。从一个加工画出一张数据流图的过程就是对加工的分解。可以用下述方法来确定加工:在数据流的组成或值发生变化的地方应该画出一个加工,这个加工的功能就是实现这一变化,也可以当作一个单位来处理(这些数据一起到达、一起处理)时,可以把这些数据看成一个数据流。关于数据存储,对于一些以后某个时间要使用的数据,可以组织成为一个数据存储来表示。

第三步,画加工的内部

把每个加工看作一个小系统,把加工的输入输出数据流看成小系统的输入输出流。于是可以像画0层图一样画出每个小系统的加工DFD图。

第四步,画子加工的分解图

对第三步分解出来的DFD图中的每个加工,重复第三步的分解过程,直到图中尚未分解的加工都是足够简单的(即不可再分解)。至此,得到一套分层数据流图。

第五步,对数据流图和加工编号

对于一个软件系统,其数据流图可能有许多层,每一层又有许多张图。为了区分不同的加工和不同的DFD子图,应该对每张图进行编号,以便于管理。

  • 顶层图只有一张,图中的加工也只有一个,所以不必为其编号。
  • 0层图只有一张,图中的加工号分别为0.1、0.2、…,或者1,2.
  • 子图就是父图中被分解的加工号。
  • 子图中的加工号是由图号、圆点和序号组成,如:1.12、1、3等等。

4、用例和场景建模及其UML表达(用例图、活动图、泳道图、顺序图)

用例图:

用例是对一个活动者使用系统的一项功能时所进行的交互过程的一个文字描述序列。对系统的用户需求的描述,表达的是系统的功能和所提供的服务,它只描述活动者和系统在交互过程中做些什么,并不描述怎么做。

image-20201202140233031

活动图:

活动图是状态图的一种特殊情况。用于简化描述一个过程或者操作的工作步骤。活动用圆角矩形表示-比状态图更窄,更接近椭圆。一个活动中的处理一旦完成,则自动引起下一个活动的发生。箭头表示从一个活动转移到下一个活动。和状态图类似,活动图中的起点用一个实心圆表示,终点用一个同心圆(内圆是实心圆)表示。在活动图中可以带判定点,即一组条件引发一条执行路径,另一组条件则引发另一条执行路径,并且这两条执行路径时互斥的。判定点常用小的菱形图标表示,同时在相关路径的附近指明引起这条路径被执行的条件,条件用方括号括起来。请用活动图描述打电话过程。

image-20201202142229171

泳道图:

泳道将活动图中的活动划分为若干组。并将一组指定给负责这组活动的业务组织。在活动图中,泳道使用垂直的实线绘制。

image-20201202142408417

顺序图:

顺序图是强调消息时间的交互图,其描述了对象之间传送消息的时间顺序,用来表示用例中的行为顺序。在该二维图中,对象由左至右排列,消息则沿着纵轴由时间顺序排列。在构筑改图时,应布局简洁。

示意图,购买小车简图。

image-20201202144912288

5、数据模型建模及其UML表达(类图)

答:

类图(Class Diagram)是描述类、接口、协作以及它们之间关系的图,用来显示系统中各个类的静态结构。类图是定义其他图的基础,在类图基础上,可以使用状态图、协作图、组件图和配置图等进一步描述系统其他方面的特性。

在UML中,类被表述成为具有相同结构、行为和关系的一组对象的描述符号。所用的属性与操作都被附在类中。类定义了一组具有状态和行为的对象。其中,属性和关联用来描述状态。属性通常使用没有身份的数据值来表示,如数字和字符串。关联则使用有身份的对象之间的关系表示。行为由操作来描述,方法是操作的具体实现。对象的生命周期则由附加给类的状态机来描述。

在UML的图形表示中,类的表示法是一个矩形,这个矩形由三个部分构成,分别是类的名称(Name)、类的属性(Attribute)和类的操作(Operation)。类的名称位于矩形的顶端,类的属性位于矩形的中间部位,而矩形的底部显示类的操作。中间部位不仅显示类的属性,还可以显示属性的类型以及属性的初始化值等。矩形的底部也可以显示操作的参数表和返回类型等,如图1所示。

image-20201202150918016

在类的构成中还应当包含类的职责(Resopnsibility)、类的约束(Constraint)和类的注释(Note)等信息。

6、行为模型建模及其UML表达(状态机图)

答:

状态机图是用来为对象的状态及造成状态改变的事件建模。UML的状态机图主要用于建立对象类或对象的动态行为模型,表现一个对象所经历的状态序列,引起状态或活动转移的事件,以及因状态或活动转移而伴随的动作。状态机图也可用于描述Use Case,以及全系统的动态行为。

image-20201202151324737

三、软件设计与构造

1、软件体系结构及体系结构风格的概念

答:

软件体系结构(百度版):具有一定形式的结构化元素,即构件的集合,包括处理构件、数据构件和连接构件。处理构件负责对数据进行加工,数据构件是被加工的信息,链接构件把体系结构的不同部分分组组合连接起来。这一定义注重区分处理构件、数据构件和连接构件,这一方法在其他的定义和方法中基本上得到保持。由于软件系统具有的一些共同特性,这种模型可以在多个系统之间传递,特别是可以应用到具有相似质量属性和功能需求的系统中,并能够促进大规模软件的系统级复用。

软件体系结构(指定教材版本):程序或计算机系统的软件体系结构是指系统的一个或者多个结构,它包括软件构件、构件的外部可以见属性以及它们之间的相互关系。

体系结构并非可运行的程序。它是一种表达,能达到以下三种目的:

1)对设计在满足既定需求方面的有效性分析

2)在设计变更相对容易的阶段,考虑体系结构可能的选择方案

3)降低与软件构建相关的风险

体系结构风格:对软件体系结构风格的研究和实践促进了对设计的复用,一些经过实践证实的解决方案也可以可靠地用于解决新的问题。体系结构风格的不变部分使不同的系统可以分享同一个实现代码。只要系统是使用常用的、规范的方法来组织,就可使别的设计者很容易地理解系统的体系结构。例如,如果某人把系统描述为“客户/服务器”模式,则不必给出设计细节,我们立刻就会明白系统是如何组合和工作的。

下面是Garlan和Shaw对通用体系结构风格的分类:

1)数据流风格:批处理序列;管道/过滤器

2)调用/返回风格:主程序/子程序;面向对象风格;层次结构

3)独立构件风格:进程通讯;事件系统

4)虚拟机风格:解释器;基于规则的系统

5)仓库风格:数据库系统;超文本系统;黑板系统

2、设计模式的概念(来自百度)

答:

软件设计模式(Design Pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。

3、模块化设计的基本思想及概念(抽象、分解、模块化、封装、信息隐藏、功能独立)(来自教材)

1)模块化:模块化设计思想是把整个软件划分为几个独立命名的或者独立访问的构成成分,这些模块集合起来就满足了问题的需要,就能把一个大而复杂的软件系统划分成易于理解的比较单纯的模块化结构。

模块化的要求:需要满足信息隐蔽原则,模块之间相互独立,并且尽量符合高内聚低耦合的要求。

2)封装:是一种信息隐蔽技术,用户只能看到对象封装界面上的信息,对象的内部实现对用户是隐蔽的。封装的目的是使对象的使用和生产者分离。使对象的定义和实现分开。一个对象通常由对象名、属性和操作三部分组成。

3)信息隐蔽:每个模块的实现细节对于其它模块来说是隐蔽的(不可访问),即模块中所包含的信息(数据和过程)不允许其它不需要这些信息的模块使用。

信息隐蔽作用:设计时把一些可能发生变化的因素隐蔽在某个模块中,以提高可维护性,并且可以减少错误向外传播。

4、软件重构的概念;软件体系结构的UML建模(包图、类图、构件图、顺序图、部署图);(来自博客与教材)

答:

1)重构:以不改变代码外部行为而改进其内部结构的方式来修改软件系统的过程。这是一种净化代码以尽可能减少引入错误的严格方法。

2)包图(略,非重点):属于静态图的一种

3)类图(重点):展示系统类的静态结构,即类与类之间的相互联系。

image-20201210104320458

具体含义及其画法(看网站一学就会)blog.csdn.net/monkey_d_me… )

4)构件图

构件图显示代码的静态结构、是用代码组件来显示代码物理结构的。

img

5)顺序图(重点)

用来显示对象之间发送消息的顺序,以及对象之间的交互,例题:

image-20201210131328790

6)部署图(略,非重点)

展现系统中硬件和软件的物理机构

画法:blog.csdn.net/wangyongxia…

5、接口的概念;面向对象设计原则(开闭原则、Liskov替换原则、依赖转置原则、接口隔离原则)

开闭原则:

类的改动是通过增加代码进行的,而不是修改源代码。

里氏代换原则:

任何抽象类出现的地方都可以用他的实现类进行替换,实际就是虚拟机制,语言级别实现面向对象功能。

依赖倒转原则:

依赖于抽象(接口),不要依赖具体的实现(类),也就是针对接口编程。

接口隔离原则:

不应该强迫用户的程序依赖他们不需要的接口方法。一个接口应该只提供一种对外功能,不应该把所有的操作都封装到一个接口中去。

6、内聚与耦合的概念,常见的内聚和耦合类型

答:

“高内聚,低耦合”

起因:模块独立性指每个模块只完成系统要求的独立子功能,并且与其他模块的联系最少且接口简单,两个定性的度量标准-耦合性和内聚性。

耦合性也称块间联系。指软件系统结构中各模块间互相联系紧密程度的一种度量。模块之间联系越紧密,其耦合性就越强,模块的独立性则越差。模块间耦合高低取决于模块间接口的复杂性、调用的方式及传递的信息。

耦合性分类(低-高):无直接耦合;数据耦合;标记耦合;控制耦合;公共耦合;内容耦合;

1)无直接耦合:两个模块之间没有直接联系,它们之间的联系完全是通过主模块的控制和调用来实现的;

2)数据耦合:指两个模块之间有调用关系,传递的是简单的数据值,相当于高级语言的值传递;

3)标记耦合:指连个模块之间传递的是数据结构,如高级语言中的数组名、记录名、文件名等这些名字即标记,其实传递的是这个数据结构的地址;

4)控制耦合:指一个模块调用另外一个模块时,传递的是控制变量(如开关、标志等),被调模块通过该控制变量的值有选择地执行块内某一功能;

5)公共耦合:指通过一个公共数据环境相互作用的那些模块间的耦合。公共耦合的复杂程序随耦合模块的个数增加而增加。

6)内容耦合:这是最高程度的耦合,也是最差的耦合。当一个模块直接使用另一个模块的内部数据,或通过非正常入口而转入另一个模块内部。

内聚性又称块内联系。指模块的功能强度的度量,即一个模块内部各个元素彼此结合的紧密程度的度量。若一个模块内各元素(语名之间、程序段之间)联系的越紧密,则它的内聚性就越高。

内聚性分类(低-高):偶然内聚;逻辑内聚;时间内聚;通信内聚;顺序内聚;功能内聚;

1)偶然内聚:指一个模块内的各处理元素之间没有任何联系。

2)逻辑内聚:指模块内执行几个逻辑上相似的功能,通过参数确定该模块完成哪一功能。

3)时间内聚:把需要同时执行的动作组合在一起形成的模块称为时间内聚模块。

4)通信内聚:指模块内所有处理元素都在同一个数据结构上操作(有时称之为信息内聚),或者指各处理使用相同的输入数据或者产生相同的数据数据。

5)顺序内聚:指一个模块中各个处理元素都密切相关于同一功能且必须顺序执行,前一功能元素输出就是下一个功能元素的输入。

6)功能内聚:这是最强的内聚,指模块内所有元素共同完成一个功能,缺一不可。与其他模块耦合是最弱的。

耦合性与内聚性是模块独立性的两个定性标准,将软件系统划分模块时,尽量做到高内聚低耦合,提高模块独立性,为设计高质量的软件结构奠定基础。

四、软件测试

1、软件测试及测试用例的概念;

答:

软件测试是在规定的条件下对程序进行操作,以发现程序错误,衡量软件质量,并对其是否能满足设计要求进行评估的过程。

测试用例是为了某个特殊的目标而编制的一组测试输入、执行条件以及预期结果,以便测试某个程序路径或核实是否满足某个特定需求。

2、单元测试、集成测试、确认测试、系统测试、回归测试的概念;

答:

单元测试:又称模块测试,着重对软件设计的最小单位(程序模块)进行验证。单元测试根据设计描述,对重要的控制路径进行测试,以发现各个模块内部的错误。单元测试通常采用白盒测试,并且多个模块并行进行测试。

集成测试:又称组装测试。、联合测试,经单元测试后,每个模块都能独立工作,但把它们放在一起往往不能正常工作。确认测试以软件需求规格说明书为依据,检查软件的功能和性能及其它特写是否与用户的需求一致,包括合同规定的全部功能和性能、文档资料(正确且合理)、其他需求(如可移植性、兼容性、错误恢复能力、可维护性等)。

系统测试:是将通过确认测试的软件,作为整个基于计算机系统的一个元素,与其它系统成分(如硬件、外设、某些支持软件、数据和人员等)集成起来,在实际运行环境下,对计算机系统进行一系列的集成测试和确认测试。系统测试的目的在于通过与系统的需求定义作比较,发现软件与系统定义不符合或与之矛盾的地方。

回归测试:是指修改旧代码后,重新进行测试以确认修改没有引入新的错误或导致其他代码产出错误。

3、调试的概念、调试与测试的关系;

答:

测试的目的是发现错误,调试(也称排错)的目的是确定错误的原因和准确位置,并加以纠正。

调试与测试的关系:测试和调试在目标、方法和思路上都有所不同,测试是一个过程,目的是显示存在的软件错误,通常由软件测试工程师实施。而调试是一种方法,一种手段,目的是发现错误原因并解决,一般来说调试时测试后的活动,通常由开发工程师实施。

4、测试覆盖度的概念

答:

测试覆盖度评估是衡量阶段性软件测试执行状态的重要手段之一,来确定测试是否达到事先设定的测试任务完成的标准。测试覆盖率则是测试覆盖度评估中一种量化的表示方法,一般通过被测试的软件产品需求、功能点、测试用例数或程序代码等来进行计算。

5、白盒测试、黑盒测试的概念

答:

白盒测试又称结构测试,这种方法把测试对象看作一个透明的盒子,测试人员根据程序内部的逻辑结构及有关信息涉及测试用例,检查程序中所有逻辑路径是否按预定的要求正确地工作。

黑盒测试又叫做功能测试,这种方法是把测试对象看做一个黑盒子,测试人员完全不考虑程序内部的逻辑结构和内部特性,只依据程序的需求规格说明书,检查程序的功能是否符合它的功能说明。

6、代码圈复杂度的计算方法

答:

圈复杂度是一种代码复杂度的衡量标准。在软件测试的概念里,圈复杂度用来衡量一个模块判定结构的复杂程度,数量上表现为独立线性路径条数,即合理的预防 错误所需测试的最小路径条数,圈复杂度大说明程序代码可能质量低且难以测试和维护,根据经验,程序的可能错误和高的圈复杂度有很大关系。它的计算方法为:V(G)=e-n-2p。e表示控制流图中边的数量,n表示控制流图中节点的数量,p表示图的连接组件数目(图的组件数是相连节点的最大集合),因为控制流图都是连通的,所以p永远为1

image-20201212110408665

V(G)=区域数=判定节点数+1

image-20201212110442050

V(G)=R。R代表平面被控制流图划分成的区域数

image-20201212110519796

7、白盒测试中的基本路径测试方法

答:

基本路径测试是一种白盒测试技术,这种方法首先根据程序或设计图画出控制流图,并计算其区域数,然后确定一组组里的程序执行路径(称为基本路径),最后为每一条基本路径设计一个测试用例。在实际问题中,一个不太复杂的程序,其路径数可能很大,特别在包含循环时。因此要把覆盖的路径数压缩到一定的限度内。

8、黑盒测试中等价类划分方法

答:

等价类划分是指由于不能穷举所有可能的输入数据来进行测试,所以只能选择少量有代表性的输入数据,来揭露尽可能多的程序错误,等价类划分方法将所有可能的输入数据划分成若干个等价类,然后在每个等价类中选取一个代表性的数据作为测试用例,测试用例由有效等价类和无效等价类的代表组成,从而保证测试用例具有完整性和代表性。

mg-5OugY4Ha-1607958334409)]

V(G)=区域数=判定节点数+1

[外链图片转存中…(img-Paxa2fta-1607958334410)]

V(G)=R。R代表平面被控制流图划分成的区域数

[外链图片转存中…(img-wcfd9uYF-1607958334410)]

7、白盒测试中的基本路径测试方法

答:

基本路径测试是一种白盒测试技术,这种方法首先根据程序或设计图画出控制流图,并计算其区域数,然后确定一组组里的程序执行路径(称为基本路径),最后为每一条基本路径设计一个测试用例。在实际问题中,一个不太复杂的程序,其路径数可能很大,特别在包含循环时。因此要把覆盖的路径数压缩到一定的限度内。

8、黑盒测试中等价类划分方法

答:

等价类划分是指由于不能穷举所有可能的输入数据来进行测试,所以只能选择少量有代表性的输入数据,来揭露尽可能多的程序错误,等价类划分方法将所有可能的输入数据划分成若干个等价类,然后在每个等价类中选取一个代表性的数据作为测试用例,测试用例由有效等价类和无效等价类的代表组成,从而保证测试用例具有完整性和代表性。

本文转载自: 掘金

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

1…581582583…956

开发者博客

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