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

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


  • 首页

  • 归档

  • 搜索

Java 17 新功能介绍(LTS)

发表于 2021-11-08

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

点赞再看,动力无限。Hello world : ) 微信搜「 程序猿阿朗 」。

本文 Github.com/niumoo/Java… 和 未读代码博客 已经收录,有很多知识点和系列文章。

JDK 发布历程

Java 17 在 2021 年 9 月 14 日正式发布,Java 17 是一个长期支持(LTS)版本,这次更新共带来 14 个新功能。

OpenJDK Java 17 下载:jdk.java.net/archive/

OpenJDK Java 17 文档:openjdk.java.net/projects/jd…

JEP 描述
JEP 306 恢复始终严格的浮点语义
JEP 356 增强的伪随机数生成器
JEP 382 使用新的 macOS 渲染库
JEP 391 支持 macOS/AArch64 架构
JEP 398 删除已启用的 Applet API
JEP 403 更强的封装 JDK 内部封装
JEP 406 Switch 模式匹配(预览)
JEP 407 移除 RMI Activation
JEP 409 密封类(Sealed Classes)
JEP 410 JEP 401:移除实验性的 AOT 和 JIT 编译器
JEP 411 弃用 Security Manager
JEP 412 外部函数和内存 API(孵化器)
JEP 414 Vector API(第二孵化器)
JEP 415 指定上下文的反序列化过滤器

此文章属于 Java 新特性教程 系列,会介绍 Java 每个版本的新功能,可以点击浏览。

  1. JEP 306: 恢复始终严格的浮点语义

既然是恢复严格的浮点语义,那么说明在某个时间点之前,是始终严格的浮点语义的。其实在 Java SE 1.2 之前,所有的浮点计算都是严格的,但是以当初的情况来看,过于严格的浮点计算在当初流行的 x86 架构和 x87 浮点协议处理器上运行,需要大量的额外的指令开销,所以在 Java SE 1.2 开始,需要手动使用关键字 strictfp(strict float point) 才能启用严格的浮点计算。

但是在 2021 年的今天,硬件早已发生巨变,当初的问题已经不存在了,所以从 Java 17 开始,恢复了始终严格的浮点语义这一特性。

扩展:strictfp 是 Java 中的一个关键字,大多数人可能没有注意过它,它可以用在类、接口或者方法上,被 strictfp 修饰的部分中的 float 和 double 表达式会进行严格浮点计算。

下面是一个示例,其中的 testStrictfp() 被 strictfp 修饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package com.wdbyte;

public class Main {
public static void main(String[] args) {
testStrictfp();
}

public strictfp static void testStrictfp() {
float aFloat = 0.6666666666666666666f;
double aDouble = 0.88888888888888888d;
double sum = aFloat + aDouble;
System.out.println("sum: " + sum);
}
}
  1. JEP 356:增强的伪随机数生成器

为伪随机数生成器 RPNG(pseudorandom number generator)增加了新的接口类型和实现,让在代码中使用各种 PRNG 算法变得容易许多。

这次增加了 RandomGenerator 接口,为所有的 PRNG 算法提供统一的 API,并且可以获取不同类型的 PRNG 对象流。同时也提供了一个新类 RandomGeneratorFactory 用于构造各种 RandomGenerator 实例,在 RandomGeneratorFactory 中使用 ServiceLoader.provider 来加载各种 PRNG 实现。

下面是一个使用示例:随便选择一个 PRNG 算法生成 5 个 10 以内的随机数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码package com.wdbyte.java17;

import java.util.Date;
import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;
import java.util.stream.Stream;

/**
* @author niulang
*/
public class JEP356 {

public static void main(String[] args) {
RandomGeneratorFactory<RandomGenerator> l128X256MixRandom = RandomGeneratorFactory.of("L128X256MixRandom");
// 使用时间戳作为随机数种子
RandomGenerator randomGenerator = l128X256MixRandom.create(System.currentTimeMillis());
for (int i = 0; i < 5; i++) {
System.out.println(randomGenerator.nextInt(10));
}
}
}

得到输出:

1
2
3
4
5
shell复制代码7
3
4
4
6

你也可以遍历出所有的 PRNG 算法。

1
2
3
java复制代码RandomGeneratorFactory.all().forEach(factory -> {
System.out.println(factory.group() + ":" + factory.name());
});

得到输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
shell复制代码LXM:L32X64MixRandom
LXM:L128X128MixRandom
LXM:L64X128MixRandom
Legacy:SecureRandom
LXM:L128X1024MixRandom
LXM:L64X128StarStarRandom
Xoshiro:Xoshiro256PlusPlus
LXM:L64X256MixRandom
Legacy:Random
Xoroshiro:Xoroshiro128PlusPlus
LXM:L128X256MixRandom
Legacy:SplittableRandom
LXM:L64X1024MixRandom

可以看到 Legacy:Random 也在其中,新的 API 兼容了老的 Random 方式,所以你也可以使用新的 API 调用 Random 类生成随机数。

1
2
3
4
5
6
7
java复制代码// 使用 Random
RandomGeneratorFactory<RandomGenerator> l128X256MixRandom = RandomGeneratorFactory.of("Random");
// 使用时间戳作为随机数种子
RandomGenerator randomGenerator = l128X256MixRandom.create(System.currentTimeMillis());
for (int i = 0; i < 5; i++) {
System.out.println(randomGenerator.nextInt(10));
}

扩展阅读:增强的伪随机数生成器

  1. JEP 382:使用新的 macOS 渲染库

macOS 为了提高图形的渲染性能,在 2018 年 9 月抛弃了之前的 OpenGL 渲染库 ,而使用了 Apple Metal 进行代替。Java 17 这次更新开始支持 Apple Metal,不过对于 API 没有任何改变,这一些都是内部修改。

扩展阅读:macOS Mojave 10.14 Release Notes,Apple Metal

  1. JEP 391:支持 macOS/AArch64 架构

起因是 Apple 在 2020 年 6 月的 WWDC 演讲中宣布,将开启一项长期的将 Macintosh 计算机系列从 x64 过度到 AArch64 的长期计划,因此需要尽快的让 JDK 支持 macOS/AArch64 。

Linux 上的 AArch64 支持以及在 Java 16 时已经支持,可以查看之前的文章了解。

扩展:Java 16 新功能介绍 - JEP 386

  1. JEP 398:删除已弃用的 Applet API

Applet 是使用 Java 编写的可以嵌入到 HTML 中的小应用程序,嵌入方式是通过普通的 HTML 标记语法,由于早已过时,几乎没有场景在使用了。

示例:嵌入 Hello.class

1
html复制代码<applet code="Hello.class" height=200 width=200></applet>

Applet API 在 Java 9 时已经标记了废弃,现在 Java 17 中将彻底删除。

  1. JEP 403:更强的 JDK 内部封装

如 Java 16 的 JEP 396 中描述的一样,为了提高 JDK 的安全性,使 --illegal-access 选项的默认模式从允许更改为拒绝。通过此更改,JDK 的内部包和 API(关键内部 API 除外)将不再默认打开。

但是在 Java 17 中,除了 sun.misc.Unsafe ,使用 --illegal-access 命令也不能打开 JDK 内部的强封装模式了,除了 sun.misc.Unsafe API .

在 Java 17 中使用 --illegal-access 选项将会得到一个命令已经移除的警告。

1
2
3
4
5
6
shell复制代码➜  bin ./java -version
openjdk version "17" 2021-09-14
OpenJDK Runtime Environment (build 17+35-2724)
OpenJDK 64-Bit Server VM (build 17+35-2724, mixed mode, sharing)
➜ bin ./java --illegal-access=warn
OpenJDK 64-Bit Server VM warning: Ignoring option --illegal-access=warn; support was removed in 17.0

扩展阅读:JEP 403:更强的 JDK 内部封装,Java 16 新功能介绍

  1. JEP 406:switch 的类型匹配(预览)

如 instanceof 一样,为 switch 也增加了类型匹配自动转换功能。

在之前,使用 instanceof 需要如下操作:

1
2
3
4
java复制代码if (obj instanceof String) {
String s = (String) obj; // grr...
...
}

多余的类型强制转换,而现在:

1
2
3
4
java复制代码if (obj instanceof String s) {
// Let pattern matching do the work!
...
}

switch 也可以使用类似的方式了。

1
2
3
4
5
6
7
8
9
java复制代码static String formatterPatternSwitch(Object o) {
return switch (o) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> o.toString();
};
}

对于 null 值的判断也有了新的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码// Java 17 之前
static void testFooBar(String s) {
if (s == null) {
System.out.println("oops!");
return;
}
switch (s) {
case "Foo", "Bar" -> System.out.println("Great");
default -> System.out.println("Ok");
}
}
// Java 17
static void testFooBar(String s) {
switch (s) {
case null -> System.out.println("Oops");
case "Foo", "Bar" -> System.out.println("Great");
default -> System.out.println("Ok");
}
}

扩展阅读: JEP 406:switch 的类型匹配(预览)

  1. JEP 407:移除 RMI Activation

移除了在 JEP 385 中被标记废除的 RMI(Remote Method Invocation)Activation,但是 RMI 其他部分不会受影响。

RMI Activation 在 Java 15 中的 JEP 385 已经被标记为过时废弃,至今没有收到不良反馈,因此决定在 Java 17 中正式移除。

扩展阅读: JEP 407:移除 RMI Activation

  1. JEP 409:密封类(Sealed Classes)

Sealed Classes 在 Java 15 中的 JEP 360 中提出,在 Java 16 中的 JEP 397 再次预览,现在 Java 17 中成为正式的功能,相比 Java 16 并没有功能变化,这里不再重复介绍,想了解的可以参考之前文章。

扩展阅读:Java 16 新功能介绍,JEP 409: Sealed Classes

  1. JEP 401:移除实验性的 AOT 和 JIT 编译器

