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

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


  • 首页

  • 归档

  • 搜索

Go内存与性能 题外话 你以为结束了

发表于 2021-11-12

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

生命不息,学习不止

题外话

周五了,明天就可以休息了,奋斗了一周想办法犒劳自己一下吧,哟,这个必x客的披萨看着不错,行,就搞个必x客吧……11月13日新闻,神秘人收购必x客,改名必b客

又是乏味的一天…… 人生就是这么简简单单

话说今天升温了,真不错

在这里插入图片描述
废话不多说,上货
在这里插入图片描述

空间复杂度与时间复杂度

1、时间复杂度

(1)时间频度 一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。
(2)时间复杂度 在刚才提到的时间频度中,n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。但有时我们想知道它变化时呈现什么规律。为此,我们引入时间复杂度概念。 一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。

2.算法的空间复杂度

空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。
一个算法在计算机存储器上所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。

举个例子

在这里插入图片描述

斐波那契数列

斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”
斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233……
这个数列从第3项开始,每一项都等于前两项之和

我们试着求一下前40项的各项值,并看看花费多长时间
上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码package main
import (
"fmt"
"time"
)
func main() {
result := 0
start := time.Now()
for i := 1; i <= 40; i++ {
result = fibonacci(i)
fmt.Printf("数列第 %d 位: %d\n", i, result)
}
end := time.Now()
delta := end.Sub(start)
fmt.Printf("程序的执行时间为: %s\n", delta)
}
func fibonacci(n int) (res int) {
if n <= 2 {
res = 1
} else {
res = fibonacci(n-1) + fibonacci(n-2)
}
return
}

运行结果太长了,我就给你们截一部份

数列第 1 位: 1

数列第 2 位: 1

数列第 3 位: 2

数列第 4 位: 3

…

数列第 39 位: 63245986

数列第 40 位: 102334155

程序的执行时间为: 2.2848865s

大概就是这样,电脑性能不同,执行时间也会有所区别

使用内存缓存以提高性能

大部分提高性能或者减少算法时间复杂度的方式都是拿空间来交换,
其中内存缓存是很常见的一种
内存缓存的实现思路是在计算得到第 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
go复制代码package main
import (
"fmt"
"time"
)
const LIM = 41
var fibs [LIM]uint64
func main() {
var result uint64 = 0
start := time.Now()
for i := 1; i < LIM; i++ {
result = fibonacci(i)
fmt.Printf("数列第 %d 位: %d\n", i, result)
}
end := time.Now()
delta := end.Sub(start)
fmt.Printf("程序的执行时间为: %s\n", delta)
}
func fibonacci(n int) (res uint64) {
// 记忆化:检查数组中是否已知斐波那契(n)
if fibs[n] != 0 {
res = fibs[n]
return
}
if n <= 2 {
res = 1
} else {
res = fibonacci(n-1) + fibonacci(n-2)
}
fibs[n] = res
return
}

同样截取部分结果
运行结果如下所示:

数列第 1 位: 1

数列第 2 位: 1

数列第 3 位: 2

数列第 4 位: 3

…

数列第 39 位: 63245986

数列第 40 位: 102334155

程序的执行时间为: 0.0149603s

通过运行结果可以看出,同样获取数列第 40 位的数字,使用内存缓存后所用的时间为 0.0149603 秒,对比之前未使用内存缓存时的执行效率,可见内存缓存的优势还是相当明显的。

你以为结束了

本次例子使用的是递归的方实,其实递归就是非常消耗内存的方实。
小问题:如何不使用递归完成斐波那契数列的实现?

上期问题:读取ini文件后,如何通过键去修改值呢?

上期答案:通过file.WriteAt方法内容覆盖写入

在这里插入图片描述

大家看完发现有什么错误,写在下面吧!跟我黑虎阿福比划比划!
在这里插入图片描述

本文转载自: 掘金

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

Value注解读取配置文件中的内容 Java随笔记

发表于 2021-11-12

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


相关文章

Java随笔记:Java随笔记


  • 为了简化读取properties文件中的配置值,spring支持@value注解的方式来获取,这种方式大大简化了项目配置,提高业务中的灵活性。
    1. 两种使用方法
      • 1)@Value(“#{configProperties[‘key’]}”)
      • 2)@Value(“${key}”)
    1. 配置文件示例
      • 1
        2
        3
        4
        5
        6
        makefile复制代码ftp:
        ftplp: 10.2.23.89
        ftpPort: 21
        ftpUser: uftp
        ftpPwd: 12345678
        ftpRemotePath: /home

说明:以上是配置文件中的信息,主要是一些账号密码等信息。

  1. 读取yml配置文件的工具类
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
typescript复制代码package com.dbright.dataprediction.entity;
​
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
​
@Component
@PropertySource("classpath:ftpconfig.yml")
@ConfigurationProperties(prefix = "ftp")
public class FtpProperties {
​
  @Value("${ftplp}")
  public String ftplp;
  @Value("${ftpPort}")
  public String ftpPort;
  @Value("${ftpUser}")
  public String ftpUser;
  @Value("${ftpPwd}")
  public String ftpPwd;
  @Value("${ftpRemotePath}")
  public String ftpRemotePath;
​
  public String getFtplp() {
      return ftplp;
  }
​
  public void setFtplp(String ftplp) {
      this.ftplp = ftplp;
  }
​
  public String getFtpPort() {
      return ftpPort;
  }
​
  public void setFtpPort(String ftpPort) {
      this.ftpPort = ftpPort;
  }
​
  public String getFtpUser() {
      return ftpUser;
  }
​
  public void setFtpUser(String ftpUser) {
      this.ftpUser = ftpUser;
  }
​
  public String getFtpPwd() {
      return ftpPwd;
  }
​
  public void setFtpPwd(String ftpPwd) {
      this.ftpPwd = ftpPwd;
  }
​
  public String getFtpRemotePath() {
      return ftpRemotePath;
  }
​
  public void setFtpRemotePath(String ftpRemotePath) {
      this.ftpRemotePath = ftpRemotePath;
  }
}
  • 说明:以上是使用@value注解来读取yml配置文件的代码示例
+ 1)@component —— 把普通pojo实例化到spring容器中,相当于配置文件中的`<bean id="" class=""/>`
+ 2. @PropertySource("classpath:ftpconfig.yml") —— 设置yml文件的路径,方便扫描到。一般我们配置文件都是放在resources下。所以我们只需要 classpath+所需要读取的配置文件名称。
+ 3)@ConfigurationProperties(prefix = "ftp") —— 这个不需要解释太多,配置文件里面内容的前缀,我们读取的是ftp下的信息。
+ 4)@Value("${ftplp}") —— 这是读取我们所需的配置信息,美元符号+{字段名}即可制定
+ 5)下面定义字符串来接收所读取到的配置信息。
+ 6)写set和get方法,方便外部类调用。
  1. 演示:效果图如下 获取结果 在这里插入图片描述
  • 可以看到,我们成功取到了我们想要的值。
  1. 一开始说的第二种和这个差不多,把{}外的 $ 变成 # 号,然后里面指定配置文件的信息+字段而已。大同小异,我就不贴代码上来了。

以上内容可优化为动态读取不同环境下的配置文件的内容!优化内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
less复制代码@Component
@PropertySource("classpath:application-${spring.profiles.active}.yml")
@ConfigurationProperties(prefix = "entid")
public class AplicationUtils {
​
   @Value("${entName}")
   public String ftplp;
​
   public String getFtplp() {
       return ftplp;
  }
​
   public void setFtplp(String ftplp) {
       this.ftplp = ftplp;
  }
}
​
  • 在实际开发中,我们一般会有三个环境:
+ 开发环境
+ 测试环境
+ 线上环境.
+ 在不同的环境下读取的东西可能不一样,如:账号密码三个环境不一致,所以这时候我们需要用到动态读取配置文件中的内容.
![在这里插入图片描述](https://gitee.com/songjianzaina/juejin_p10/raw/master/img/15b379162888eb87c52d5540cc1d7f8f2c03c79c794d31b6506d1a6e60101889)
  • 配置文件加上@PropertySource(“classpath:application-${spring.profiles.active}.yml”),表明读取当前选中的环境读取其配置文件信息.
  • 今儿太忙,来不及写了。这是几年前的文章了,发上来凑个数,见笑了。

路漫漫其修远兮,吾必将上下求索~

如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧~hahah

本文转载自: 掘金

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

SpringBoot中借助springfactories文

发表于 2021-11-12

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

SpringBoot在包扫描时,并不会扫描子模块下的内容,这样就使得我们的子模块中的Bean无法注入到Spring容器中。SpringBoot就为我们提供了spring.factories这个文件,让我们可以轻松的将子模块的Bean注入到我们的Spring容器中,本篇文章我们就一起探究一下spring.factories 跨模块实例化Bean的原理。

  1. 前言

我们在上篇文章中也讲到构建自己构建starter,其中spring.factories就起到重要的作用,我们是通过spring.factories让starer项目中的Bean注入到Web模块的Spring容器中。本篇文章就来探究一下spring.factories文件,更深层次的东西,以及我们是如何借助该文件实例化Bean的。

  1. 配置

spring.factories文件一般都是配置在src/main/resources/META-INF/ 目录下。

也就是说我们在IDEA新建的SpringBoot项目或者Maven项目的资源文件resources目录下新建一个META-INF文件夹,再建一个spring.factories文件即可,新建的文件没有问题的化,一般IDEA都能自动识别,如下图所示。

image.png

spring.factories 的文件内容就是接口对应其实现类,实现类可以有多个

文件内容必须是kv形式,即properties类型

1
ini复制代码org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.zhj.config.AutoConfiguration

如其一个接口有多个实现,如下配置

1
2
3
4
ini复制代码org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory
  1. 原理

在spring -core 中定义了SpringFactoriesLoader 类,这个类就是让spring.factories文件发挥作用的类。SpringFactoriesLoader类的作用就是检索META-INF/spring.factories文件,并获取指定接口将其实现实例化。 在这个类中定义了两个对外的方法:

  • loadFactories 根据给定的接口类获取其实现类的实例,这个方法返回的是对象列表
  • loadFactoryNames 根据给定的类型加载类路径的全限定类名,这个方法返回的是全限定类名的列表。

源码如下:

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
typescript复制代码public final class SpringFactoriesLoader {

   // 文件位置
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

   // 缓存
private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();
​
​
private SpringFactoriesLoader() {
}
​
​
/**
* 根据给定的类型加载并实例化工厂的实现类
*/
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
Assert.notNull(factoryType, "'factoryType' must not be null");
// 获取类加载器
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
// 加载类的全限定名
List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
if (logger.isTraceEnabled()) {
logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames);
}
List<T> result = new ArrayList<>(factoryImplementationNames.size());
for (String factoryImplementationName : factoryImplementationNames) {
// 实例化Bean,并将Bean放入到List集合中
result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));
}
AnnotationAwareOrderComparator.sort(result);
return result;
}

/**
* 根据给定的类型加载类路径的全限定类名
*/
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {

       // 获取工厂类型名称
String factoryTypeName = factoryType.getName();
// 加载所有META-INF/spring.factories中的value
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 根据类加载器从缓存中获取,如果缓存中存在,就直接返回,如果不存在就去加载
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}

try {
// 获取所有jar中classpath路径下的META-INF/spring.factories
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
// 遍历所有的META-INF/spring.factories
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 将META-INF/spring.factories中的key value加载为Prpperties对象
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
// key就是接口的类名称
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
// 以factoryTypeName为key,实现类为value放入map集合中
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
// 加入缓存
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}

// 通过反射实例化Bean对象
@SuppressWarnings("unchecked")
private static <T> T instantiateFactory(String factoryImplementationName, Class<T> factoryType, ClassLoader classLoader) {
try {
Class<?> factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader);
if (!factoryType.isAssignableFrom(factoryImplementationClass)) {
throw new IllegalArgumentException(
"Class [" + factoryImplementationName + "] is not assignable to factory type [" + factoryType.getName() + "]");
}
return (T) ReflectionUtils.accessibleConstructor(factoryImplementationClass).newInstance();
}
catch (Throwable ex) {
throw new IllegalArgumentException(
"Unable to instantiate factory class [" + factoryImplementationName + "] for factory type [" + factoryType.getName() + "]",
ex);
}
}
​
}
  1. 总结

Spring通过SpringFactoriesLoader实例化Bean的过程

  • 获取SpringFactoriesLoader对应的类加载器
  • 查找缓存,查看缓存中是否已经读取到所有jar中classpath路径下的META-INF/spring.factories的内容
  • 如果缓存已经存在,根据/spring.factories文件中配置的全限定类名通过反射实例化Bean
  • 如果缓存中没有值,则扫描所有jar中的这个META-INF/spring.factories文件,并将其以读取到缓存中,并返回这个配置列表
  • 然后根据这个全限定类名的列表再通过反射实例化Bean

本文转载自: 掘金

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

Synchronized与锁升级

发表于 2021-11-12

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

一 高并发时锁的使用

同步调用应该去考量锁的性能的损耗,能用无锁数据结构,就不要用锁,能锁区块,就不要锁整个方法体,能用对象锁,就不要用类锁。

锁升级的过程

synchronized锁(重量级锁):由对象头中的Mark Word根据锁标志位的不同而被复用及锁升级策略。

java5只有重量锁可以用.

java6引入了偏向锁和轻量级锁。

锁的是用有一个逐步升级的过程,不能直接使用sync重量级锁。

二 为什么每一个对象都可以成为一个锁

每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。

Monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,成本非常高。

三 synchronized锁种类及升级步骤

1 升级流程

synchronized用的锁是存在Java对象头里的Mark Word中。

锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位。

下图:(重要)

2 无锁

不使用锁,直接代码执行。

从图中value的进行解析:(结合上面的64位图使用)(只有用到hashcode第一行和第二行才有数据)【从第二行倒数往前看为顺序】

从第二行开始,第一段数据00100011到第一行的01011110这一段是hashcode的信息,而前面的暗色的001是偏向锁位和锁的标志位(代表无锁),第二行的后面的后三段00000000是上图的25位,因为只有24,所以要在第一段数据补0。

3 偏向锁

作用:

当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获得锁

在多线程情况下,锁不仅不会竞争,还存在锁由同一线程多次获得的情况,偏向锁就此产生。

也就意味着:自始至终使用锁的线程只有一个,偏向锁没有额外的开销,性能极高。

但是如果发生了竞争,当1线程和2线程抢夺资源的时候,锁就不是偏向同一个线程了,这时候就不是偏向锁了,需要进行锁升级。升级为轻量级锁。才能保证公平。(偏向锁不会主动释放)

**目的:**为了解决只有在一个线程执行同步时提高性能。

**偏向锁的实现方式:**通过CAS方式修改markword中的线程ID

执行过程(以account对象举例)

偏向锁的操作不用直接捅到操作系统,不涉及用户到内核转换,不必要直接升级为最高级,我们以一个account对象的“对象头”为例。

假如有一个线程执行到synchronized代码块的时候,JVM使用CAS操作把线程指针ID记录到Mark Word当中,并修改标偏向标示,标示当前线程就获得该锁。锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。

执行完同步代码块后,线程并不会主动释放偏向锁。这时线程获得了锁,可以执行同步代码块。当该线程第二次到达同步代码块时会判断此时持有锁的线程是否还是自己(持有锁的线程ID也在对象头里),JVM通过account对象的Mark Word判断:当前线程ID还在,说明还持有着这个对象的锁,就可以继续进入临界区工作。由于之前没有释放锁,这里也就不需要重新加锁。 如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

结论:JVM不用和操作系统协商设置Mutex(争取内核),它只需要记录下线程ID就标示自己获得了当前锁,不用操作系统接入。上述就是偏向锁:在没有其他线程竞争的时候,一直偏向偏心当前线程,当前线程可以一直执行。

偏向锁JVM命令

java -XX:+PrintFlagsInitial |grep BiasedLock*(查询java信息中偏向锁的信息)

实际上偏向锁在JDK1.6之后是默认开启的,但是启动时间有延迟。

所以需要添加参数-XX:BiasedLockingStartupDelay=0,让其在程序启动时立刻启动。

开启偏向锁

XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

关闭偏向锁:

关闭之后程序默认会直接进入轻量级锁状态

-XX:-UseBiasedLocking

关闭延迟(演示偏向锁开启)

-XX:BiasedLockingStartupDelay=0

**要关闭延迟的原因:**在jdk1.6之后,程序启动几秒之后才会激活,在JVM中关闭。这时候才会实时的显示偏向锁的偏移为101。

偏向锁的撤销

竞争线程尝试CAS更新对象头失败,会等待到全局安全点(此时不会执行任何代码)撤销偏向锁。

偏向锁的撤销偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。

撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:

① 第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级。此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。

② 第一个线程执行完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向 。

红线部分为偏向锁获取和撤销过程:

4 轻量级锁

本质:自旋锁

标志位:0 0;

升级到重量级锁:先自旋再阻塞。

长期的自旋,会空耗cpu的资源,自旋的时候是有一定的次数,如果次数内不成功,就升级为重锁。

在jdk6之前:默认启用,默认的自旋次数10次。或者自选线程数超过cpu核数的一半(了解即可)

jdk6之后:根据同一个自旋锁上一次自旋的时间和拥有锁线程的状态来决定。(自适应)

轻量锁与偏向锁的区别和不同(重)

1 争夺轻量级锁失败时,自旋尝试抢占锁。

2 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁。

5 重量级锁

什么时候需要使用?

大量线程参与竞争。使用sync。

总结

synchronized的升级过程:先自旋,不行再阻塞。

四 锁粗化和锁消除

JIT:just in time Compiler(即时编译器)

1
2
3
vbnet复制代码12345678910111213141516171819

//锁消除public class SafeDoubleCheckTwo { static Object object = new Object(); public void test(){ Object object = new Object(); //锁消除 synchronized (object){ } } public static void main(String[] args) { } }Copy