在 Java 9 的 JEP 295 中,引入了实验性的提前编译 jaotc 工具,但是这个特性自从引入依赖用处都不太大,而且需要大量的维护工作,所以在 Java 17 中决定删除这个特性。

主要移除了三个 JDK 模块:

  1. jdk.aot - jaotc 工具。
  2. Jdk.internal.vm.compiler - Graal 编译器。
  3. jdk.internal.vm.compiler.management

同时也移除了部分与 AOT 编译相关的 HotSpot 代码:

  1. src/hotspot/share/aot — dumps and loads AOT code
  2. Additional code guarded by #if INCLUDE_AOT
  1. JEP 411:弃用 Security Manager

Security Manager 在 JDK 1.0 时就已经引入,但是它一直都不是保护服务端以及客户端 Java 代码的主要手段,为了 Java 的继续发展,决定弃用 Security Manager,在不久的未来进行删除。

1
2
3
4
java复制代码@Deprecated(since="17", forRemoval=true)
public class SecurityManager {
// ...
}
  1. JEP 412:外部函数和内存 API (孵化)

新的 API 允许 Java 开发者与 JVM 之外的代码和数据进行交互,通过调用外部函数,可以在不使用 JNI 的情况下调用本地库。

这是一个孵化功能;需要添加--add-modules jdk.incubator.foreign来编译和运行 Java 代码。

历史

  • Java 14 JEP 370引入了外部内存访问 API(孵化器)。
  • Java 15 JEP 383引入了外部内存访问 API(第二孵化器)。
  • Java 16 JEP 389引入了外部链接器 API(孵化器)。
  • Java 16 JEP 393引入了外部内存访问 API(第三孵化器)。
  • Java 17 JEP 412引入了外部函数和内存 API(孵化器)。

扩展阅读:JEP 412:外部函数和内存 API (孵化)

  1. JEP 414:Vector API(二次孵化)

在 Java 16 中引入一个新的 API 来进行向量计算,它可以在运行时可靠的编译为支持的 CPU 架构,从而实现更优的计算能力。

现在 Java 17 中改进了 Vector API 性能,增强了例如对字符的操作、字节向量与布尔数组之间的相互转换等功能。

  1. JEP 415:指定上下文的反序列化过滤器

Java 中的序列化一直都是非常重要的功能,如果没有序列化功能,Java 可能都不会占据开发语言的主导地位,序列化让远程处理变得容易和透明,同时也促进了 Java EE 的成功。

但是 Java 序列化的问题也很多,它几乎会犯下所有的可以想象的错误,为开发者带来持续的维护工作。但是要说明的是序列化的概念是没有错的,把对象转换为可以在 JVM 之间自由传输,并且可以在另一端重新构建的能力是完全合理的想法,问题在于 Java 中的序列化设计存在风险,以至于爆出过很多和序列化相关的漏洞。

反序列化危险的一个原因是,有时候我们不好验证将要进行反序列化的内容是否存在风险,而传入的数据流可以自由引用对象,很有可能这个数据流就是攻击者精心构造的恶意代码。

所以,JEP 415 允许在反序列化时,通过一个过滤配置,来告知本次反序列化允许或者禁止操作的类,反序列化时碰到被禁止的类,则会反序列化失败。

14.1. 反序列化示例

假设 Dog 类中的 Poc 是恶意构造的类,但是正常反序列化是可以成功的。

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复制代码package com.wdbyte.java17;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
* @author niulang
*/
public class JEP415 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Dog dog = new Dog("哈士奇");
dog.setPoc(new Poc());
// 序列化 - 对象转字节数组
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);) {
objectOutputStream.writeObject(dog);
}
byte[] bytes = byteArrayOutputStream.toByteArray();
// 反序列化 - 字节数组转对象
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object object = objectInputStream.readObject();
System.out.println(object.toString());
}
}

class Dog implements Serializable {
private String name;
private Poc poc;

public Dog(String name) {
this.name = name;
}

@Override
public String toString() {
return "Dog{" + "name='" + name + '\'' + '}';
}
// get...set...
}

class Poc implements Serializable{

}

输出结果:

1
ini复制代码Dog{name='哈士奇'}

14.2. 反序列化过滤器