锁消除理解:方法中锁住的对象没有被共用扩散,不管被当前线程或者下一个线程访问,都没什么区别,每个人都可以拿一份锁,都可以进入新的对象空间,没意义。

锁失去了作用,即锁消除。

锁粗化理解:假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块, 加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能。

本文转载自: 掘金

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

python matplotlib绘制折线图 复习回顾 1

发表于 2021-11-12

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

复习回顾

众所周知,matplotlib 是一款功能强大开源的数据可视化模块,凭借着强大的扩展性构建出更高级别的绘图工具接口如seaborn、ggplot。我们来看看往期学习章节内容概述吧~

  • matplotlib 模块概述:对matplotlib模块具有便利的交互新创建图表、多种图表定制以及强大的可扩展性,matplotlib可满足不同用户的使用,初次体验绘制图表功能
  • matplotlib 模块底层原理:matplotlib 模块包含脚本层、美工层及后端层,对各个层相关提供的操作进行学习

在 matplotlib 官网教程中,可以绘制诸如折线图、柱状图、饼图等常规图外,还有可以绘制动态图、散点图、等高线图、帽子图、多个子图等

matplotlib.png

接下来,我们将继续学习matplotlib 图表绘制具体的功能实操,掌握针对不同图表的绘制

本期,我们重点对折线图的绘制进行学习和掌握,Let’s go~

  1. 折线图概述

  • 什么是折线图?

+ 折线图是在坐标中通过线条升降的方式展示随着某种变化而变化的连续性数据
+ 通过折线的起伏表示数据的增减变化的情况
+ 折线图可以拆分为动态折线图、依存关系折线图和次数分布折线图
  • 折线图使用场景

折线图自身的线条的变化,可以在图表中清晰读取到数据变化情况,可以运用的场景特点如下

+ 描绘统计事项总体指标的动态
+ 研究对象间的依存关系
+ 总体中各个部分的分配情况
+ 适合大量数据展示其趋势变化
  • 绘制折线图步骤

1. 导入matplotlib.pyplot模块
2. 准备数据,可以使用numpy/pandas整理数据
3. 调用pyplot.plot()绘制折线图
  • 案例展示

本期,我们使用折线图来展示从 10份 所有文章访问量数据展示

+ 本期,我们所有的案例用到的数据如下
1
2
3
4
5
6
python复制代码import random


x_data = ["10月{}日".format(i+1) for i in range(30)]

y_view = [random.randint(50,200) for i in range(30)]
+ 展示10月份数据折线图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Python复制代码 
import matplotlib.pyplot as plt
import random


plt.rcParams["font.sans-serif"]=['SimHei']
plt.rcParams["axes.unicode_minus"]=False

x_data = ["10月{}日".format(i+1) for i in range(30)]

y_view = [random.randint(50,200) for i in range(30)]

plt.figure(figsize=(20,5),dpi=90)

plt.plot(x_data,y_view)

plt.xticks(rotation=45)
plt.title("访问量分析")
plt.xlabel("日期")
plt.ylabel("访问量")

plt.show()

image.png

  1. 折线2D属性

  • linestyle:折线样式

属性值 说明
“-“ 、”solid” 默认实线显示
“–”、”dashed” 虚线
“-.” “dashdot” 点划线
“:”、”dotted” 虚线
“None” “””” 空
* color:折线颜色
———-
+ 颜色简称:



| 属性值 | 说明 | 属性值 | 说明 |
| --- | --- | --- | --- |
| "b"/"bule" | 蓝色 | "m"/"magenta" | 品红 |
| "g" /"green" | 绿色 | "y"/"yellow" | 黄色 |
| "r"/"red" | 红色 | "k"/"black" | 黑色 |
| "c"/"cyan" | 青色 | "w"/"white" | 白色 |
+ rgb


    - 格式形式:(r,g,b) 或者(r,g,b,a)
    - 取值范围:r,g,b,a 取值范围在[0,1]之间
    - [0,1]之间的浮点数的字符串形式,0表示黑色,1表示白色
  • marker:坐标值标记

+ marker 标记物通常在折线图plot、散点图scatter和误差图errorbar上应用
+ marker 提供多达40个标记的样式可供选择,具体详情看见[marker官方说明](https://matplotlib.org/stable/api/markers_api.html#module-matplotlib.markers)
+ marker 在图表中常用的有如下:



| 属性值 | 说明 | 属性值 | 说明 |
| --- | --- | --- | --- |
| "o" | ⏺️圆圈标记 | "8" | 八边形 |
| "v" | 🔽倒三角标记 | "s" | ⏹️正方形标记 |
| "^" | 🔼正三角标记 | "\*" | ⭐星号 |
| "<" | ◀️左三角标记 | "+" | ➕加号 |
| ">" | ▶️右三角标记 | "x" | X星星 |
| "1" | 向下Y标记 | "D" | 🔷钻石标记 |
| "2" | 向上Y标记 | " | " | I垂直线标记 |
| "3" | 向左Y标记 | "\_" | \_水平线标记 |
| "4" | 向右Y标记 | "p" | ⭐五角星标记 |
+ 标记还提供其他方法


    - markeredgecolor:标记边界颜色
    - markeredgewidth:标记宽度
    - markfacecorlor:标记填充色
    - markersize:标记大小
  • fillstyle:标记填充方法

属性值 说明
“full” 整个标记
“left” 左边标记一半
“right” 右边标记一半
“bottom” 底部标记一半
“top” 顶部标记一半
“none” 无填充
* linewidth(lw): 直线宽度
——————-
* 对第一节案例添加直线属性:虚线表示,坐标用绿色左半填充圈标记
——————————
1
2
3
python复制代码# 直线属性
plt.plot(x_data,y_view,linestyle="--"
,marker="o",markeredgecolor="g",fillstyle="left")

image.png

  • 更多属性

在matplotlib官网对直线2D属性有更多的介绍

  1. 坐标管理

  • 坐标轴名字设置

+ 设置X轴名称:pyplot.xlabel(str)
+ 设置y轴名称:pyplot.ylabel(str)
  • 坐标轴刻度设置

+ x轴坐标刻度设置:pyplot.xticks(ticks=[],rotation)
+ y轴坐标刻度设置:pyplot.yticks(ticks=[],rotation)
+ 参数说明:
    - ticks:列表类型,表示x轴范围
    - rotation:翻转角度
  • 坐标轴位置设置

+ 坐标轴位置设置需要通过pyplot.gca()先获取当前的Axes
+ 然后调用ax.spines[].set\_position()设置位置
+ ax.spines['bottom'].set\_position(('axes',0.5)) 表示将x轴设置在y轴50%处
  • 指定坐标值标注

+ pyplot.annotate() 展示指定坐标点的(x,y)值
+ 常用接口参数说明:



| 参数 | 说明 |
| --- | --- |
| txt | 展示的文本 |
| xy | 注释的(x,y) |
| xytext | xy展示的文本 |
| color | 展示的文本颜色 |
  • 继续改造第一节案例:标记出最大访问,y轴移到x轴中心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码max_id = np.argmax(y_view)


show_max = '['+str(x_data[max_id])+','+str(y_view[max_id])+']'


plt.figure(figsize=(20,5),dpi=90)

ax= plt.gca()

ax.spines["left"].set_position(('axes',0.5))

plt.plot(x_data,y_view,linestyle="--",marker="o",markeredgecolor="g",fillstyle="left")

plt.xticks(ticks=np.arange(0,30),rotation=60)

plt.annotate(show_max, xy=(x_data[max_id],y_view[max_id] ), xytext=(x_data[max_id],y_view[max_id]), color='r')

image.png

  1. 多条折线展示图

在一个图表中,我们可以多次调用plot()绘制多条折线展示在同一个表格中

1
2
3
4
python复制代码 star_view = [random.randint(100,200) for i in range(30)]

plt.plot(x_data,y_view,linestyle="--",marker="o",markeredgecolor="g",fillstyle="left")
plt.plot(x_data,star_view,linestyle="-",marker="s",markeredgecolor="r",fillstyle="right")

image.png

  1. 图列管理

当一个图表中存在多个折线图时,我们需要使用图例管理来对每个折线代表对象

  • pyplot.legend(loc): 对图表中折线进行说明
  • loc参数属性值:
属性 代码 属性 代码
‘best’ 0 ‘right’ 5
‘upper right’ 1 ‘center left’ 6
‘upper left’ 2 ‘center right’ 7
‘lower left’ 3 ‘lower center’ 8
‘lower right’ 4 ‘upper center’ 9
‘center’ 10

对上述代码plot()方法添加label属性,注释每条折线的对象

1
2
3
4
ini复制代码plt.plot(x_data,y_view,linestyle="--",marker="o",markeredgecolor="g",fillstyle="left",label="all")
plt.plot(x_data,star_view,linestyle="-",marker="s",markeredgecolor="r",fillstyle="right",label="star")

plt.legend()

image.png

总结

本期,我们对matplotlib 模块 折线图plot()相关方法和属性进行,大家在平时工作中可以多多实践,折线图还是用的比较多的

以上是本期内容,欢迎大家点赞评论,下期见~

本文转载自: 掘金

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

SpringBoot基础之统一数据格式返回

发表于 2021-11-12

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

前言

  1. 在正常的开发过程中,因为前端展示的界面多种多样,所以数据会有各种各样的结构,如果后台对数据不加封装的情况下,数据格式就会千奇百怪,最终会降低开发效率,因此需要统一格式.
  2. 在程序发生异常的情况下, 异常信息提示的格式,很大程度上也和正常返回不一致(和全局异常处理一起阅读更佳),所以需要统一格式
  3. 在业务进行中,有些接口请求成功,但是因为业务需要,需要返回一些特殊的状态,在统一了数据格式之后,前端可以在请求入口对这些状态统一的处理.所以需要统一格式

和全局异常处理一起阅读更佳

实现思路

  1. 在项目我我们常常封装一个返回对象类,在类中定义好标准的数据格式.

正常情况下,我们会将定义三个参数code,msg,data.

data用于存防业务数据,

code用来展示统一定义的状态码

msg用来展示一些基础的成功或者失败信息
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
30
31
arduino复制代码public enum  ResultCode {

SUCCESS(200,"操作成功"),

NO_LOGIN(301,"请重新登录"),
NO_AUTH(302,"您还没有此功能的权限"),
NO_UPDATE_DATA(303,"该数据不能操作"),
NOT_ACCESS(304,"禁止访问"),

FAIL(5000,"操作失败,请联系管理员"),

CUSTOM_FAIL(5001,"自定义错误")

//等等等
;

private int code;
private String msg;

public int code(){
return this.code ;
}
public String msg(){
return this.msg ;
}

ResultCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
}

一般对于中小型公司的中小型项目,正常的状态和异常的状态各个定义一个就好,全靠msg展示信息是没问问题的, 如果有问题的话就去看alibaba出品的java规范.

返回数据的多种实现

(1) Object接收各种数据

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
typescript复制代码public class R implements Serializable {

private int code;
private String msg;
private Object data ;

private R() {}
private R(ResultCode resultCode, String msg) {
this.code = resultCode.code();
this.msg = msg;
}

/***
* 操作成功,当参数为1个,则将数据直接指向data字典, 当 参数为偶数个时,将单数参数设置为Map的key,每个随后的参数设置为Value
*/
public static R ok(Object... data){
R resultVo = new R(200,"操作成功");
resultVo.data =integrationData(data);
return resultVo;
}


/***
* 自定义异常,需要返回异常信息
*/
public static R fail(String failMsg, Object... data){
R resultVo = new R(5000,failMsg);
resultVo.data =integrationData(data);
return resultVo;
}

//.... 只包含基本的,各种变种没有写...

/***
* 将返回参数整理到data中
* @param data
* @return
*/
private static Object integrationData(Object... data){

if(data==null||data.length==0){ return null; }

int length = data.length;

if(length==1){ return data[0]; }

if(length %2 ==1){
throw new RuntimeException("返回结果参数数量不正确,应该为偶数,实际为:"+length);
}
if(length %2 ==0){
HashMap<Object, Object> map = new HashMap<>();
for (int i = 0; i < length; i+=2) {
map.put(data[i],data[i+1]);
}
return map;
}
return "";
}

}

使用方式

1
2
arduino复制代码R.OK("数据1");
R.OK("数据1","KEY2","数据2");

或许你看到的是这种,这种如果存在多个数据,只能手动创建一个Map然后将数据放到data中

1
2
3
4
5
arduino复制代码public class R<T> implements Serializable {
private int code;
private String msg;
private T data ;
}

(2) 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
typescript复制代码public class R extends HashMap<String, Object>{

public R() {
put("code", 0);
put("msg", "操作成功");
}

public static R error(String msg) {
return error(5000, msg);
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}

public static R ok() {
return new R();
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}

public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}

public static R ok(Object data) {
return new R().put("data",data);
}

@Override
public R put(String key, Object value) {
super.put(key, value);
return this;
}

}