在 Java 17 中可以自定义反序列化过滤器,拦截不允许的类。

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
java复制代码package com.wdbyte.java17;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputFilter;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
* @author niulang
*/
public class JEP415 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Dog dog = new Dog("哈士奇");
dog.setPoc(new Poc());
// 序列化 - 对象转字节数组
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);) {
objectOutputStream.writeObject(dog);
}
byte[] bytes = byteArrayOutputStream.toByteArray();
// 反序列化 - 字节数组转对象
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
// 允许 com.wdbyte.java17.Dog 类,允许 java.base 中的所有类,拒绝其他任何类
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.wdbyte.java17.Dog;java.base/*;!*");
objectInputStream.setObjectInputFilter(filter);
Object object = objectInputStream.readObject();
System.out.println(object.toString());
}
}

class Dog implements Serializable {
private String name;
private Poc poc;

public Dog(String name) {
this.name = name;
}

@Override
public String toString() {
return "Dog{" + "name='" + name + '\'' + '}';
}
// get...set...
}

class Poc implements Serializable{
}

这时反序列化会得到异常。

1
2
3
4
5
shell复制代码Exception in thread "main" java.io.InvalidClassException: filter status: REJECTED
at java.base/java.io.ObjectInputStream.filterCheck(ObjectInputStream.java:1412)
at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2053)
at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1907)
....

扩展阅读:JEP 415:指定上下文的反序列化过滤器

参考

  1. openjdk.java.net/projects/jd…
  2. docs.oracle.com/en/java/jav…

<完>

文章持续更新,可以微信搜一搜「 程序猿阿朗 」或访问「程序猿阿朗博客 」第一时间阅读。本文 Github.com/niumoo/Java… 已经收录,有很多知识点和系列文章,欢迎Star。

本文转载自: 掘金

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

CompletableFuture执行过程源码分析 如何创建

发表于 2021-11-08

简单了解过CompletableFuture的API之后,接下来尝试着从源码的角度说明一下CompletableFuture的执行过程。包括:

  • CompletableFuture是如何创建并执行一个异步任务的
  • 如何创造一个被回调的任务
  • 任务是如何被回调的

如何创建一个异步任务

以supplyAsync为例来说明CompletableFuture如何创建一个异步任务并运行;

supplyAsync的重载方法中,都通过调用asyncSupplyStage方法来创建任务,区别在于二者提供的线程池对象不同。一个使用默认的ForkJoinPool线程池,另一个使用调用者提供的线程池。

supplyAsync

1
2
3
4
5
6
java复制代码    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
return asyncSupplyStage(asyncPool, supplier);
}
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor) {
return asyncSupplyStage(screenExecutor(executor), supplier);
}

asyncSupplyStage提交任务的过程可以分为三步:

  1. 首先,创建了一个代表当前任务执行阶段的CompletableFuture对象并最终返回;
  2. 其次,将代表当前阶段的CompletableFuture对象与Supplier接口封装到AsyncSupply;
  3. 最终,将AsyncSupply对象提交到线程池中执行

这一波操作,与平时的使用Runnable、Callable接口创建异步任务提交到线程池执行基本一致。

1
2
3
4
5
6
java复制代码   static <U> CompletableFuture<U> asyncSupplyStage(Executor e,Supplier<U> f) {
if (f == null) throw new NullPointerException();
CompletableFuture<U> d = new CompletableFuture<U>();
e.execute(new AsyncSupply<U>(d, f));//封装Supplier接口并提交到线程池中
return d;
}

任务如何被执行的

任务被提交到线程池中后,可能会通过exec方法或run方法被调用,这取决于具体的线程池实现。而AsyncSupply将任务逻辑都封装到了run方法中,Supplier接口的get方法也在这里被调用,我们封装的逻辑也在此时被执行。如果执行没有异常,那么通过completeValue方法将值封装到CompletableFuture中。如果执行出现异常,则通过completeThrowable方法封装一个异常的结果到CompletableFuture中。

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复制代码static final class AsyncSupply<T> extends ForkJoinTask<Void>
implements Runnable, AsynchronousCompletionTask {
CompletableFuture<T> dep; Supplier<T> fn;
AsyncSupply(CompletableFuture<T> dep, Supplier<T> fn) {
this.dep = dep; this.fn = fn;
}

public final Void getRawResult() { return null; }
public final void setRawResult(Void v) {}
public final boolean exec() { run(); return true; }

public void run() {
CompletableFuture<T> d; Supplier<T> f;
if ((d = dep) != null && (f = fn) != null) {
dep = null; fn = null;
if (d.result == null) {
try {
d.completeValue(f.get());//执行supply函数的get方法
} catch (Throwable ex) {
d.completeThrowable(ex);
}
}
d.postComplete();
}
}
}

以上就是一个CompletableFuture异步任务的创建与执行过程。那么,如果需要在当前的异步任务完成时执行其他逻辑,CompletableFuture时如何实现的呢?

如何创建一个回调任务

要说清楚CompletableFuture时如何执行一个回调任务,需要先简单说明一下CompletableFuture的主要结构。

CompletableFuture的主要结构

CompletableFuture的源码中,除去注释,前两行源码是这个样子的。

1
2
3
4
5
java复制代码public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {

volatile Object result; // Either the result or boxed AltResult
volatile Completion stack; // TopS of Treiber stack of dependent actions
}

result代表着CompletableFuture对象的执行结果。Completion类型的stack字段代表着需要回调的后续任务。Completion对象内部维护着一个next指针,可以指向下一个需要被回调的对象,所有需要被回调的对象组成了一个单项链表。链表的节点由Completion对象组成。而stack指向的是这个链表的头结点,也是最后一个入栈的Completion对象。

所以,被封装了回调任务的CompletableFuture对象应该长这个样子。

image-20211028161540796

Completion对象里面封装的是需要被回调的任务逻辑。但是代表当前阶段的任务又在哪里?其实,CompletableFuture并不知道当前阶段的任务在哪里,而是返过来通过任务指向代表当前阶段的对象。Completion对象通过dep字段,持有代表当前任务阶段的CompletableFuture对象。所以,完成的调用链可能长这个样子:

image-20211028161925393

封装回调任务

那么,任务时何时被封装到调用链中的呢?下面以uniApplyXXX方法为例进行说明。uniApplyXXX的三个重载方法最终都转发到了uniApplyStage方法中,区别在于传递的线程池参数不同。
uniApplyStage源码

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码private <V> CompletableFuture<V> uniApplyStage(
Executor e, Function<? super T,? extends V> f) {
if (f == null) throw new NullPointerException();
CompletableFuture<V> d = new CompletableFuture<V>();
if (e != null || !d.uniApply(this, f, null)) {//判断线程池是否为空,为空则直接尝试执行
//线程池不为空,或前置任务未完成,需要入栈排队。
UniApply<T,V> c = new UniApply<T,V>(e, d, this, f);//新建对象,封装代表执行逻辑的函数式接口对象f,代表当前阶段的CP对象d,还有前置任务this,以及线程池e;
push(c);//将封装好的任务push到当前CP对象的stack中
c.tryFire(SYNC);//防止push过程中前置任务变更完成状态,漏掉当前阶段的任务。尝试执行一次。
}
return d;
}

uniApplyStage的源码中,完成将任务封装到stack中的目标,只用了两行代码:

1
2
java复制代码    UniApply<T,V> c = new UniApply<T,V>(e, d, this, f);
push(c);
  1. 创建Completion对象(UniApply为其子类)
  2. push到stack队列中,成为新的头结点。

可以通过源码看到,任务并不是无条件的放到节点中的。满足以下其中一个条件,任务才会被放到调用链中:

  1. 当任务需要提交到线程池中时,无条件的创建Completion对象,push到stack链表中。
  2. 当uniAppply方法执行失败时,创建Completion对象,push到stack链表中。

uniApply的源码下面会讲到,为了不影响理解这里的逻辑,我们先忽略源码,理解其功能。uniApply的作用是判断任务是否满足执行条件,满足则执行封装的函数式接口。那么,这句话if (e != null || !d.uniApply(this, f, null))中的uniApply方法的作用就很明显了。即,判断当前任务是否已经满足了执行条件,如果满足就直接执行。这样的操作省去了创建Completion对象和入栈的步骤。这里的“满足执行条件”,可以理解为依赖的前任任务是否已经执行完成。

tryFire方法

那么剩下的这个“尝试开火”方法是做什么的呢?当然是激发弹夹中的子弹了!stack就是CompletableFuture的弹夹(stack–>栈结构–>弹夹🤣),Completion就是子弹。

所以tryFire方法的作用就是尝试执行stack中的任务。此处的tryFire方法,通过刚刚创建的UniApply对象调用,并执行封装在其中的任务逻辑。

此处调用是为了避免任务完成入栈后,前置CompletableFuture已经执行完成,从而错过了回调的时机,导致当前的任务无法被触发的情况。

tryFire源码

1
2
3
4
5
6
7
8
java复制代码final CompletableFuture<V> tryFire(int mode) {
CompletableFuture<V> d; CompletableFuture<T> a;
if ((d = dep) == null ||
!d.uniApply(a = src, fn, mode > 0 ? null : this))//执行任务逻辑
return null;
dep = null; src = null; fn = null;
return d.postFire(a, mode);
}

通过源码也可以再次确认,tyrFire方法其中的主要逻辑之一就是尝试执行封装的任务逻辑。

postFire源码

postFire主要用来处理任务执行完成的后续工作。如清理stack中的无效节点,嵌套调用时返回当前CompletableFuture对象或在非嵌套调用时执行postComplete方法,用来激发后续任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码final CompletableFuture<T> postFire(CompletableFuture<?> a, int mode) {
if (a != null && a.stack != null) {
if (mode < 0 || a.result == null)
a.cleanStack();//清理stack中的无效节点
else
a.postComplete();//使用当前线程帮助前置任务执行stack
}
if (result != null && stack != null) {
if (mode < 0)
return this;//嵌套调用,stack不为空,返回当前阶段。
else
postComplete();//非嵌套调用,stack不为空,处理当前阶段的stack
}
return null;
}

UniApply源码

前面已经说过,UniApply的作用就是判断任务是否满足执行条件,然后执行封装的函数式接口。这个过程大概可以分为四个部分:

  1. 判断前置任务是否完成;
  2. 判断前置任务是否有异常;
  3. 判断当前任务是否已经被其他线程声明了执行权限;
  4. 调用函数式接口中的方法,执行任务逻辑,并封装结果。
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复制代码 final <S> boolean uniApply(CompletableFuture<S> a,
Function<? super S,? extends T> f,
UniApply<S,T> c) {//a前置CP,f当前阶段函数,c封装当前阶段逻辑的Completion对象
Object r; Throwable x;
if (a == null || (r = a.result) == null || f == null)
return false;//前置任务未完成或其他异常情况
tryComplete: if (result == null) {//当前CP的结果为空
if (r instanceof AltResult) {
if ((x = ((AltResult)r).ex) != null) {
completeThrowable(x, r);//之前任务结果为异常
break tryComplete;
}
r = null;//前置任务结果为空
}
try {
if (c != null && !c.claim())//claim判断任务是否被执行过;
return false;
@SuppressWarnings("unchecked") S s = (S) r;//转换前置任务的结果类型
completeValue(f.apply(s));//调用function函数的apply方法,并将结果封装到CompletableFuture对象中。
} catch (Throwable ex) {
completeThrowable(ex);
}
}
return true;
}

如何触发回调任务

再说CompletableFuture的结构

虽然前面已经说过了结构的问题,但是实际的情况还要更复杂些。要想说清楚回调的问题,有必要将CompletableFuture的结构说的更准确一些。

前面说过,Completion对象通过dep指向代表当前阶段的CompletableFuture对象。但是没有说的是,这个CompletableFuture对象也可能会有自己的回调链(stack指向的单项链表)。因此,完整的回调结构可能长这个样子的。

未命名文件(5)

什么时候触发回调

看过了CompletableFuture的回调结构,我们来看回调是如何被触发的。

supplyAsync的源码中,执行了这样一个方法d.postComplete();,该方法就是触发后续任务的关键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码final void postComplete() {
/*
* On each step, variable f holds current dependents to pop
* and run. It is extended along only one path at a time,
* pushing others to avoid unbounded recursion.
* f-->当前CP对象,h-->CP对象的stack,t-->stack的next节点
*/
CompletableFuture<?> f = this; Completion h;
while ((h = f.stack) != null ||
(f != this && (h = (f = this).stack) != null)) {
CompletableFuture<?> d; Completion t;
if (f.casStack(h, t = h.next)) {//保证postComplete被并发调用时,同一个任务只能被一个线程拿到
if (t != null) {
if (f != this) {//下一阶段CP对象的stack不为空,将stack压入当前CP对象的stack中。防止递归调用过深。
pushStack(h);
continue;
}
h.next = null; // detach
}
f = (d = h.tryFire(NESTED)) == null ? this : d;//d-->tryFire的返回值;d不为空时,f被指向d。即h任务所在阶段的CompletableFuture对象
}
}
}

触发的过程可以分为一下几部

  1. 取下stack中的首节点:首先从当前CompletableFuture对象(this)中,获取到回调链stack。如果stack不为空,先获取首节点的引用,然后将stack通过CAS指向next。如果CAS更新成功,获取了头结点的执行权限,可以进行下一步。否则重复上述过程,直到成功取下一个节点或没有任务需要执行。
  2. 执行节点的任务逻辑:第一次取得头结点后if (f != this)显然是不成立的,先不考虑里面包含的逻辑。关注这行代码 h.tryFire(NESTED)。tryFire方法与前面说的一致,就是执行Completion中封装的任务逻辑。如果一切顺利,那么第一个需要被回调的任务就开始执行了。
  3. 重新赋值f:tryFire在嵌套调用时,如果Completion指向的CompletableFuture对象也有需要被回调的任务,那么tryFire方法会返回该CompletableFuture对象,否则返回null。因此,f = (d = h.tryFire(NESTED)) == null ? this : d;这句话的作用就是:如果有后续任务,依赖于当前执行的阶段,那么返回代表这个阶段的CompletableFuture对象,赋值给f。否则,f仍然指向this。
  4. 将递归调用转为循环调用:当f指向了下一阶段的CompletableFuture对象后,if (f != this)条件成立,执行pushStack方法。该方法把上一步tryFire返回的CompletableFuture对象的回调任务压入到了自己的stack栈中。通过while循环,直到所有的任务都被压入后,f.stack的值变为null。此时,f被重新指向this继续回调后续的任务,直到所有的任务都被触发。这样做是为了将递归调用改为循环调用,防止递归过深。

过程如下图:

未命名文件(6)

未命名文件(6)

未命名文件(6)

未命名文件(6)

本文转载自: 掘金

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

冲刺大厂每日算法&面试题,动态规划21天——第十一天 导读

发表于 2021-11-08

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

导读

在这里插入图片描述

肥友们为了更好的去帮助新同学适应算法和面试题,最近我们开始进行专项突击一步一步来。我们先来搞一下让大家最头疼的一类算法题,动态规划我们将进行为时21天的养成计划。还在等什么快来一起肥学进行动态规划21天挑战吧!!

21天动态规划入门

给你一个整数 n ,请你找出并返回第 n 个 丑数 。

丑数 就是只包含质因数 2、3 和/或 5 的正整数。

1
2
3
4
5
java复制代码示例 1:

输入:n = 10
输出:12
解释:[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。
1
2
3
4
5
java复制代码示例 2:

输入:n = 1
输出:1
解释:1 通常被视为丑数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码class Solution {
public int nthUglyNumber(int n) {
int[] dp = new int[n + 1];
dp[1] = 1;
int p2 = 1, p3 = 1, p5 = 1;
for (int i = 2; i <= n; i++) {
int num2 = dp[p2] * 2, num3 = dp[p3] * 3, num5 = dp[p5] * 5;
dp[i] = Math.min(Math.min(num2, num3), num5);
if (dp[i] == num2) {
p2++;
}
if (dp[i] == num3) {
p3++;
}
if (dp[i] == num5) {
p5++;
}
}
return dp[n];
}
}

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

在这里插入图片描述

1
2
3
4
5
java复制代码示例 1:


输入:n = 3
输出:5
1
2
3
4
java复制代码示例 2:

输入:n = 1
输出:1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码class Solution {
public int numTrees(int n) {
int[] G = new int[n + 1];
G[0] = 1;
G[1] = 1;

for (int i = 2; i <= n; ++i) {
for (int j = 1; j <= i; ++j) {
G[i] += G[j - 1] * G[i - j];
}
}
return G[n];
}
}

面试题

续写Linux命令系列

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码八、进程相关的命令
17 jps命令
(显示当前系统的java进程情况,及其id号)

jps(Java Virtual Machine Process Status Tool)是JDK 1.5提供的一个显示当前所有java进程pid的命令,简单实用,非常适合在linux/unix平台上简单察看当前java进程的一些简单情况。
18 ps命令
(用于将某个时间点的进程运行情况选取下来并输出,process之意)

-A :所有的进程均显示出来
-a :不与terminal有关的所有进程
-u :有效用户的相关进程
-x :一般与a参数一起使用,可列出较完整的信息
-l :较长,较详细地将PID的信息列出

本文转载自: 掘金

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

Shell基础

发表于 2021-11-08

###Shell是什么?

  • Shell是一个命令行解释器他为用户提供了一个向linux内核发送请求以便运行程序的界面系统程序,用户可以用Shell来启动、挂起、停止甚至编写一些程序。
  • Shell还是一个功能相当强大的编程语言,易编写易调试,灵活性较强。Shell是解释执行的脚本语言,在Shell中可以直接调用linux系统命令。

echo 输出命令

1
2
3
4
5
6
bash复制代码echo [选项] [输出内容]
选项:
-e 支持反斜线控制的字符串转换
例:
echo -e "\e[1;31m 我是你爸比 \e[0m"
其中\e[1;31m 开启颜色 \e[0m 关闭颜色(严格按格式来,不能有多余的空格)

第一个脚本:

1
2
3
bash复制代码#!/bin/bash
#the first program
echo -e "\e[1;32m 爸爸去哪了 \e[0m"

脚本执行

1
2
3
4
5
6
bash复制代码赋予执行权限,直接运行
chmod 75 hellow.sh
./hellow.sh

通过bash调用执行脚本
bash hellow.sh

###Bash的基本功能:

  • 1、命令的别名与快捷键:
1
2
3
4
5
6
7
8
9
10
bash复制代码  alias 
查看系统中所有命令的别名

alias 别名='原命令'

别名永久生效与删除别名
vi ~/.bashrc
写入环境变量配置文件
unalias 别名
删除别名

命令生效顺序

  • 执行绝对路径或相对路径执行的命令
  • 执行别名
  • 执行bash的内部命令
  • 执行按照$PATH环境变量定义的目录查找顺序找到的第一个命令

常用快捷键:

1
2
3
4
5
6
7
css复制代码Ctrl+c 强制终止当前命令
Ctrl+l 清屏
Ctrl+a 光标移动到命令行首
Ctrl+e 光标移动到命令行尾
Ctrl+u 从光标所在位置删除到行首
Ctrl+z 把命令放入后台
Ctrl+r 在历史命令中搜索

2、历史命令

历史命令默认保留1000条,可以在环境变量配置文件/etc/profile中进行修改

1
2
3
4
bash复制代码history [选项] [历史命令保存文件]
选项:
-c 清空历史命令
-w 把缓存中的历史命令写入历史命令保存文件~/.bash_history

######历史命令的调用

1
2
3
4
5
6
7
markdown复制代码* 使用上下箭头调用以前的命令

* 使用!n 重复执行第n条历史命令

* 使用!! 重复执行上一条命令

* 使用!字符串 重复执行最后一条以该字符串开头的历史命令

######命令与文件的补全

使用Tab键就能自动补全文件或命令

3、输出重定向(注意空格位置)

1
2
3
4
5
6
css复制代码统计键盘输入
wc [选项] [文件名]
-c 统计字节数
-w 统计单词数
-l 统计行数
Ctrl+d 全部统计

image.png

image.png
4、多命令顺序执行

image.png
5、管道符

1
2
csharp复制代码[root@localhost ~]#    命令1 | 命令2
命令1的正确输出作为命令2的操作对象

本文转载自: 掘金

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

Redis持久化之AOF

发表于 2021-11-08

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

AOF

image.png

  • 上文我们提到了rRedis中的一种持久化方式就是RDB
    • 此文我们来讲另一种实现方式那就是AOF
    • AOF是通过只记录Redis写入命令的追加式日志文件
    • AOF日志存储的因为是顺序指令,所以在Redis宕机后,内部”重走一遍老路”,就可以达到恢复数据的目的

AOF原理

  • 每次我们执行写入命令的时候,Redis都会把命令追加到AOF文件里面
    • 实际是将内容写到内存的一个缓存汇总
    • 所以Redis需要实时的去调用Linux提供的fsync函数每次将制定文件从内核缓存写到磁盘
    • 因为如果实时的去调用函数是非常我们也可以通过改变配置参数来调整调用fsync函数的频率

重写

  • 如果一直追加AOF文件,日志会变得越来越大
    • 所以Redis内部实现了「重写」,用来压缩文件
    • 首先因为很多的key-value早就已经国企,或者很多值不断被更新,那么日志还存这些无用的数据显然是可以进行优化的
    • 所以创建一个子进程对内存进行遍历
    • 遍历成一个新的文件日志,所以实现了只有最新的值才需要存储在AOF日志中
    • 我们可以使用下图的命令来开启重写,当然也可以设置成自动执行AOF

image.png

AOF缺点

  1. 每次遍历整个内存,会给系统带来很大的额外性能开销
  2. 每次恢复数据是一个很长的过程并且每次进行从内核写到磁盘很耗费IO性能

本文转载自: 掘金

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

dart系列之 dart语言中的特殊操作符 简介 普通操作符

发表于 2021-11-08

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

简介

有运算就有操作符,dart中除了普通的算术运算的操作符之外,还有自定义的非常特殊的操作符,今天带大家一起来探索一下dart中的特殊操作符。

普通操作符

普通操作符就很好解释了,就是加减乘除,逻辑运算符,比较运算符和位运算符等。

这些操作符和其他语言的操作符没什么差别,这里就不详细介绍了。大家看几个普通操作符的例子:

1
2
3
4
5
6
7
8
9
css复制代码a++
a + b
a = b
a == b
c ? a : b
assert(2 == 2);
assert(2 != 3);
assert(3 > 2);
assert(2 < 3);

类型测试操作符

dart中的类型测试符类似JAVA中的instance of操作,主要有三个,分别是as,is和is!

其中is是类型判断操作符,而as是类型转换操作符,也就是常说的强制转换。

对下面的语句来说,如果obj是T的子类或者实现了T的接口,那么就会返回true。

1
csharp复制代码obj is T

而下面的语句则会始终返回true:

1
vbnet复制代码obj is Object?

dart中的as操作符表示的是类型转换,转换类型之后就可以使用对应类型中的方法了。如下所示:

1
ini复制代码(student as Student).firstName = 'Bob';

那么问题来了,上面的写法和下面的写法有什么区别吗?

1
2
3
4
ini复制代码if (student is Person) {
// Type check
student.firstName = 'Bob';
}

第一种写法中,如果student是空,或者不是Student的实例,则会报错,而第二种并不会。

条件运算符

dart中也支持条件运算符,最常见的就是三元运算符:

1
sql复制代码condition ? expr1 : expr2

表示如果condition是true,则返回expr1, 否则返回expr2。

我们在日常的工作中,经常会有一些判空操作,dart为我们提供了非常简便的判空操作符:

1
ruby复制代码expr1 ?? expr2

上式表示如果expr1为空,则选择expr2。举个例子:

1
arduino复制代码String playerName(String? name) => name ?? 'Guest';

级联符号

级联符号是 .. 或者?.. , 用来在同一对象上进行序列操作,级联操作可以让我们少写很多代码,可以在创建一个对象的同时,给对象赋值:

1
2
3
4
ini复制代码var paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0;

上面的代码等同于:

1
2
3
4
ini复制代码var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;

如果对象可能为空,则可以在第一个级联操作符之前加上?,这样如果对象为空的话,后续的级联操作都不会进行,如下所示:

1
2
3
4
ini复制代码var paint = Paint()
?..color = Colors.bla
..strokeCap = Stroke
..strokeWidth = 5.0;

类中的自定义操作符

在dart中可以实现类似C++那种操作符的重写功能。可以实现对象之间的加减乘除之类的运算。

比如下面的类中,我们自定义了类之间的加法和减法运算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scss复制代码class Vector {
final int x, y;

Vector(this.x, this.y);

Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
Vector operator -(Vector v) => Vector(x - v.x, y - v.y);

// Operator == and hashCode not shown.
// ···
}

void main() {
final v = Vector(2, 3);
final w = Vector(2, 2);

assert(v + w == Vector(4, 5));
assert(v - w == Vector(0, 1));
}

自定义操作符是用operator关键字来修饰的,非常的方便。

总结

以上就是dart中的操作符的介绍和使用。

本文已收录于 <www.flydean.com>

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

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

spring循环依赖

发表于 2021-11-08

什么是循环依赖?

很简单,就是A对象依赖了B对象,B对象依赖了A对象

比如:

1
2
3
4
5
6
7
8
9
css复制代码// A依赖了B
class A{
public B b;
}

// B依赖了A
class B{
public A a;
}

那么循环依赖是个问题吗? 如果不考虑Spring,循环依赖并不是问题,因为对象之间相互依赖是很正常的事情。

⽐如

1
2
3
4
5
ini复制代码 A a = new A();
B b = new B();

a.b = b;
b.a = a;

这样,A,B就依赖上了。 但是,在Spring中循环依赖就是⼀个问题了,为什么? 因为,在Spring中,⼀个对象并不是简单new出来了,⽽是会经过⼀系列的Bean的⽣命周期,就是因为 Bean的⽣命周期所以才会出现循环依赖问题。当然,在Spring中,出现循环依赖的场景很多,有的场景 Spring⾃动帮我们解决了,⽽有的场景则需要程序员来解决,下⽂详细来说。 要明⽩Spring中的循环依赖,得先明⽩Spring中Bean的⽣命周期。

Bean的生命周期

这⾥不会对Bean的⽣命周期进⾏详细的描述,只描述⼀下⼤概的过程。 Bean的⽣命周期指的就是:在Spring中,Bean是如何⽣成的? 被Spring管理的对象叫做Bean。Bean的⽣成步骤如下:

  1. Spring扫描class得到BeanDefinition
  1. 根据得到的BeanDefinition去⽣成bean
  1. ⾸先根据class推断构造⽅法
  1. 根据推断出来的构造⽅法,反射,得到⼀个对象(暂时叫做原始对象)
  1. 填充原始对象中的属性(依赖注⼊)
  1. 如果原始对象中的某个⽅法被AOP了,那么则需要根据原始对象⽣成⼀个代理对象
  1. 把最终⽣成的代理对象放⼊单例池(源码中叫做singletonObjects)中,下次getBean时就直接从单例 池拿即可

可以看到,对于Spring中的Bean的⽣成过程,步骤还是很多的,并且不仅仅只有上⾯的7步,还有很多很 多,⽐如Aware回调、初始化等等,这⾥不详细讨论。

可以发现,在Spring中,构造⼀个Bean,包括了new这个步骤(第4步构造⽅法反射)。

得到⼀个原始对象后,Spring需要给对象中的属性进⾏依赖注⼊,那么这个注⼊过程是怎样的?

⽐如上⽂说的A类,A类中存在⼀个B类的b属性,所以,当A类⽣成了⼀个原始对象之后,就会去给b属性 去赋值,此时就会根据b属性的类型和属性名去BeanFactory中去获取B类所对应的单例bean。如果此时 BeanFactory中存在B对应的Bean,那么直接拿来赋值给b属性;

如果此时BeanFactory中不存在B对应的 Bean,则需要⽣成⼀个B对应的Bean,然后赋值给b属性。

问题就出现在第⼆种情况,如果此时B类在BeanFactory中还没有⽣成对应的Bean,那么就需要去⽣成, 就会经过B的Bean的⽣命周期。

那么在创建B类的Bean的过程中,如果B类中存在⼀个A类的a属性,那么在创建B的Bean的过程中就需要 A类对应的Bean,但是,触发B类Bean的创建的条件是A类Bean在创建过程中的依赖注⼊,所以这⾥就出 现了循环依赖:

ABean创建–>依赖了B属性–>触发BBean创建—>B依赖了A属性—>需要ABean(但ABean还在创 建过程中)

从⽽导致ABean创建不出来,BBean也创建不出来。 这是循环依赖的场景,但是上⽂说了,在Spring中,通过某些机制帮开发者解决了部分循环依赖的问题, 这个机制就是三级缓存。

三级缓存

三级缓存是通⽤的叫法。

⼀级缓存为:singletonObjects

⼆级缓存为:earlySingletonObjects

三级缓存为:singletonFactories

先稍微解释⼀下这三个缓存的作⽤,后⾯详细分析:

singletonObjects中缓存的是已经经历了完整⽣命周期的bean对象。

earlySingletonObjects⽐singletonObjects多了⼀个early,表示缓存的是早期的bean对象。早期是 什么意思?表示Bean的⽣命周期还没⾛完就把这个Bean放⼊了earlySingletonObjects。

singletonFactories中缓存的是ObjectFactory,表示对象⼯⼚,⽤来创建某个对象的。

解决循环依赖思路分析

先来分析为什么缓存能解决循环依赖。

上⽂分析得到,之所以产⽣循环依赖的问题,

A创建时—>需要B—->B去创建—>需要A,从⽽产⽣了循环

那么如何打破这个循环,加个中间⼈(缓存)

A的Bean在创建过程中,在进⾏依赖注⼊之前,先把A的原始Bean放⼊缓存(提早暴露,只要放到缓存 了,其他Bean需要时就可以从缓存中拿了),放⼊缓存后,再进⾏依赖注⼊,此时A的Bean依赖了B的 Bean,如果B的Bean不存在,则需要创建B的Bean,⽽创建B的Bean的过程和A⼀样,也是先创建⼀个B 的原始对象,然后把B的原始对象提早暴露出来放⼊缓存中,然后在对B的原始对象进⾏依赖注⼊A,此时 能从缓存中拿到A的原始对象(虽然是A的原始对象,还不是最终的Bean),B的原始对象依赖注⼊完了之 后,B的⽣命周期结束,那么A的⽣命周期也能结束。

因为整个过程中,都只有⼀个A原始对象,所以对于B⽽⾔,就算在属性注⼊时,注⼊的是A原始对象,也 没有关系,因为A原始对象在后续的⽣命周期中在堆中没有发⽣变化。 从上⾯这个分析过程中可以得出,只需要⼀个缓存就能解决循环依赖了,那么为什么Spring中还需要 singletonFactories呢? 这是难点,基于上⾯的场景想⼀个问题:如果A的原始对象注⼊给B的属性之后,A的原始对象进⾏了AOP 产⽣了⼀个代理对象,此时就会出现,对于A⽽⾔,它的Bean对象其实应该是AOP之后的代理对象,⽽B 的a属性对应的并不是AOP之后的代理对象,这就产⽣了冲突。

B依赖的A和最终的A不是同⼀个对象。

那么如何解决这个问题?这个问题可以说没有办法解决。 因为在⼀个Bean的⽣命周期最后,Spring提供了BeanPostProcessor可以去对Bean进⾏加⼯,这个加⼯ 不仅仅只是能修改Bean的属性值,也可以替换掉当前Bean。

举个例⼦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码 @Component
public class User {
}
@Component
public class LubanBeanPostProcessor implements BeanPostProcessor
{

@Override
public Object postProcessAfterInitialization(Object bean, Str
ing beanName) throws BeansException {

// 注意这⾥,⽣成了⼀个新的User对象
if (beanName.equals("user")) {
System.out.println(bean);
User user = new User();
return user;
}
return bean;
}
}
public class Test {
public static void main(String[] args) {

AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.
class);

User user = context.getBean("user", User.class);
System.out.println(user);

}
}

运⾏main⽅法,得到的打印如下:

1
2
kotlin复制代码com.luban.service.User@5e025e70
com.luban.service.User@1b0375b3

所以在BeanPostProcessor中可以完全替换掉某个beanName对应的bean对象。

⽽BeanPostProcessor的执⾏在Bean的⽣命周期中是处于属性注⼊之后的,循环依赖是发⽣在属性注⼊ 过程中的,所以很有可能导致,注⼊给B对象的A对象和经历过完整⽣命周期之后的A对象,不是⼀个对 象。这就是有问题的

所以在这种情况下的循环依赖,Spring是解决不了的,因为在属性注⼊时,Spring也不知道A对象后续会 经过哪些BeanPostProcessor以及会对A对象做什么处理。

Spring到底解决了哪种情况下的循环依赖

虽然上⾯的情况可能发⽣,但是肯定发⽣得很少,我们通常在开发过程中,不会这样去做,但是,某个 beanName对应的最终对象和原始对象不是⼀个对象却会经常出现,这就是AOP。

AOP就是通过⼀个BeanPostProcessor来实现的,这个BeanPostProcessor就是 AnnotationAwareAspectJAutoProxyCreator,它的⽗类是AbstractAutoProxyCreator,⽽在Spring中 AOP利⽤的要么是JDK动态代理,要么CGLib的动态代理,所以如果给⼀个类中的某个⽅法设置了切⾯, 那么这个类最终就需要⽣成⼀个代理对象。

⼀般过程就是:A类—>⽣成⼀个普通对象–>属性注⼊–>基于切⾯⽣成⼀个代理对象–>把代理对象放 ⼊singletonObjects单例池中

⽽AOP可以说是Spring中除开IOC的另外⼀⼤功能,⽽循环依赖⼜是属于IOC范畴的,所以这两⼤功能想 要并存,Spring需要特殊处理。

如何处理的,就是利⽤了第三级缓存singletonFactories。

⾸先,singletonFactories中存的是某个beanName对应的ObjectFactory,在bean的⽣命周期中,⽣成 完原始对象之后,就会构造⼀个ObjectFactory存⼊singletonFactories中。这个ObjectFactory是⼀个函 数式接⼝,所以⽀持Lambda表达式:() -> getEarlyBeanReference(beanName, mbd, bean)

上⾯的Lambda表达式就是⼀个ObjectFactory,执⾏该Lambda表达式就会去执⾏ getEarlyBeanReference⽅法,⽽该⽅法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码protected Object getEarlyBeanReference(String beanName, RootBeanD
efinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProces
sors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProc
essor) {
SmartInstantiationAwareBeanPostProcessor ibp = (S
martInstantiationAwareBeanPostProcessor) bp;
exposedObject = ibp.getEarlyBeanReference(exposed
Object, beanName);
}
}
}
return exposedObject;
}

该⽅法会去执⾏SmartInstantiationAwareBeanPostProcessor中的getEarlyBeanReference⽅法,⽽这 个接⼝下的实现类中只有两个类实现了这个⽅法,⼀个是AbstractAutoProxyCreator,⼀个是 InstantiationAwareBeanPostProcessorAdapter,它的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码// InstantiationAwareBeanPostProcessorAdapter
@Override
public Object getEarlyBeanReference(Object bean, String beanName)
throws BeansException {
return bean;
}

// AbstractAutoProxyCreator
@Override
public Object getEarlyBeanReference(Object bean, String beanName)
{
Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.put(cacheKey, bean);
return wrapIfNecessary(bean, beanName, cacheKey);
}

所以很明显,在整个Spring中,默认就只有AbstractAutoProxyCreator真正意义上实现了 getEarlyBeanReference⽅法,⽽该类就是⽤来进⾏AOP的。上⽂提到的 AnnotationAwareAspectJAutoProxyCreator的⽗类就是AbstractAutoProxyCreator。

那么getEarlyBeanReference⽅法到底在⼲什么? ⾸先得到⼀个cachekey,cachekey就是beanName。 然后把beanName和bean(这是原始对象)存⼊earlyProxyReferences中 调⽤wrapIfNecessary进⾏AOP,得到⼀个代理对象。