这种数据格式就是利用了map来做返回对象,可以想map一样操作他

1
2
arduino复制代码R.OK("数据1");
R.OK("数据1").put("KEY2","数据2");

(3) 实现ResponseBodyAdvice并注解@RestControllerAdvice

这种方法比较少见,并且在实际使用过程中,如果沟通不畅出现问题的几率是比较大的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typescript复制代码@RestControllerAdvice
public class MyResponseAdvice implements ResponseBodyAdvice<Object> {

//开启支持
   @Override
   public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
       return true;
  }

   //处理封装数据
   @Override
   public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
       //处理指定类型的数据
       if(o instanceof String){
         R.ok(o)
      }
//更多的处理方式
       return o;
  }
}

这个的使用方式就是;在cotroller想怎么写就怎么写,在MyResponseAdvice会自动封装.

配合全局异常处理需要做的地方

详见指定文章的指定两个部分:

1.拦截404或者服务器错误等未进入controller的异常

2.filter等自定义代码中异常

1
2
3
4
arduino复制代码    作者:ZOUZDC
链接:https://juejin.cn/post/7028963866063306760
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本文转载自: 掘金

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

使用 Prometheus 采集指标数据 什么是 Prome

发表于 2021-11-12

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

什么是 Prometheus?

Prometheus 是系统监控和告警工具包,许多公司和组织都采用了 Prometheus 作为监控告警工具。Prometheus 的开发者和用户社区非常活跃,它现在是一个独立的开源项目,可以独立于任何公司进行维护。

数据模型

Prometheus 所有采集的监控数据均以指标(metric)的形式保存在内置的时间序列数据库当中(TSDB)。

每一条时间序列由指标名称(Metrics Name)以及一组标签(键值对)唯一标识。其中指标的名称(metric name)可以反映被监控样本的含义(例如,http_requests_total — 表示当前系统接收到的 HTTP 请求总量),指标名称只能由 ASCII 字符、数字、下划线以及冒号组成,同时必须匹配正则表达式 [a-zA-Z_:][a-zA-Z0-9_:]*。

例如,指标名称为 api_http_requests_total,标签为 method="POST" 和 handler="/messages" 的时间序列可以表示为:

1
ini复制代码api_http_requests_total{method="POST", handler="/messages"}

指标类型

Counter(计数器)

Counter 类型代表一种样本数据单调递增(f(x) 随着 x 的增大而增大)的指标,即只增不减,除非监控系统发生了重置。例如,你可以使用 counter 类型的指标来表示服务的请求数、已完成的任务数、错误发生的次数等。counter 主要有两个方法:

1
2
3
4
scss复制代码//将counter值加1.
Inc()
// 将指定值加到counter值上,如果指定值<0 会panic.
Add(float64)

Counter 类型数据可以让用户方便的了解事件产生的速率的变化,在 PromQL 内置的相关操作函数可以提供相应的分析,比如以 HTTP 应用请求量来进行说明:

1
2
3
4
scss复制代码//通过rate()函数获取HTTP请求量的增长率
rate(http_requests_total[5m])
//查询当前系统中,访问量前10的HTTP地址
topk(10, http_requests_total)

Java 客户端文档

Guage(仪表盘)

Guage 类型代表一种样本数据可以任意变化的指标,即可增可减。guage 通常用于像温度或者内存使用率这种指标数据,也可以表示能随时增加或减少的“总数”,例如:当前并发请求的数量。

对于 Gauge 类型的监控指标,通过 PromQL 内置函数 delta() 可以获取样本在一段时间内的变化情况,例如,计算 CPU 温度在两小时内的差异:

1
ini复制代码dalta(cpu_temp_celsius{host="zeus"}[2h])

你还可以通过PromQL 内置函数 predict_linear() 基于简单线性回归的方式,对样本数据的变化趋势做出预测。例如,基于 2 小时的样本数据,来预测主机可用磁盘空间在 4 个小时之后的剩余情况:

1
ini复制代码predict_linear(node_filesystem_free{job="node"}[2h], 4 * 3600) < 0

Java 客户端文档

Histogram(直方图)

在大多数情况下人们都倾向于使用某些量化指标的平均值,例如 CPU 的平均使用率、页面的平均响应时间。这种方式的问题很明显,以系统 API 调用的平均响应时间为例:如果大多数 API 请求都维持在 100ms 的响应时间范围内,而个别请求的响应时间需要 5s,那么就会导致某些 WEB 页面的响应时间落到中位数的情况,而这种现象被称为长尾问题。

为了区分是平均的慢还是长尾的慢,最简单的方式就是按照请求延迟的范围进行分组。例如,统计延迟在 0-10ms 之间的请求数有多少而 10-20ms 之间的请求数又有多少。通过这种方式可以快速分析系统慢的原因。Histogram 和 Summary 都是为了能够解决这样问题的存在,通过 Histogram 和 Summary 类型的监控指标,我们可以快速了解监控样本的分布情况。

Histogram 在一段时间范围内对数据进行采样(通常是请求持续时间或响应大小等),并将其计入可配置的存储桶(bucket)中,后续可通过指定区间筛选样本,也可以统计样本总数,最后一般将数据展示为直方图。

Histogram 类型的样本会提供三种指标(假设指标名称为 <basename>):

  • 样本的值分布在 bucket 中的数量,命名为 <basename>_bucket{le="<上边界>"}。解释的更通俗易懂一点,这个值表示指标值小于等于上边界的所有样本数量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码  // 在总共2次请求当中。http 请求响应时间 <=0.005 秒 的请求次数为0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.005",} 0.0
// 在总共2次请求当中。http 请求响应时间 <=0.01 秒 的请求次数为0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.01",} 0.0
// 在总共2次请求当中。http 请求响应时间 <=0.025 秒 的请求次数为0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.025",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.05",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.075",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.1",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.25",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.5",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.75",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="1.0",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="2.5",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="5.0",} 0.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="7.5",} 2.0
// 在总共2次请求当中。http 请求响应时间 <=10 秒 的请求次数为 2
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="10.0",} 2.0
io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="+Inf",} 2.0
  • 所有样本值的大小总和,命名为 <basename>_sum。
1
2
ini复制代码  // 实际含义: 发生的2次 http 请求总的响应时间为 13.107670803000001 秒
io_namespace_http_requests_latency_seconds_histogram_sum{path="/",method="GET",code="200",} 13.107670803000001
  • 样本总数,命名为 <basename>_count。值和 <basename>_bucket{le="+Inf"} 相同。
1
2
ini复制代码  // 实际含义: 当前一共发生了 2 次 http 请求
io_namespace_http_requests_latency_seconds_histogram_count{path="/",method="GET",code="200",} 2.0

Java 客户端文档

Summary(摘要)

与 Histogram 类型类似,用于表示一段时间内的数据采样结果(通常是请求持续时间或响应大小等),但它直接存储了分位数(通过客户端计算,然后展示出来),而不是通过区间来计算。

Summary 类型的样本也会提供三种指标(假设指标名称为 ):

  • 样本值的分位数分布情况,命名为 <basename>{quantile="<φ>"}。
1
2
3
4
ini复制代码  // 含义:这 12 次 http 请求中有 50% 的请求响应时间是 3.052404983s
io_namespace_http_requests_latency_seconds_summary{path="/",method="GET",code="200",quantile="0.5",} 3.052404983
// 含义:这 12 次 http 请求中有 90% 的请求响应时间是 8.003261666s
io_namespace_http_requests_latency_seconds_summary{path="/",method="GET",code="200",quantile="0.9",} 8.003261666
  • 所有样本值的大小总和,命名为 <basename>_sum。
1
2
ini复制代码  // 含义:这12次 http 请求的总响应时间为 51.029495508s
io_namespace_http_requests_latency_seconds_summary_sum{path="/",method="GET",code="200",} 51.029495508
  • 样本总数,命名为 <basename>_count。
1
2
ini复制代码  // 含义:当前一共发生了 12 次 http 请求
io_namespace_http_requests_latency_seconds_summary_count{path="/",method="GET",code="200",} 12.0

现在可以总结一下 Histogram 与 Summary 的异同:

  • 它们都包含了 <basename>_sum 和 <basename>_count 指标。
  • Histogram 需要通过 <basename>_bucket 来计算分位数,而 Summary 则直接存储了分位数的值。

关于 Summary 与 Histogram 的详细用法,请参考 histograms and summaries。

Java 客户端文档

安装及使用

示例配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
yaml复制代码global:
scrape_interval: 15s # By default, scrape targets every 15 seconds.

# Attach these labels to any time series or alerts when communicating with
# external systems (federation, remote storage, Alertmanager).
external_labels:
monitor: 'codelab-monitor'

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: 'prometheus'

# Override the global default and scrape targets from this job every 5 seconds.
scrape_interval: 5s

static_configs:
- targets: ['localhost:9090']

完整配置选项说明,请查看配置文档

使用以下脚本启动 Prometheus:

1
2
3
4
5
6
7
bash复制代码#!/bin/bash
docker kill prometheus
docker rm prometheus
docker run -d -p 9090:9090 \
-v /tmp/prometheus.yml:/etc/prometheus/prometheus.yml \
--name prometheus
prom/prometheus:v2.15.2

本文转载自: 掘金

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

幂等解决方案集合(一)

发表于 2021-11-12

什么是幂等(idempotent)

百度百科:

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。

在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。

这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的。更复杂的操作幂等保证是利用唯一交易号(流水号)实现。

幂等的数学概念

幂等是源于一种数学概念。其主要有两个定义

如果在一元运算中,x 为某集合中的任意数,如果满足 f(f(x))=f(x)f(f(x))=f(x) f(f(x))=f(x) ,那么该 f 运算具有幂等性,比如绝对值运算 abs(a)=abs(abs(a))abs(a) = abs(abs(a)) abs(a)=abs(abs(a)) 就是幂等性函数。

如果在二元运算中,x 为某集合中的任意数,如果满足 f(x,x)=xf(x,x) = xf(x,x)=x,前提是 f 运算的两个参数均为 x,那么我们称 f 运算也有幂等性,比如求大值函数 max(x,x)=xmax(x,x) =x max(x,x)=x 就是幂等性函数。

幂等的业务概念

幂等性不仅仅只是一次或多次操作对资源没有产生影响,还包括第一次操作产生影响后,以后多次操作不会再产生影响。并且幂等关注的是是否对资源产生影响,而不关注结果。

举例:服务端会进行重试等操作或客户端有可能会进行多次点击提交。如果这样请求多次的话,那最终处理的数据结果就一定要保证统一,如支付场景。此时就需要通过保证业务幂等性方案来完成。

幂等的维度

  • 时间
  • 空间

时域唯一性

定义幂等的有效期。有些业务需要永久性保证幂等,如下单、支付等。而部分业务只要保证一段时间幂等即可。
你希望在多长时间内保证某次操作的幂等?

空域唯一性

定义了幂等的范围,如生成订单的话,不允许出现重复下单。
一次操作=服务方法+传入的业务数据

同时对于幂等的使用一般都会伴随着出现锁的概念,用于解决并发安全问题。

HTTP 协议语义幂等性

引用自:

  • Http/1.1 文档 www.w3.org/Protocols/r…
  • zh.wikipedia.org/wiki/%E8%B6…

安全方法
对于 GET 和 HEAD 方法而言,除了进行获取资源信息外,这些请求不应当再有其他意义。也就是说,这些方法应当被认为是“安全的”。 客户端可能会使用其他“非安全”方法,例如 POST,PUT 及 DELETE,应该以特殊的方式(通常是按钮而不是超链接)告知客户可能的后果(例如一个按钮控制的资金交易),或请求的操作可能是不安全的(例如某个文件将被上传或删除)。但是,不能想当然地认为服务器在处理某个 GET 请求时不会产生任何副作用。事实上,很多动态资源会把这作为其特性。这里重要的区别在于用户并没有请求这一副作用,因此不应由用户为这些副作用承担责任。
副作用
假如在不考虑诸如错误或者过期等问题的情况下,若干次请求的副作用与单次请求相同或者根本没有副作用,那么这些请求方法就能够被视作“幂等 (idempotence)”的。GET,HEAD,PUT 和 DELETE 方法都有这样的幂等属性,同样由于根据协议,OPTIONS,TRACE 都不应有副作用,因此也理所当然也是幂等的。
假如一个由若干请求组成的请求序列产生的结果,在重复执行这个请求序列或者其中任何一个或多个请求后仍没有发生变化,则这个请求序列便是“幂等”的。但是,可能出现一个由若干请求组成的请求序列是“非幂等”的,即使这个请求序列中所有执行的请求方法都是幂等的。例如,这个请求序列的结果依赖于某个会在下次执行这个序列的过程中被修改的变量。

总结下:

HTTP Method Idempotent Safe
OPTIONS yes yes
GET yes yes
HEAD yes yes
PUT yes no
POST no no
DELETE yes no
PATCH no no

常见幂等问题

业务上

  • 当用户购物进行下单操作,用户 操作多次,但订单系统对于本次操作只能产生一个订单(不控制会导致恶意刷单)。
  • 当用户对订单进行付款,支付系统不管出现什么问题,应该只对用户扣一次款。
  • 当支付成功对库存扣减时,库存系统对订单中商品的库存数量也只能扣减一次。
  • 当对商品进行发货时,也需保证物流系统有且只能发一次货。

技术上

  • 前端重复提交表单,导致同一条数据重复提交。
  • 当添加重试机制,一个请求重试多次,导致数据不一致。
  • 当使用 MQ 消息中间件时候,如果发生消息中间件出现错误未及时提交消费信息,导致发生重复消费。

前端解决方案

前端防重

通过前端防重保证幂等是最简单的实现方式,前端相关属性和 JS 代码即可完成设置。可靠性并不好,有经验的人员可以通过工具跳过页面仍能重复提交。主要适用于表单重复提交或按钮重复点击。

PRG 模式

PRG 模式即 POST-REDIRECT-GET。当用户进行表单提交时,会重定向到另外一个提交成功页面,而不是停留在原先的表单页面。这样就避免了用户刷新导致重复提交。同时防止了通过浏览器按钮前进/后退导致表单重复提交。是一种比较常见的前端防重策略。