那么,什么时候会调⽤getEarlyBeanReference⽅法呢?回到循环依赖的场景中

左边⽂字:

这个ObjectFactory就是上⽂说的labmda表达式,中间有getEarlyBeanReference⽅法,注意存⼊ singletonFactories时并不会执⾏lambda表达式,也就是不会执⾏getEarlyBeanReference⽅法

右边⽂字:

从singletonFactories根据beanName得到⼀个ObjectFactory,然后执⾏ObjectFactory,也就是执⾏ getEarlyBeanReference⽅法,此时会得到⼀个A原始对象经过AOP之后的代理对象,然后把该代理对象 放⼊earlySingletonObjects中,注意此时并没有把代理对象放⼊singletonObjects中,那什么时候放⼊ 到singletonObjects中呢?

我们这个时候得来理解⼀下earlySingletonObjects的作⽤,此时,我们只得到了A原始对象的代理对象, 这个对象还不完整,因为A原始对象还没有进⾏属性填充,所以此时不能直接把A的代理对象放⼊ singletonObjects中,所以只能把代理对象放⼊earlySingletonObjects,假设现在有其他对象依赖了A, 那么则可以从earlySingletonObjects中得到A原始对象的代理对象了,并且是A的同⼀个代理对象。

当B创建完了之后,A继续进⾏⽣命周期,⽽A在完成属性注⼊后,会按照它本身的逻辑去进⾏AOP,⽽此 时我们知道A原始对象已经经历过了AOP,所以对于A本身⽽⾔,不会再去进⾏AOP了,那么怎么判断⼀个 对象是否经历过了AOP呢?会利⽤上⽂提到的earlyProxyReferences,在AbstractAutoProxyCreator的 postProcessAfterInitialization⽅法中,会去判断当前beanName是否在earlyProxyReferences,如果 在则表示已经提前进⾏过AOP了,⽆需再次进⾏AOP。

对于A⽽⾔,进⾏了AOP的判断后,以及BeanPostProcessor的执⾏之后,就需要把A对应的对象放⼊ singletonObjects中了,但是我们知道,应该是要A的代理对象放⼊singletonObjects中,所以此时需要 从earlySingletonObjects中得到代理对象,然后⼊singletonObjects中。

整个循环依赖解决完毕

总结

⾄此,总结⼀下三级缓存:

  1. singletonObjects:缓存某个beanName对应的经过了完整⽣命周期的bean
  1. earlySingletonObjects:缓存提前拿原始对象进⾏了AOP之后得到的代理对象,原始对象还没有进⾏ 属性注⼊和后续的BeanPostProcessor等⽣命周期
  1. singletonFactories:缓存的是⼀个ObjectFactory,主要⽤来去⽣成原始对象进⾏了AOP之后得到的 代理对象,在每个Bean的⽣成过程中,都会提前暴露⼀个⼯⼚,这个⼯⼚可能⽤到,也可能⽤不到, 如果没有出现循环依赖依赖本bean,那么这个⼯⼚⽆⽤,本bean按照⾃⼰的⽣命周期执⾏,执⾏完后 直接把本bean放⼊singletonObjects中即可,如果出现了循环依赖依赖了本bean,则另外那个bean执 ⾏ObjectFactory提交得到⼀个AOP之后的代理对象(如果有AOP的话,如果⽆需AOP,则直接得到⼀ 个原始对象)。
  1. 其实还要⼀个缓存,就是earlyProxyReferences,它⽤来记录某个原始对象是否进⾏过AOP了。

本文转载自: 掘金

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

给你介绍下,Hippo4J 动态线程池基础架构

发表于 2021-11-08

很多小伙伴知道小编从今年六月份开始,陆陆续续开始提交 Hippo4J 动态线程池项目

经过 200+ 的 Commit,也是快要能发布 1.0.0 正式版本,今天就写一篇文章正式介绍下 Hippo4J 的项目架构

Hippo4J GitHub:github.com/acmenlt/dyn…

小伙伴如果访问 GitHub 速度慢,可以通过改 Host 的方式提高访问速度,修改 Host 方案

  1. 架构设计

简单来说,Hippo4J 从部署的角度上分为两种角色:Server 端和 Client 端

Server 端是 Hippo4J 项目打包出的 Java 进程,功能包括用户权限、线程池监控以及执行持久化的动作

Client 端指的是我们 SpringBoot 应用,通过引入 Hippo4J Starter Jar 包负责与 Server 端进行交互

比如拉取 Server 端线程池数据、动态更新线程池配置以及采集上报线程池运行时数据等

  1. 基础组件

2.1 配置中心(Config)

配置中心位于 Server 端,它的主要作用是监控 Server 端线程池配置变更,实时通知到 Client 实例执行线程池变更流程

代码设计基于 Nacos 1.x 版本的 长轮询以及异步 Servlet 机制 实现

2.2 注册中心(Discovery)

负责管理 Client 端(单机或集群)注册到 Server 端的实例,包括不限于实例注册、续约、过期剔除 等操作,代码基于 Eureka 源码实现

上面的配置中心很容易理解,动态线程池参数变更的根本。但是注册中心是用来做什么的?

注册中心管理 Client 端注册的实例,通过这些实例可以 实时获取线程池的运行时参数信息

目前的设计是如此,不排除后续基于 Discovery 做更多的扩展

2.3 控制台(Console)

对接前端项目,包括不限于以下模块管理

2.4 抽象工具(Tools)

顾名思义就是将某些工具单独抽象出来,并以 Module 的形式进行展现,这样的拆分方式有两点好处:一是更符合职责分离特性,二是需要用到某块功能,做到拿来即用

目前已集成两块内容:

  1. log-record-tool:基于 mzt-biz-log 的操作日志变更记录组件
  2. open-change-tool:监控 Hippo4J 项目在 GitHub 的 Star Fork 变更,默认五分钟内有变更则通知
  1. 消息通知(Notify)

Hippo4J 内置了很多需要通知的事件,比如:线程池参数变更通知、线程池活跃度报警、拒绝策略执行报警以及阻塞队列容量报警等

目前 Notify 已经接入了钉钉,后续持续集成企业微信、邮件、短信等通知渠道;并且,Notify 模块提供了消息事件的 SPI 方案,可以接受三方自定义的推送

  1. Hippo4j-Spring-Boot-Starter

熟悉 SpringBoot 的小伙伴对 Starter 应该不会陌生。Hippo4J 提供以 Starter Jar 包的形式嵌套在应用内,负责与 Server 端完成交互

Starter Jar 包推送到 Maven 公共仓库,目前公共仓库已存在 0.0.2 版本的 Jar

  1. SpringBoot 快速开始

5.1 Server 端启动

导入 Hippo4J 初始化 SQL 语句

Hippo4J 代码拉至本地,启动 Server 模块下 ServerApplication 应用类

5.2 SpringBoot 引入 Hippo4j Starter

SpringBoot 应用引入 Hippo4j Starter Jar。备注:0.0.2 版本仅是过渡期版本,正式请等待发布 1.0.0

1
2
3
4
5
xml复制代码<dependency>
   <groupId>io.github.acmenlt</groupId>
   <artifactId>hippo4j-spring-boot-starter</artifactId>
   <version>0.0.2</version>
</dependency>

SpringBoot 应用添加 Hippo4J 相关配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
yaml复制代码spring:
profiles:
  active: dev
application:
  name: dynamic-threadpool-example
dynamic:
  thread-pool:
    notifys:
      - type: DING
        url: https://oapi.dingtalk.com/robot/send?access_token=
         # 此处可以选择自己的钉钉群
        token: 4a582a588a161d6e3a1bd1de7eea9ee9f562cdfcbe56b6e72029e7fd512b2eae
         # 通知时 @ 人员
        receives: '15601166691'
     # 报警发送间隔
    alarm-interval: 30
     # 服务端地址
    server-addr: http://localhost:6691
     # 租户 id, 对应 tenant 表
    namespace: prescription
     # 项目 id, 对应 item 表
    item-id: ${spring.application.name}

添加线程池配置类,动态线程池支持两种创建方式

  1. DynamicThreadPoolWrapper 包装器创建,指定线程池标识
  2. @DynamicThreadPool 注解修饰 Spring Bean

Spring 后置处理器会扫描这两种方式创建的 Bean,拿到线程池 ID 调用 Server 端获取配置