Token 模式

token 模式主要是为了防重的。

需要前后端进行一定程度的交互来完成。需要利用到 Redis。

具体流程步骤:

  1. 客户端会先发送一个请求去获取 token,服务端会生成一个全局唯一的 ID 作为 token 保存在 redis 中,同时把这个 ID 返回给客户端
  2. 客户端第二次调用业务请求的时候必须携带这个 token
  3. 服务端会校验这个 token,如果校验成功,则执行业务,并删除 redis 中的 token
  4. 如果校验失败,说明 redis 中已经没有对应的 token,则表示重复操作,直接返回指定的结果给客户端

注意:

  • 在并发情况下,执行 Redis 查找数据与删除需要保证原子性,否则很可能在并发下无法保证幂等性。其实现方法可以使用分布式锁或者使用 Lua 表达式来注销查询与删除操作。
  • 全局唯一 ID 可以用百度的 uid-generator、美团的 Leaf 去生成

Redis 分布式锁 可以利用:典型的实现 setnx + getset 或 Redisson

这里给出两种实现的核心方法,首先是用 Redisson 的:

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
java复制代码/**
* 创建 Token 存入 Redis,并返回该 Token
* @param value 用于辅助验证的 value 值
* @return 生成的 Token 串
*/
public String generateToken(String value) {

String token = UUID.randomUUID().toString();
String key = IDEMPOTENT_TOKEN_PREFIX + token;
/**
* 在真实业务中 采用唯一标志 例如 流水号啊
*/
redisTemplate.opsForValue().set(key, value, 5, TimeUnit.MINUTES);
return token;
}

/**
* 分布式锁实现幂等性
*/
@PostMapping("/distributeLock")
@ApiOperation(value = "分布式锁实现幂等性")
public String distributeLock(HttpServletRequest request) {

String token = request.getHeader("token");
// 获取用户信息(这里使用模拟数据)
String userInfo = "mydlq";
RLock lock = redissonClient.getLock(token);
lock.lock(10, TimeUnit.SECONDS);
try {

Boolean flag = tokenUtilService.validToken2(token, userInfo);
// 根据验证结果响应不同信息
if (flag) {

/**
* 执行正常的逻辑
*/
log.info("执行正常的逻辑………………");
}
return flag ? "正常调用" : "重复调用";
} catch (Exception e) {

e.printStackTrace();
return "重复调用";
} finally {

lock.unlock();
}
}

然后是带 Lua 脚本的

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复制代码  /**
* 验证 Token 正确性
*
* @param token token 字符串
* @param value value 存储在 Redis 中的辅助验证信息
* @return 验证结果
*/
public Boolean validToken(String token, String value) {

// 设置 Lua 脚本,其中 KEYS[1] 是 key,KEYS[2] 是 value, 这段 lua 脚本的意思是获取 redis 的 KEYS[1] 的值,与 KEYS[2] 的值作比较,如果相等则返回 KEYS[1] 的值并删除 redis 中的 KEYS[1], 否则返回 0
String script = "if redis.call('get',KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
// 根据 Key 前缀拼接 Key
String key = IDEMPOTENT_TOKEN_PREFIX + token;
// 执行 Lua 脚本
Long result = redisTemplate.execute(redisScript, Arrays.asList(key, value));
// 根据返回结果判断是否成功匹配并删除 Redis 键值对,若果结果不为空和 0,则验证通过
if (result != null && result != 0L) {

log.info("验证 token={},key={},value={} 成功", token, key, value);
return true;
}
log.info("验证 token={},key={},value={} 失败", token, key, value);

return false;
}

Lua 脚本的比较好理解,这里说两句 Redisson 的,Redisson 的锁 在调用redissonClient.getLock(“myLockKey”) 时,redis 中不能存在同名的 key, 不然会报错。redisson 锁其内部是基于 lua 脚本语言完成锁获取的。因为获取锁的过程涉及到了多步,为了保证执行过程的原子性,使用了 Lua 脚本。

具体就是这段:

1
2
3
java复制代码<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)});
}

关于 Redisson 锁还有几个进阶的概念比如:“红锁”,感兴趣的朋友可以看一下:redis.cn/topics/dist…

后端解决方案

利用数据库实现的方案

去重表

去重表的实现思路也非常简单,首先创建一张表作为去重表,同时在该表中建立一个或多个字段的唯一索引作为防重字段,用于保证并发情况下,数据只有一条。在向业务表中插入数据之前先向去重表插入,如果插入失败则表示是重复数据。

比如:同一个用户同一件商品不能在同一分钟下两次单,那么就需要 user_id, product_id, created_at 这三个字段做为去重字段。

唯一主键 insert、delete 场景

可以通过设置数据库的唯一主键约束或唯一索引约束来实现,这样重复的 key 就不会插入成功了。比如你用 userId 为 1 的数据插入,第一次成功了再重试一次就不会成功了。

这个方案可以防止新增脏数据,具有防重效果。

防重设计 和 幂等设计,是有区别的。防重设计主要为了避免产生重复数据,对接口返回没有太多要求。而幂等设计除了避免产生重复数据之外,还要求每次请求都返回一样的结果

乐观锁 update 场景

数据库乐观锁方案一般只能适用于执行更新操作的过程,我们可以提前在对应的数据表中多添加一个字段,充当当前数据的版本标识。

基本思路是:版本号+条件 ,实现思想是基于 MySQL 的行锁思想来实现的。
以订单扣减库存举例:

如果我们只以版本号作为条件更新

1
sql复制代码update tb_stock set amount=amount-#{num},version=version+1 where goods_id=#{goodsId} and version=#{version}

那么同时下单的用户就只有一个能扣减库存成功,其他的都失败。这种情况我们可以再加入一个条件来判断比如:

1
sql复制代码update tb_stock set amount=amount-#{num} where goods_id=#{goodsId} and amount-#{num}>=0

只要不发生超卖就可以了。

举一反三,也可以用在订单只支付一次的场景,只不过条件不同罢了(也有称这种方法为状态标识或状态机幂等),比如:

1
sql复制代码update table item set item.status=:newstatus where item.id = :id and item.status = oldstatus

当然这也仅仅是保证了接口的幂等性,放在真实的分布式环境里,服务间的调用很可能会涉及分布式事务,那么还需要在幂等的基础上加分布式事务的解决方案,比如 seata, 有关分布式事务由于不属于本文的重点就不多讨论了。但幂等是一个很重要基础,它能够保证业务数据的一致性。

为什么不用悲观锁?

首先悲观锁有可能会锁表,有性能问题。

比如 select for update

由于 InnoDB 预设是 Row-Level Lock,所以只有「明确」的指定主键,MySQL 才会执行 Row lock (行锁) ,否则 MySQL 将会执行 Table Locck(锁表)Lock

其次使用悲观锁有可能产生死锁

比如:一个用户 A 访问表 A(锁住了表 A),然后试图访问表 B;另一个用户 B 访问表 B(锁住了表 B),然后试图访问表 A。 这时对于用户 A 来说,由于表 B 已经被用户 B 锁住了,所以用户 A 必须等到用户 B 释放表 B 才能访问。同时对于用户 B 来说,由于表 A 已经被用户 A 锁住了,所以用户 B 必须等到用户 A 释放表 A 才能访问。此时死锁就已经产生了。

redis 分布式锁实现

具体流程步骤:

  1. 客户端先请求服务端,会拿到一个能代表这次请求业务的唯一字段
  2. 将该字段以 SETNX 的方式存入 redis 中,并根据业务设置相应的超时时间
  3. 如果设置成功,证明这是第一次请求,则执行后续的业务逻辑
  4. 如果设置失败,则代表已经执行过当前请求,直接返回

整体来看,与上文的 token 方案中使用 redis 类似,基本还是 分布式 ID+分布式锁。

比较适合于服务间调用时的接口幂等,比如订单调库存,订单调用支付等。

比如订单先生成了一个 id 标识(比如订单号),id 标识可由分布式 ID 生成器生成,然后带着这个标识一起请求库存,如果之前没有在 redis 存过则正常执行,如果发生接口重试,再次用相同 id 标识请求,redis 返回失败表示重复请求。

这里比较重要的是要记得设置 redis 的过期时间,具体时间要根据

  • 业务执行时间
  • 上游系统或整个系统整体的重试次数以及时间设置

假设我们设置为 2 秒,那么套一下业务就是:比如同一个订单(id 标识是订单号),在 2 秒内执行了 2 次以上扣减库存,很明显是一个重复操作,需要幂等处理(返回失败表示重复请求)。

如果不考虑重试次数和时间,一旦 redis key 超时失效,调用方服务再次重试还是无法保证幂等。

消息幂等

在接收消息的时候,消息推送重复。如果处理消息的接口无法保证幂等,那么重复消费消息产生的影响可能会非常大。

具体解决方案请看下回分解。

参考

  • www.bianchengquan.com/article/133…
  • baike.baidu.com/item/%E5%B9…
  • mp.weixin.qq.com/s/vsvfnj5RL…
  • mp.weixin.qq.com/s/xq2ks76hT…