如果获取 Server 端配置失败,根据默认线程池创建实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Configuration
public class ThreadPoolConfig {
   public static final String MESSAGE_PRODUCE = "message-produce";
   public static final String MESSAGE_CONSUME = "message-consume";
​
   @Bean
   // {@link DynamicThreadPoolWrapper} 完成 Server 端订阅配置功能.
   public DynamicThreadPoolWrapper messageCenterDynamicThreadPool() {
       return new DynamicThreadPoolWrapper(MESSAGE_CONSUME);
  }
​
   @Bean
   @DynamicThreadPool
   // 通过 {@link DynamicThreadPool} 修饰 {@link DynamicThreadPoolExecutor} 完成 Server 端订阅配置功能.
   // 由动态线程池注解修饰后, IOC 容器中保存的是 {@link DynamicThreadPoolExecutor}
   public ThreadPoolExecutor dynamicThreadPoolExecutor() {
       return ThreadPoolBuilder.builder().threadFactory(MESSAGE_PRODUCE).dynamicPool().build();
  }
}

启动 SpringBoot 应用后,动态线程池的准备工作就算完成了

5.3 测试线程池动态变更

通过接口修改线程池中的配置。HTTP POST 路径:http://localhost:6691/v1/cs/configs ,Body 请求体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
json复制代码{
   "ignore": "tenantId、itemId、tpId 代表唯一线程池,请不要修改",
   "tenantId": "prescription",
   "itemId": "dynamic-threadpool-example",
   "tpId": "message-produce",
   "coreSize": 10,
   "maxSize": 15,
   "queueType": 9,
   "capacity": 100,
   "keepAliveTime": 10,
   "rejectedType": 3,
   "isAlarm": 0,
   "capacityAlarm": 81,
   "livenessAlarm": 82
}

接口调用成功后,观察 IDEA Client 控制台日志输出,日志输出包括不限于此信息即为成功

1
ini复制代码[🔥 MESSAGE-PRODUCE] Changed thread pool. coreSize :: [11=>10], maxSize :: [15=>15], queueType :: [9=>9], capacity :: [100=>100], keepAliveTime :: [10000=>10000], rejectedType :: [7=>7]

另外,当 Client 集群部署时,可以选择修改所有实例或某一实例。修改请求路径:http://localhost:6691/v1/cs/configs?identify=xxx ,Body 体同上

identify 参数如何获取?每一台 Client 端都会分配到独一无二的值,并在启动时进行打印

1
arduino复制代码Client identity :: xxxxxx

identify 参数不传或为空,会修改该线程池 Client 集群下该线程池所有实例参数

5.4 报警通知

如果加入了钉钉群(号码:31764717)的小伙伴,此时就能收到一条钉钉机器人的推送通知,示例如下:

  1. 最后

在 GitHub 上,检验项目的质量如何,Star 数占了一定因素;从上次 Hippo4J 登上 GitHub Trending 至今,已收获 400+ Star 数,进而证明了 Hippo4J 的项目质量

随着时间的推移,更多的小伙伴关注到 Hippo4J 项目,提出了相关的功能建议,以及希望参与项目共建,整体显得朝气蓬勃

下图来自小伙伴的问题以及建议,提的内容都非常好 👍👍👍

如果说看了上面的介绍,让你对 Hippo4J 产生了兴趣,通过以下方式联系到小编,不仅可以解答你对 Hippo4J 项目的疑惑,同时也接受正向的功能建议

微信:m7798432 备注:GitHub

一直以来,Hippo4J 目标一直都是企业级应用,小编也不断在向这个方向对齐,发布 1.0 的时间不会太远

屏幕前的小伙伴,如果觉得项目功能规划、代码设计还不错的话,辛苦点个 🚀 Star ,方便后续查看

对于这个项目,是否有什么不一样看法,欢迎在评论区一起沟通交流~

本文转载自: 掘金

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

队列的实现

发表于 2021-11-08

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

💦 队列的概念及结构

1
2
3
4
sql复制代码相比栈,队列的特性和栈是相反的。
它只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO (First In First Out) 的特性。
入队列:进行插入操作的一端称为队尾;出队列:进行删除操作的一端称为队头
对于队列来说,一种入队顺序,只有一种出队顺序

在这里插入图片描述

队列的拓展:

实际中我们有时还会使用一种队列叫循环队列。如操作系统课程讲解生产者消费者模型时可以使用循环队列。环形队列可以使用数组实现,也可以使用循环链表实现。

在这里插入图片描述

为了能使用Q.rear == Q.front 来区别是队空还是队满,我们常常认为出现左图时的情况即为队空的情况,此时: rear == front;而右图的情况即为队满的情况,此时:rear + 1 == front

在这里插入图片描述

关于环形队列在下面的的栈和队列面试题中会讲到

队列的应用:

1️⃣ 实际中要保证公平排队的地方可以用它

2️⃣ 广度优先遍历

💦 队列的实现

这里对于队列的实现我们使用链表的方式
在这里插入图片描述

1.初始化

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
c复制代码void QueueInit(Queue* pq)
{
assert(pq);
//把2个指针置空
pq->phead = pq->ptail = NULL;
}
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
c复制代码void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
//malloc空间,如果需要频繁的开辟空间建议再实现一个BuyQueueNode用于malloc
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
if (newnode == NULL)
{
printf("malloc fail\n");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
//第一次插入
if (pq->phead == NULL)
{
pq->phead = pq->ptail = newnode;
}
//非第一次插入
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
}
3.判空

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
c复制代码bool QueueEmpty(Queue* pq)
{
assert(pq);
//空链表返回true,非空链表返回false
return pq->phead == NULL;
}
4.删除

函数原型
在这里插入图片描述

函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
c复制代码void QueuePop(Queue* pq)
{
assert(pq);
//链表为空时不能删除
assert(!QueueEmpty(pq));
//只有一个节点的情况
if (pq->phead->next == NULL)
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
//多个节点的情况
else
{
QueueNode* next = pq->phead->next;
free(pq->phead) ;
pq->phead = next;
}
}
5.长度

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
c复制代码QDataType QueueSize(Queue* pq)
{
assert(pq);
//如果需要频繁的调用QueueSize这个接口,可以在Queue这个结构体中增加一个成员用于记录长度
int sz = 0;
QueueNode* cur = pq->phead;
while (cur)
{
sz++;
cur = cur->next;
}
return sz;
}
6.取头

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
7
8
c复制代码QDataType QueueFront(Queue* pq)
{
assert(pq);
//链表为空时不能取头
assert(!QueueEmpty(pq));

return pq->phead->data;
}
7.取尾

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
7
8
c复制代码QDataType QueueBack(Queue* pq)
{
assert(pq);
//链表为空时不能取尾
assert(!QueueEmpty(pq));

return pq->ptail->data;
}
8.销毁

函数原型

在这里插入图片描述

函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c复制代码void QueueDestory(Queue* pq)
{
assert(pq);

QueueNode* cur = pq->phead;
//遍历链表
while (cur)
{
QueueNode* next = cur->next;
free(cur);
cur = next;
}
pq->phead = pq->ptail = NULL;
}

💦 完整代码

这里需要三个文件

1️⃣ Queue.h,用于函数的声明

2️⃣ Queue.c,用于函数的定义

3️⃣ Test.c,用于测试函数


🧿 Queue.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
c复制代码#pragma once

//头
#include<stdio.h>
#include<assert.h>
#include<stdbool.h>
#include<stdlib.h>

//结构体
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next; //指向下一个节点
QDataType data; //存储整型数据
}QueueNode;

typedef struct Queue
{
QueueNode* phead;//头指针
QueueNode* ptail;//尾指针
}Queue;

//函数
void QueueInit(Queue* pq);
void QueuePush(Queue* pq, QDataType x);
bool QueueEmpty(Queue* pq);
void QueuePop(Queue* pq);
QDataType QueueSize(Queue* pq);
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
void QueueDestory(Queue* pq);
🧿 Queue.c
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
c复制代码#include"Queue.h"

void QueueInit(Queue* pq)
{
assert(pq);
//把2个指针置空
pq->phead = pq->ptail = NULL;
}
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
//malloc空间,如果需要频繁的开辟空间建议再实现一个BuyQueueNode用于malloc
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
if (newnode == NULL)
{
printf("malloc fail\n");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
//第一次插入
if (pq->phead == NULL)
{
pq->phead = pq->ptail = newnode;
}
//非第一次插入
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
//空链表返回true,非空链表返回false
return pq->phead == NULL;
}
void QueuePop(Queue* pq)
{
assert(pq);
//链表为空时不能删除
assert(!QueueEmpty(pq));
//只有一个节点的情况
if (pq->phead->next == NULL)
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
//多个节点的情况
else
{
QueueNode* next = pq->phead->next;
free(pq->phead) ;
pq->phead = next;
}
}
QDataType QueueSize(Queue* pq)
{
assert(pq);
//如果需要频繁的调用QueueSize这个接口,可以在Queue这个结构体中增加一个成员用于记录长度
int sz = 0;
QueueNode* cur = pq->phead;
while (cur)
{
sz++;
cur = cur->next;
}
return sz;
}
QDataType QueueFront(Queue* pq)
{
assert(pq);
//链表为空时不能取头
assert(!QueueEmpty(pq));

return pq->phead->data;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
//链表为空时不能取尾
assert(!QueueEmpty(pq));

return pq->ptail->data;
}
void QueueDestory(Queue* pq)
{
assert(pq);

QueueNode* cur = pq->phead;
//遍历链表
while (cur)
{
QueueNode* next = cur->next;
free(cur);
cur = next;
}
pq->phead = pq->ptail = NULL;
}
🧿 Test.c
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
c复制代码#include"Queue.h"

int main()
{
//1、传二级指针
//2、返回值
//3、带哨兵位的头节点
//4、嵌套结构体 (这里使用这种方式)
QueueNode q;
//初始化
QueueInit(&q);
//插入
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
QueuePush(&q, 4);
//删除
QueuePop(&q);
QueuePop(&q);
//取头
QueueFront(&q);
//取尾
QueueBack(&q);
//释放
QueueDestory(&q);

return 0;
}

本文转载自: 掘金

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

Java NIO系列教程一