本文转载自: 掘金

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

ThreadPoolExecutor 的参数含义及源码执行流

发表于 2021-11-12

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

线程池是为了避免线程频繁的创建和销毁带来的性能消耗,而建立的一种池化技术,它是把已创建的线程放入“池”中,当有任务来临时就可以重用已有的线程,无需等待创建的过程,这样就可以有效提高程序的响应速度。但如果要说线程池的话一定离不开**ThreadPoolExecutor,

说明:Executors 返回的线程池对象的弊端如下:

  • 1)FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM(Out Of Memory)
  • 2)CachedThreadPool 和 ScheduledThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

其实当我们去看**Executors的源码会发现,Executors.newFixedThreadPool()** 、Executors.newSingleThreadExecutor() 和 Executors.newCachedThreadPool() 等方法的底层都是通过 ThreadPoolExecutor 实现的。

过程

ThreadPoolExecutor 的核心参数指的是它在构建时需要传递的参数,其构造方法如下所示:

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复制代码 public ThreadPoolExecutor(int corePoolSize,
                             int maximumPoolSize,
                             long keepAliveTime,
                             TimeUnit unit,
                             BlockingQueue<Runnable> workQueue,
                             ThreadFactory threadFactory,
                             RejectedExecutionHandler handler) {
       if (corePoolSize < 0 ||
           //maximumPoolSize必须大于0,且必须大于corePoolSize
           maximumPoolSize <= 0 ||
           maximumPoolSize < corePoolSize ||
           keepAliveTime < 0)
           throw new IllegalArgumentException();
       if (workQueue == null || threadFactory == null || handler == null)
           throw new NullPointerException();
       this.acc = System.getSecurityManager() == null ?
               null :
               AccessController.getContext();
       this.corePoolSize = corePoolSize;
       this.maximumPoolSize = maximumPoolSize;
       this.workQueue = workQueue;
       this.keepAliveTime = unit.toNanos(keepAliveTime);
       this.threadFactory = threadFactory;
       this.handler = handler;
  }
  • 第1个参数:**corePoolSize**表示线程池的常驻核心线程数。如果设置为0,则表示在没有任何任务时,销毁线程池;如果大于0,即使没有任务时也会保证线程池的线程数量等于此值。但需要注意,此值如果设置的比较小,则会频繁的创建和销毁线程;如果设置的比较大,则会浪费系统资源,所以开发者需要根据自己的实际业务来调整此值。
  • 第2个参数:maximumPoolSize表示线程池在任务最多时,最大可以创建的线程数。官方规定此值必须大于0,也必须大于等于corePoolSize,此值只有在任务比较多,且不能存放在任务队列时,才会用到。
  • 第3个参数:keepAliveTime表示线程的存活时间,当线程池空闲时并且超过了此时间,多余的线程就会销毁,直到线程池中的线程数量销毁的等于corePoolSize为止,如果maximumPoolSize等于corePoolSize,那么线程池在空闲的时候也不会销毁任何线程。
  • 第4个参数:unit 表示存活时间的单位,它是配合**keepAliveTime**参数共同使用的。
  • 第5个参数:**workQueue**表示线程池执行的任务队列,当线程池的所有线程都在处理任务时,如果来了新任务就会缓存到此任务队列中排队等待执行。
  • 第6个参数:**threadFactory**表示线程的创建工厂,此参数一般用的比较少,我们通常在创建线程池时不指定此参数,它会使用默认的线程创建工厂的方法来创建线程,源代码如下:
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
java复制代码  public ThreadPoolExecutor(int corePoolSize,
                             int maximumPoolSize,
                             long keepAliveTime,
                             TimeUnit unit,
                             BlockingQueue<Runnable> workQueue,
                             ThreadFactory threadFactory) {
       this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
            threadFactory, defaultHandler);
  }
​
public static ThreadFactory defaultThreadFactory() {
       return new DefaultThreadFactory();
  }
​
// 默认的线程创建工厂,需要实现 ThreadFactory 接口
static class DefaultThreadFactory implements ThreadFactory {
       private static final AtomicInteger poolNumber = new AtomicInteger(1);
       private final ThreadGroup group;
       private final AtomicInteger threadNumber = new AtomicInteger(1);
       private final String namePrefix;
​
       DefaultThreadFactory() {
           SecurityManager s = System.getSecurityManager();
           group = (s != null) ? s.getThreadGroup() :
                                 Thread.currentThread().getThreadGroup();
           namePrefix = "pool-" +
                         poolNumber.getAndIncrement() +
                        "-thread-";
      }
​
  // 创建线程
       public Thread newThread(Runnable r) {
           Thread t = new Thread(group, r,
                                 namePrefix + threadNumber.getAndIncrement(),
                                 0);
           if (t.isDaemon())
               //创建一个非守护线程
               t.setDaemon(false);
           if (t.getPriority() != Thread.NORM_PRIORITY)
               //线程优先级设置为默认值
               t.setPriority(Thread.NORM_PRIORITY);
           return t;
      }
  }

我们也可以自定义一个线程工厂,通过实现**ThreadFactory**接口来完成,这样就可以自定义线程的名称或线程执行的优先级了。

  • 第7个参数:RejectedExecutionHandler 表示指定线程池的拒绝策略,当线程池的任务已经在缓存队列**workQueue中存储满了之后,并且不能创建新的线程来执行此任务时,就会用到此拒绝策略,它属于一种限流保护的机制。线程池的工作流程要从它的执行方法execute()** 说起,源码如下:
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
java复制代码public void execute(Runnable command) {
       if (command == null)
           throw new NullPointerException();
       /*
          *分3步进行:
          *
          * 1.如果少于正在运行的corePoolSize线程,请尝试
          *以给定的命令作为第一个线程启动一个新线程
          *任务。 对addWorker的调用自动检查runState和
          * workerCount,因此可以防止假警报的增加
          *通过返回false返回不应该执行的线程。
          *
          * 2.如果任务可以成功排队,那么我们仍然需要
          *仔细检查我们是否应该添加线程
          *(因为现有的自上次检查后死亡)或
          *自从进入此方法以来,该池已关闭。 所以我们
          *重新检查状态,并在必要时回退排队
          *停止,如果没有,则启动一个新线程。
          *
          * 3.如果我们无法将任务排队,那么我们尝试添加一个新的
          *线程。 如果失败,我们知道我们已经关闭或饱和
          *并因此拒绝任务。
          */
       int c = ctl.get();
  // 当前工作的线程小于核心线程数
       if (workerCountOf(c) < corePoolSize) {
           //创建新的线程执行次任务
           if (addWorker(command, true))
               return;
           c = ctl.get();
      }
  // 检查线程池是否处于可运行状态,如果是则把任务添加到队列
       if (isRunning(c) && workQueue.offer(command)) {
           int recheck = ctl.get();
           //再次检查线程池是否处于可运行状态,防止第一次校验通过后线程池关闭
           //如果是非运行状态,则把刚加入队列的任务移除
           if (! isRunning(recheck) && remove(command))
               reject(command);
           //如果线程池的线程数为0时(当corePoolSize设置为0时会发生)
           else if (workerCountOf(recheck) == 0)
               //新建线程执行任务
               addWorker(null, false);
      }
  //核心线程都在忙且队列都已爆满,尝试新启动一个线程执行失败
       else if (!addWorker(command, false))
           //执行拒绝策略
           reject(command);
  }

其中**addWorker(Runnable firstTask,boolean core)** 方法参数说明:

+ **`firstTask`**:线程首先执行的任务,如果没有则设置为null
+ **core**:判断是否可以创建线程的阈值(最大值),如果等于true则表示使用\*\*`corePoolSize`**作为阈值,false则表示使用**`maximumPoolSize`\*\*作为阈值

扩展

execute()和submit()

execute()和submit()都是用来执行线程任务的,他们最主要的区别是,submit()能接受线程池执行的返回值,execute()不能接受返回值。

两个方法的具体使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public static void main(String[] args) throws ExecutionException, InterruptedException {
       ThreadPoolExecutor execute = new ThreadPoolExecutor(2,10,10L, TimeUnit.SECONDS,new LinkedBlockingQueue<>(20));
       //execute使用
       execute.execute(new Runnable() {
           @Override
           public void run() {
               System.out.println("Hello,execute");
          }
      });
       //submit使用
       Future<String> future = execute.submit(new Callable<String>() {
           @Override
           public String call() throws Exception {
               System.out.println("Hello,submit");
               return "Success";
          }
      });
       System.out.println(future.get());
  }

程序执行结果:

1
2
3
lua复制代码Hello,execute
Hello,submit
Success

从以上结果可以看出submit()方法可以配合Futrue来接收线程执行的返回值。它们的另一个区别是execute()方法属于Executor接口的方法,而 submit() 方法则是属于 ExecutorService 接口的方法

  • 线程池拒绝策略

当线程池中的任务队列已经被存满,再有任务添加时会先判断当前线程池中的线程数是否大于等于线程池的最大值,如果是,则会触发线程池的拒绝策略。

Java自带的拒绝策略有四种:

  • AbortPolicy,终止策略,线程池会抛出异常并终止执行,它是默认的拒绝策略;
  • CallerRunsPolicy,把任务交给当前线程来执行
  • DiscardPolicy,忽略此任务(最新的任务)
  • DiscardOldestPolicy,忽略最早的任务(最先加入队列的任务)

AbortPolicy拒绝策略示例:

1
2
3
4
5
6
java复制代码ThreadPoolExecutor executor = new ThreadPoolExecutor(1,3,10,TimeUnit.SECONDS,new LinkedBlockingQueue<>(2),new ThreadPoolExecutor.AbortPolicy());
       for (int i=0;i<6;i++){
           executor.execute(()->{
               System.out.println(Thread.currentThread().getName());
          });
      }

程序运行结果:

1
2
3
4
5
6
7
8
9
10
arduino复制代码pool-1-thread-1
pool-1-thread-2
pool-1-thread-2
pool-1-thread-2
pool-1-thread-3
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task example.ThreadPoolExecutorExample$$Lambda$1/1149319664@4dd8dc3 rejected from java.util.concurrent.ThreadPoolExecutor@6d03e736[Running, pool size = 3, active threads = 3, queued tasks = 2, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at example.ThreadPoolExecutorExample.main(ThreadPoolExecutorExample.java:34)

可以看出当第6个任务来的时候,线程池则执行了AbortPolicy 拒绝策略,抛出了异常。因为队列最多存储2个任务,最大可以创建3个线程来执行任务(2+3=5),所以当第6个任务来的时候,此线程池就“忙”不过来了。

自定义拒绝策略

自定义拒绝策略只需要新建一个RejectedExecutionHandler对象,然后重写它的rejectedExecution()方法即可,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 3, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2),
               //添加自定义拒绝策略
               new RejectedExecutionHandler() {
           @Override
           public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
               //业务执行方法
               System.out.println("执行自定义拒绝策略");
          }
      });
       for (int i=0;i<6;i++){
           executor.execute(()->{
               System.out.println(Thread.currentThread().getName());
          });
      }

程序执行结果:

1
2
3
4
5
6
arduino复制代码执行自定义拒绝策略
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-2
pool-1-thread-3

可以看出线程池执行了自定义的拒绝策略,我们可以在rejectedExecution中添加自己业务处理的代码。

ThreadPoolExecutor扩展

ThreadPoolExecutor的扩展主要是通过重写它的beforeExecute()和afterExecute()方法实现的,我们可以在扩展方法中添加日志或者实现数据统计,比如统计线程的执行时间,如下代码所示:

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
java复制代码public class ThreadPoolExtend {
   public static void main(String[] args) {
       //线程池扩展调用
       MyThreadPoolExtend executor = new MyThreadPoolExtend(2,4,10, TimeUnit.SECONDS,new LinkedBlockingQueue<>());
       for (int i=0;i<3;i++){
           executor.execute(()->{
               System.out.println(Thread.currentThread().getName());
          });
      }
  }
​
   /**
    * 线程池扩展
    */
   static class MyThreadPoolExtend extends ThreadPoolExecutor{
       //保存线程开始执行的时间
       private final ThreadLocal<Long> localTime = new ThreadLocal<>();
       public MyThreadPoolExtend(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
           super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
      }
​
       /**
        * 开始执行之前
        * @param t 线程
        * @param r 任务
        */
       @Override
       protected void beforeExecute(Thread t, Runnable r) {
           //开始时间(单位:纳秒)
           Long sTime = System.nanoTime();
           localTime.set(sTime);
           System.out.println(String.format("%s|before|time=%s",t.getName(),sTime));
           super.beforeExecute(t, r);
      }
​
       /**
        * 执行 完成之后
        * @param r 任务
        * @param t 抛出的遗产
        */
       @Override
       protected void afterExecute(Runnable r, Throwable t) {
           //结束时间(单位:纳秒)
           long eTime = System.nanoTime();
           long totalTime = eTime - localTime.get();
           System.out.println(String.format("%s|after|time=%s|耗时:%s毫秒",Thread.currentThread().getName(),eTime,totalTime/1000000.0));
           super.afterExecute(r, t);
      }
  }
}

程序执行结果:

1
2
3
4
5
6
7
8
9
ini复制代码pool-1-thread-1|before|time=95659085427600
pool-1-thread-1
pool-1-thread-2|before|time=95659085423300
pool-1-thread-2
pool-1-thread-2|after|time=95659113130200|耗时:27.7069毫秒
pool-1-thread-1|after|time=95659112193700|耗时:26.7661毫秒
pool-1-thread-2|before|time=95659117635200
pool-1-thread-2
pool-1-thread-2|after|time=95659117822000|耗时:0.1868毫秒

小结

线程池的使用必须要通过 ThreadPoolExecutor 的方式来创建,这样才可以更加明确线程池的运行规则,规避资源耗尽的风险。同时,也介绍了 ThreadPoolExecutor 的七大核心参数,包括核心线程数和最大线程数之间的区别,当线程池的任务队列没有可用空间且线程池的线程数量已经达到了最大线程数时,则会执行拒绝策略,Java 自动的拒绝策略有 4 种,用户也可以通过重写 rejectedExecution() 来自定义拒绝策略,还可以通过重写 beforeExecute() 和 afterExecute() 来实现 ThreadPoolExecutor 的扩展功能。

本文转载自: 掘金

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

这个功能应该谁来做?

发表于 2021-11-12

跨部门开发,总会有一些参数转换逻辑会涉及双边的领域逻辑,这些转换逻辑到底应该谁来做呢?下面使用一个简单的实例来演示下争议的过程,并给出一定的思考。

  1. 争议举例需求背景

用户购买「买一赠一」两件商品,「促销部门」关心分别独立的两个商品(不同商品价格和促销不一样),「库存部门」不关心买一赠一,告诉我扣减两件库存就可以,「结算部门」调用「库存部门」的接口到底应不应该把「买一赠一」合并商品数量为二呢?

  1. 方案一:结算作为「胶水层」

  • 库存域(部门):认为自己提供API,使用方根据自己API来拼接参数(库存不关心主赠,只关心数量,按照库存的标准来合并数量)
  • 促销域(部门):认为自己提供API,使用方根据自己API来拼接参数(促销关心主赠,按照促销的标准不合并数量)
  • 结算页部门:大家都认为结算是胶水层,理所应当衔接每个交易子域,结算部门调用每个系统前都需要根据需要加工参数,结算页需要学习各个子域的接口隐含知识。

胶水层示例.png

  1. 结算作为胶水整体架构

整体形成每个子域很舒服,是大爷;结算页需要学习每个子域的不同,进行包装,并且每个子域的变化,结算页都需要进行响应。

胶水层架构.png

  1. 方案二:结算页是核心业务子域,对等架构

如果结算域认为自己和库存一样也是核心业务域,认为自己不应该耦合其他域呢?

  • 结算页制定「通用模型标准」,传递每个交易子域都是「通用模型」,每个子域的变化波及结算的概率变低。
  • 每个子域比如「库存」,「促销」自己把结算页的「通用参数」转换为自己需要的格式。
  • 每个子域比如「库存」,「促销」需要学习结算的「通用的模型标准」。

对等架构.png

5.方案二应用到示例

上面的示例,改变为结算页传递库存和促销都是两个苹果,库存和促销自己决定要不要合并。

  • 更加灵活:如果库存出现同一个商品不需要合并的场景,或者需要部分合并的场景,结算页不会感知。

对等示例.png

6.方案对比

方案一:简单,直接,原始。

  • 胶水和核心业务域界限分明,变化集中在胶水层。
  • 伴随业务复杂度提升,胶水层职责会越来越多,会引起胶水层的复杂度升高难以维护。

方案二:复杂,灵活。

  • 管理沟通难度上升:胶水层升级业务域,对等架构,对等架构要求不同部门认可「通用模型」。
  • 应对复杂业务整体更稳定:逻辑集中在具体子域,每个子域针对需求分别做出响应更专业和内聚。
  • 难点:不同业务域的「通用模型」的合理性。

7.方案二中「通用模型」的悖论与思考

通用模型与子域划分相悖

划分不同子域的初衷是为了不同子域可以独立设计「子域专有模型」,彼此互不影响,从这个角度看,子域划分是反对「通用模型」的,事实上并不存在放在哪里都适用的通用模型,这一点需要说明白。

局部通用模型

那么方案二的「通用模型」到底是什么?虽然不存在哪里都通用的模型,如果我们加个范围,交易的几个子域呢(不算财务等相差太大的子域)?是不是就有可能存在交易的几个子域可以一定程度上复用的「通用模型」呢?

如下图:更近一步,可以在「通用模型」的基础上,适当改造,消除每个子域必然不关心的属性,形成介于「通用模型」和「专有模型」之间的适合沟通的模型,产生每个子域的通用模型(比如「库存通用模型」,「促销通用模型」)

通用模型.png

不同的方案都有自己的生命力,适用于不同的场景,很难用一句「好」与「坏」来评判。

本文转载自: 掘金

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

1…361362363…956

开发者博客

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