发表于 2021-11-08

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

欢迎关注公众号OpenCoder,来和我做朋友吧~❤😘😁🐱‍🐉👀

一、 java NIO概述

1.1 NIO的基本作用

  • 替代java io的一个操作
  • 面向缓冲区也可以基于通道操作
  • 更高效的进行文件的读写操作

1.2 阻塞 IO

读或者写数据的时候,会阻塞直到数据能够正常的读或者写入在传统的方法中,服务器为客户端建立一个线程,这种模式如果线程增加,大量线程会造成服务器的开销,为了解决这种问题,采用了线程池,并设置线程池的上限,但超出线程池的上限的线程就会访问不上

1.3 非阻塞 IO(NIO)

​ 非阻塞指的是 IO 事件本身不阻塞,是获取 IO 事件的 select()方法是需要阻塞等待的,区别是阻塞的 IO 会阻塞在 IO 操作上, NIO 阻塞在事件获取上,没有事件就没有 IO,select()阻塞的时候 IO 还没有发生,何谈 IO 的阻塞。本质是延迟io操作,真正发生io的时候才执行,而不是发生的时候再阻塞。用Selector负责去监听多个通道,注册感兴趣的特定 I/O 事件,之后系统进行通知.

当有读或写等任何注册的事件发生时,可以从 Selector 中获得相应的 SelectionKey,同时从 SelectionKey 中可以找到发生的事件和该事件所发生的具体的 SelectableChannel,以获得客户端发送过来的数据。

IO NIO
面向流 面向缓冲区
阻塞IO 非阻塞IO
无 选择器

1.4 NIO 概述

java NIO 由以下几个核心部分组成,还有其他组件(pipe、filelock)

  • Channel(双向的,既可以用来进行读操作,又可以用来进行写操作) 主要有如下: FileChannel(IO)、DatagramChannel(UDP )、 SocketChannel (TCP中Server )和 ServerSocketChannel(TCP中Client)
  • Buffer 主要有如下: ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer
  • Selector(处理多个 Channel)

二、Channel

  • 可以进行读取和写入,或者进行读写操作,全双工
  • 操作的数据源可以多种,比如文件、网络socket
  • Channel 用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据
  • 从通道读取数据到缓冲区,从缓冲区写入数据到通道

Java NIO 的通道类似流,但又有些不同:

  • 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
  • 通道可以异步地读写。
  • 通道中的数据总是要先读到一个 Buffer,或者总是要从一个 Buffer 中写入

主要是接口实现,不同操作系统不同接口实现,通过代码也可以看到其代码为接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码public interface Channel extends Closeable {

    /**
     * Tells whether or not this channel is open.
     *
     * @return {@code true} if, and only if, this channel is open
     */
    public boolean isOpen();

    /**
     * Closes this channel.
     *
     * <p> After a channel is closed, any further attempt to invoke I/O
     * operations upon it will cause a {@link ClosedChannelException} to be
     * thrown.
     *
     * <p> If this channel is already closed then invoking this method has no
     * effect.
     *
     * <p> This method may be invoked at any time.  If some other thread has
     * already invoked it, however, then another invocation will block until
     * the first invocation is complete, after which it will return without
     * effect. </p>
     *
     * @throws  IOException  If an I/O error occurs
     */
    public void close() throws IOException;

}

实现接口主要有以下几个常用类:

  • FileChannel 从文件中读写数据。
  • DatagramChannel 能通过 UDP 读写网络中的数据。
  • SocketChannel 能通过 TCP 读写网络中的数据。
  • ServerSocketChannel 可以监听新进来的 TCP 连接,像 Web 服务器那样。对每一个新进来的连接都会创建一个 SocketChannel

2.1 FileChannel

主要是文件IO也是最常用的一个类,以下是FileChannel类的核心方法和主要的作用:

Buffer 通常的操作

  1. 将数据写入缓冲区
  2. 调用 buffer.flip() 反转读写模式
  3. 从缓冲区读取数据
  4. 调用 buffer.clear() 或 buffer.compact() 清除缓冲区内容

部分步骤代码展示:

  1. 先打开文件,无法直接打开一个

FileChannel,需要通过使用一个 InputStream、OutputStream 或RandomAccessFile 来获取一个 FileChannel

1
2
3
java复制代码//创建FileChannel
RandomAccessFile aFile = new RandomAccessFile("b://1.txt","rw");
FileChannel channel = aFile.getChannel();
  1. 创建Buffer
1
java复制代码ByteBuffer buf = ByteBuffer.allocate(1024);
  1. 从 FileChannel 读取数据

read()方法返回的 int 值表示了有多少字节被读到了 Buffer 中。如果返回-1,表示到了文件末尾

1
java复制代码int bytesRead = channel.read(buf);
  1. FileChannel.write()方法向 FileChannel 写数据,该方法的参数是一个 Buffer。在 while 循环中调用的。因为无法保证 write()方法一次能向 FileChannel 写入多少字节,因此需要重复调用 write()方法,直到 Buffer 中已经没有尚未写入通道的字节。

读数据主要的代码思路步骤是:

  1. 创建一个FileChannel
  2. 创建一个数据缓冲区
  3. 读取数据到缓冲区中
  4. 判断数据是否有,如果有,则取出,判断的依据是获取到的数据是否为-1,取出的数据要先反转读写操作,之后如果数据缓冲区还有,则取出,最后清除数据缓冲区后在判断是否缓冲区还有数据。关闭FileChannel

完整代码展示

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 class FileChannelDemo1 {
    //FileChannel读取数据到buffer中
    public static void main(String[] args) throws Exception {
        //创建FileChannel
        RandomAccessFile aFile = new RandomAccessFile("d://opencoder.txt","rw");
        FileChannel channel = aFile.getChannel();

        //创建Buffer
        ByteBuffer buf = ByteBuffer.allocate(1024);

        //读取数据到buffer中
        int bytesRead = channel.read(buf);
        while(bytesRead != -1) {
            System.out.println("读取了:"+bytesRead);
            buf.flip();
            while(buf.hasRemaining()) {
                System.out.println((char)buf.get());
            }
            buf.clear();
            bytesRead = channel.read(buf);
        }
        aFile.close();
        System.out.println("结束了");
    }
}

写数据主要代码思路是:

  1. 创建一个FileChannel
  2. 创建一个数据缓冲区
  3. 创建要写入的数据对象,以及清空以下缓冲区(防止出错)
  4. 要写入的数据写入到缓冲区中
  5. 缓冲区读写反转
  6. 判断缓冲区是否有数据,将数据一个一个写入到FileChannel
  7. 关闭FileChannel

完整代码展示:

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
java复制代码//FileChanne写操作
public class FileChannelDemo2 {

    public static void main(String[] args) throws Exception {
        // 打开FileChannel
        RandomAccessFile aFile = new RandomAccessFile("d://opencoder.txt","rw");
        FileChannel channel = aFile.getChannel();

        //创建buffer对象
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        String newData = "manongyanjiuseng";
        buffer.clear();

        //写入内容
        buffer.put(newData.getBytes());

        buffer.flip();

        //FileChannel完成最终实现
        while (buffer.hasRemaining()) {
            channel.write(buffer);
        }

        //关闭
        channel.close();
    }
}

2.2 其他常用方法

  1. position方法

需要在 FileChannel 的某个特定位置进行数据的读/写操作。可以通过调用position()方法获取 FileChannel 的当前位置。也可以通过调用 position(long pos)方法设置 FileChannel 的当前位置

注意这样设置会造成两个后果: 位置如果设置在文件结束符之后,读取数据的文件结束标志返回-1,而且写入数据的时候前面会有间隙,导致文件空洞

1
2
java复制代码long pos = channel.position();
channel.position(pos +404);
  1. size 方法

返回该实例所关联文件的大小
3. truncate 方法

截取一个文件。截取文件时,文件将中指定长度,后面的部分将被删除,而且截取的数据长度是以字节截取
4. force 方法

尚未写入磁盘的数据强制写到磁盘上
5. transferTo 和 transferFrom 方法

进行通道之间的传输注意一个To与From的区别,一个主动一个被动。

以下是transferFrom 的完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码//通道之间数据传输
public class FileChannelDemo3 {

    //transferFrom()
    public static void main(String[] args) throws Exception {
        // 创建两个fileChannel
        RandomAccessFile aFile = new RandomAccessFile("d://opencoder.txt","rw");
        FileChannel fromChannel = aFile.getChannel();

        RandomAccessFile bFile = new RandomAccessFile("d://opencoder2.txt","rw");
        FileChannel toChannel = bFile.getChannel();

        //fromChannel 传输到 toChannel
        long position = 0;
        long size = fromChannel.size();
        toChannel.transferFrom(fromChannel,position,size);

        aFile.close();
        bFile.close();
        System.out.println("over!");
    }
}

以下是transferTo的完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码//通道之间数据传输
public class FileChannelDemo4 {

    //transferTo()
    public static void main(String[] args) throws Exception {
        // 创建两个fileChannel
        RandomAccessFile aFile = new RandomAccessFile("d://opencoder1.txt","rw");
        FileChannel fromChannel = aFile.getChannel();

        RandomAccessFile bFile = new RandomAccessFile("d://opencoder2.txt","rw");
        FileChannel toChannel = bFile.getChannel();

        //fromChannel 传输到 toChannel
        long position = 0;
        long size = fromChannel.size();
        fromChannel.transferTo(0,size,toChannel);

        aFile.close();
        bFile.close();
        System.out.println("over!");
    }
}

总结

​ 今天主要给大家介绍的是NIO的基本的概念以及Channel中常用的FileChannel的基本的用法,算是对Channel有一个简单的介绍。下一篇文章我们将详细的为大家介绍其他的常用Channel。

欢迎关注公众号OpenCoder,来和我做朋友吧~❤😘😁🐱‍🐉👀

本文转载自: 掘金

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

1…399400401…956

开发者博客

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