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

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


  • 首页

  • 归档

  • 搜索

面试官:高并发场景下,你们是怎么保证数据的一致性的?

发表于 2021-05-15

面试的时候,总会遇到这么一个场景。

  1. 场景分析

面试官:你们的服务的QPS是多少?

我:我们的服务高峰期访问量还挺大的,大约是3万吧。

面试官:这么大的访问量,你们的服务器能撑住吗?有加缓存吗?

我:有的,我们使用了Redis做缓存,接口优先查询缓存,缓存不存在,才访问数据库。这样可以减少数据库访问压力,加快查询效率。

面试官:一份数据存储在两个地方,更新数据的时候,你们是怎么保证数据的一致性的?

看到了吧,好的面试官一般不直接问你数据一致性的解决方案,而是循循善诱,结合具体的使用场景,再问你解决方法。如果你没做过这方面,没有线上的实战经验,一般很难回答的有条理性、有思考性。

保证数据一致性,一般有这4种方法:

  1. 先更新缓存,再更新数据库。
  2. 先更新数据库,再更新缓存。
  3. 先删除缓存,再更新数据库。
  4. 先更新数据库,再删除缓存。

每种方案都详细的讨论一下:

  1. 解决方案

2.1 先更新缓存,再更新数据库

如果同时来了两个并发写请求,执行过程是这样的:

数据一致性1.jpg

  1. 写请求1更新缓存,设置age为1
  2. 写请求2更新缓存,设置age为2
  3. 写请求2更新数据库,设置age为2
  4. 写请求1更新数据库,设置age为1

执行结果就是,缓存里age被设置2,数据库里的age被设置成1,导致数据不一致,此方案不可行。

2.2 先更新数据库,再更新缓存

如果同时来了两个并发写请求,执行过程是这样的:

  1. 写请求1更新数据库,设置age为1
  2. 写请求2更新数据库,设置age为2
  3. 写请求2更新缓存,设置age为2
  4. 写请求1更新缓存,设置age为1

执行结果就是,数据库里age被设置2,缓存里的age被设置成1,导致数据不一致,此方案不可行。

2.3 先删除缓存,再更新数据库

如果同时来了两个并发读写请求,执行过程是这样的:

数据一致性2.jpg

  1. 写请求删除了缓存
  2. 读请求查询缓存没数据,然后查询数据库,再把数据写到缓存中
  3. 写请求更新数据库

执行结果是,缓存中是旧数据,而数据库里是新数据,导致数据不一致,此方案不可行。

2.4 先更新数据库,再删除缓存

这种方案,在并发写的时候,不会出问题。因为都是先更新数据库再删除缓存,不会出现不一致的情况。

但是在并发读写的时候,还是有可能出现数据不一致。

  1. 读请求查询缓存没数据,然后查询数据库
  2. 写请求更新数据库,删除缓存
  3. 读请求回写缓存

执行结果是,缓存中是旧数据,而数据库里是新数据,导致数据不一致。

其实这种情况出现的概率很低,写缓存比写数据库快出几个量级,读写缓存都是内存操作,速度非常快。

遇到了这种极端场景,我们也需要做一下兜底方案,缓存都要设置过期时间。这种方案属于数据的弱一致性和最终一致性,而不是强一致性。

  1. 总结与思考

有读者可能会好奇,为什么不在更新缓存和数据库方法上加上事务注解,实现强一致性,这么哪种方案都不会有问题。

是的,当我们的服务只在一台机器上,加本地事务是可行的。但是工作中,我们会把一个服务部署到几十台、上百台机器上,有时候为了应对更极端的查询请求,又在Redis缓存加一层本地缓存,这时候我们再用本地事务是不起作用的。

一份数据在多台机器上,存在多个副本,为了实现强一致性,我们也可以使用分布式事务。这样一来更新缓存操作将会变得非常复杂,得不偿失。

但是在另外的一些场景,比如更新订单状态、更新用户资产,这种场景,我们无论付出多大代价也要实现数据的强一致性,具体实现方案一般有以下几种:

  1. 二阶段提交
  2. TCC
  3. 本地消息表
  4. MQ事务消息
  5. 分布式事务中间件

下篇文章咱们再一起详细的分析这几种方案优缺点。

本文转载自: 掘金

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

我应该使用哪个NotNull Java注释?

发表于 2021-05-15

我应该使用哪个@NotNull Java注释?| Java Debug 笔记

本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看<活动链接>

提问:

我希望使我的代码更具可读性,并使用诸如IDE代码检查和 / 或静态代码分析(FindBugs和Sonar——这两个是检查代码规范的工具集,译者注)之类的工具来避免NullPointerExceptions。许多工具似乎彼此互不兼容,在我的代码中列出了这些工具里的@NotNull / @NonNull / @Nonnull注释,所有这些都很难读。你们有没有最佳的建议?这是我发现的等效注释的列表:

  • javax.validation.constraints.NotNull
    创建用于运行时验证,而非静态分析。
    注释文档
  • edu.umd.cs.findbugs.annotations.NonNull
    被 FindBugs (dead project) 和他的继承类 SpotBugs 静态分析和 Sonar (现在叫 Sonarqube)使用

FindBugs 注释文档, SpotBugs 注释文档

  • javax.annotation.Nonnull
    这可能也适用于FindBugs, 但是JSR-305 处于非活动状态(不太被很多人使用的意思*,*译者注)。 (也可以看如下资料: What is the status of JSR 305?) 源码
  • org.jetbrains.annotations.NotNull
    由IntelliJ IDEA IDE进行静态分析。
    注释文档
  • lombok.NonNull
    在Project Lombok中用于控制代码生成。
    占位符注释,因为没有标准。
    源码, 注释文档
  • android.support.annotation.NonNull
    在Android中可用的标记注释,由support-annotations包提供
    注释文档
  • org.eclipse.jdt.annotation.NonNull
    Eclipse用于静态代码分析
    注释文档

回答:

由于JSR 305(其目标是使@NonNull和@Nullable标准化)已经休眠了几年,所以恐怕没有好的答案。我们所能做的就是找到一个务实的解决方案,我的方法如下:

句法

从纯粹的风格角度来看,除了Java本身,我想避免引用任何IDE,框架或任何工具包。

这排除了:

  • android.support.annotation
  • edu.umd.cs.findbugs.annotations
  • org.eclipse.jdt.annotation
  • org.jetbrains.annotations
  • org.checkerframework.checker.nullness.qual
  • lombok.NonNull

剩下的就是javax.validation.constraints或javax.annotation。前者带有JEE。如果这比javax.annotation更好,它最终可能会与JSE一起出现,或者根本不会出现,那是有争议的问题。我个人更喜欢javax.annotation,因为我不喜欢JEE依赖项。

这给我们留下了

javax.annotation

这也是最短的。

只有一种语法会更好:java.annotation.Nullable。随着过去其他软件包从javax升级到java,javax.annotation将是朝着正确方向迈出的一步。

执行

我希望它们都具有基本相同的琐碎实现,但是经过详细的分析表明,事实并非如此。

首先是相似之处:

@NonNull批注都有一行:

1
java复制代码public @interface NonNull {}

除了:

  • org.jetbrains.annotations将其称为@NotNull并具有简单的实现
  • 具有更长实现的javax.annotation
  • javax.validation.constraints也称为@NotNull并具有实现

@Nullableannotations都有一行

1
java复制代码public @interface Nullable{}

再次除了org.jetbrains.annotations以及他们的一些实现。

对于差异:

引人注目的是

  • javax.annotation
  • javax.validation.constraints
  • org.checkerframework.checker.nullness.qual

都有运行时注释 (@Retention(RUNTIME))。而

  • android.support.annotation
  • edu.umd.cs.findbugs.annotations
  • org.eclipse.jdt.annotation
  • org.jetbrains.annotations

只是编译阶段才有 (@Retention(CLASS))。

如此回答所述,运行时批注的影响比人们想象的要小,但是它们的好处是,除了编译时,它还使工具能够执行运行时检查。

另一个重要的区别是注释可以在代码中的何处使用。有两种不同的方法。一些软件包使用JLS 9.6.4.1样式上下文。下表概述了:

1
2
3
4
5
6
kotlin复制代码                                FIELD   METHOD  PARAMETER LOCAL_VARIABLE 
android.support.annotation X X X
edu.umd.cs.findbugs.annotations X X X X
org.jetbrains.annotation X X X X
lombok X X X X
javax.validation.constraints X X X

org.eclipse.jdt.annotation, javax.annotation 和 org.checkerframework.checker.nullness.qua使用JLS 4.11中定义的上下文,我认为这是正确的方法。

这给我们留下了

  • javax.annotation
  • org.checkerframework.checker.nullness.qual

代码示例

为了帮助您自己比较更多详细信息,我在下面列出了每个注释的代码。为了使比较容易,我删除了注释,导入和@Documented批注。 (除了Android包中的类之外,其他所有文件都具有@Documented)。我对这些代码行和@Target字段进行了重新排序,并进行了规范化。

1
2
3
4
java复制代码package android.support.annotation;
@Retention(CLASS)
@Target({FIELD, METHOD, PARAMETER})
public @interface NonNull {}

1
2
3
4
java复制代码package edu.umd.cs.findbugs.annotations;
@Retention(CLASS)
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE})
public @interface NonNull {}

1
2
3
4
java复制代码package org.eclipse.jdt.annotation;
@Retention(CLASS)
@Target({ TYPE_USE })
public @interface NonNull {}

1
2
3
4
java复制代码package org.jetbrains.annotations;
@Retention(CLASS)
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE})
public @interface NotNull {String value() default "";}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package javax.annotation;
@TypeQualifier
@Retention(RUNTIME)
public @interface Nonnull {
When when() default When.ALWAYS;
static class Checker implements TypeQualifierValidator<Nonnull> {
public When forConstantValue(Nonnull qualifierqualifierArgument,
Object value) {
if (value == null)
return When.NEVER;
return When.ALWAYS;
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码package org.checkerframework.checker.nullness.qual;
@Retention(RUNTIME)
@Target({TYPE_USE, TYPE_PARAMETER})
@SubtypeOf(MonotonicNonNull.class)
@ImplicitFor(
types = {
TypeKind.PACKAGE,
TypeKind.INT,
TypeKind.BOOLEAN,
TypeKind.CHAR,
TypeKind.DOUBLE,
TypeKind.FLOAT,
TypeKind.LONG,
TypeKind.SHORT,
TypeKind.BYTE
},
literals = {LiteralKind.STRING}
)
@DefaultQualifierInHierarchy
@DefaultFor({TypeUseLocation.EXCEPTION_PARAMETER})
@DefaultInUncheckedCodeFor({TypeUseLocation.PARAMETER, TypeUseLocation.LOWER_BOUND})
public @interface NonNull {}

为了完整起见,下面是@Nullable实现:

1
2
3
4
java复制代码package android.support.annotation;
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD})
public @interface Nullable {}

1
2
3
4
java复制代码package edu.umd.cs.findbugs.annotations;
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE})
@Retention(CLASS)
public @interface Nullable {}

1
2
3
4
java复制代码package org.eclipse.jdt.annotation;
@Retention(CLASS)
@Target({ TYPE_USE })
public @interface Nullable {}

1
2
3
4
java复制代码package org.jetbrains.annotations;
@Retention(CLASS)
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE})
public @interface Nullable {String value() default "";}

1
2
3
4
5
java复制代码package javax.annotation;
@TypeQualifierNickname
@Nonnull(when = When.UNKNOWN)
@Retention(RUNTIME)
public @interface Nullable {}

1
2
3
4
5
6
7
8
9
10
java复制代码package org.checkerframework.checker.nullness.qual;
@Retention(RUNTIME)
@Target({TYPE_USE, TYPE_PARAMETER})
@SubtypeOf({})
@ImplicitFor(
literals = {LiteralKind.NULL},
typeNames = {java.lang.Void.class}
)
@DefaultInUncheckedCodeFor({TypeUseLocation.RETURN, TypeUseLocation.UPPER_BOUND})
public @interface Nullable {}

以下两个软件包没有@Nullable,因此我将它们分开列出;Lombox有一个很少用的@NonNul。在javax.validation.constraints中,@NonNull实际上是一个@NotNull,并且执行耗时很长。

1
2
3
4
java复制代码package lombok;
@Retention(CLASS)
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE})
public @interface NonNull {}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package javax.validation.constraints;
@Retention(RUNTIME)
@Target({ FIELD, METHOD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Constraint(validatedBy = {})
public @interface NotNull {
String message() default "{javax.validation.constraints.NotNull.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default {};
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@interface List {
NotNull[] value();
}
}

支持

根据我的经验,Eclipse和Checker Framework至少是开箱即用地支持javax.annotation。

总结

我理想的注释是Checker Framework实现的java.annotation语法。

如果您不打算使用Checker Framework,则暂时最好还是使用javax.annotation(JSR-305)。

如果您愿意尝试Checker Framework,请使用其org.checkerframework.checker.nullness.qual。

用到的源码

  • android.support.annotation from android-5.1.1_r1.jar
  • edu.umd.cs.findbugs.annotations from findbugs-annotations-1.0.0.jar
  • org.eclipse.jdt.annotation from org.eclipse.jdt.annotation_2.1.0.v20160418-1457.jar
  • org.jetbrains.annotations from jetbrains-annotations-13.0.jar
  • javax.annotation from gwt-dev-2.5.1-sources.jar
  • org.checkerframework.checker.nullness.qual from checker-framework-2.1.9.zip
  • lombok from lombok commit f6da35e4c4f3305ecd1b415e2ab1b9ef8a9120b4
  • javax.validation.constraints from validation-api-1.0.0.GA-sources.jar

原文链接

本文转载自: 掘金

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

定时任务不在硬编码,动态定时刷起来 Java Debug

发表于 2021-05-15

本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看<活动链接>

前言

  • 传统定时器是硬编码。但是有的时候业务上需要不断的调整

问题描述

  • 我们开发了一个定闹钟的功能。这个功能肯定是定时器开发。但是这就存在一个问题这个定时是动态的。那么我们如何实现呢?请接着看

简介

  • 定时器在开发中真的算是一种福利了。通过定时器我们省去了很多人力。我们通过定时器将一些繁琐定期的事情通过代码去完成。在Java开发中我们通过Timer类可以简单实现定时器功能。既然是springboot课程今天我们就来看看srpingboot整合定时器的事情

传统定时器

  • 这里使用的是之前课程一的配置。springboot打算是系列讲解。所以配置都是承前启后的。建议大家按顺序观看。
1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码@Component
public class SimpleSchedule {

@Autowired
TestMapper testMapper;

@Scheduled(cron = "*/6 * * * * ?")
private void process() {
List<Test> tests = testMapper.getTests();
System.out.println(tests);
}
}
  • 定时器的编写也很简单,只需要在类或者方法上加上@Scheduled注解。然后配置cron表达式就可以了。这里得注意一下需要在spirngboot启动类上加上开发定时器的注解。
1
2
3
4
5
6
7
java复制代码
@SpringBootApplication
public class CrontabApplication {
public static void main(String[] args) {
SpringApplication.run(CrontabApplication.class, args);
}
}

  • 代码中我们使用的是最简单的一种方式。
  • cron表达式:指定任务在特定时间执行
  • fixedDelay:表示上一次任务执行完成后多久再执行,参数类型long,单位:ms
  • fixedDelayString:与fixedDelay一样,只是参数类型是String
  • fixedRate:表示按一定的频率执行任务,参数类型long,单位:ms 如: fixedRate(5000),表示这个定时器任务每5秒执行一次
  • fixedRateString:与fixedRate一样,只是参数类型变为String
  • initialDelay:表示延迟多久再第一次执行任务,参数类型为long ,单位:ms
  • initialDelayString:与initialDelay一样,只是参数类型String

#动态定时器

  • 上面的定时器已经成功的配置了。但是现在有一个需求客户想通过页面定制配置定时器执行的频率。上面代码我们是写死6S执行一次。如果客户想通过可视化配置。配置完成之后我总不能在手动改写代码吧。那么动态定时器就产生了。

V1.0

  • 既然动态我们就得将客户配置的数据进行本地化。当然是存储在数据库中。

  • 对应的我们新建Mapper查询定时任务信息。因为这里只配置了表达式。没有配置表达式对应的定时器。也是为了测试。这里默认表达式就是一个。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码
@Configuration
public class ScheduleConfigV1 implements SchedulingConfigurer {

@Autowired
CronMapper cronMapper;
@Autowired
TestMapper testMapper;
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.addTriggerTask(()-> {
System.out.println("执行定时器任务:" + LocalDateTime.now().toLocalTime());
List<Test> tests = testMapper.getTests();
System.out.println(tests);
},
triggerContext -> {
List<Cron> crons = cronMapper.getCron();
Cron cron = crons.get(0);
return new CronTrigger(cron.getCron()).nextExecutionTime(triggerContext);
});
}
}
  • 执行这个代码我们最好先关掉前面那个静态的定时器。这样看的效果明显点。首先数据库配置的是6秒执行一次。然后把数据改成2秒执行一次。看看效果。


  • 我们发现只要数据库信息修改了。定时任务会自动修改频率的。最重要的是不需要重启我们的代码。
  • 上面虽然是动态配置了。但是有一个缺点。就是修改之后生效是在下一次出发定时器执行后有效。说白了就是一开始一小时执行一次,在这期间修改了不能立马生效必须得到下一次一小时才会去刷新配置。这里的动态可以理解成懒动态。

V2.0

  • 上面的功能虽然是动态的。但是对于量产的话肯定是不科学的。首先数据库不可能只存一条数据的。
  • 如果存多条数据那么多条定时规则与具体的定时器这么进行匹配呢?
  • 既然是动态的那么如何通过数据库控制定时器的开关呢?
  • 定时任务的通过代码启动实际是scheduler.schedule(task, new CronTrigger("*/2 * * * * ?"));实现的。这个方法返回的对象是ScheduledFuture。通过canel方法取消定时任务。基于这两个方法我们来改进下我们之前的定时任务。

Registar

  • 首先我们提供一个注册器,注册器的功能就是管理定时任务。提供增加删除功能。在增加定时器的节点上我们调用scheduler.schedule(task, new CronTrigger("*/2 * * * * ?"));来启动定时任务。在删除节点上调用之前获取的ScheduledFuture来canel这个定时任务。这样做的好处我们可以随时控制定时任务的开关
1
2
3
4
java复制代码
public void addCronTask(Runnable task, String cron) {
addCronTask(new CronTask(task,cron));
}
  • 上面添加需要有一个runnable和cron表达式。用一个ConcurrentHashMap来管理添加进来的runnable。runnable为key,ScheduledTask为值。
1
2
3
4
5
6
7
java复制代码
public ScheduledTask scheduleCronTask(CronTask cronTask) {
ScheduledTask scheduledTask;
scheduledTask = new ScheduledTask();
scheduledTask.future = this.taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger());
return scheduledTask;
}
  • 这样构建一个ScheduledTask对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码
public final class ScheduledTask {

public volatile ScheduledFuture<?> future;
/**
* 取消定时任务
*/
public void cancel() {
ScheduledFuture<?> future = this.future;
if (future != null) {
future.cancel(true);
}
}
}
  • 这样我们就可以通过构建一个runnable线程,结合表达式通过注册器注册就可以开启这个线程已固定频率执行。通过remove关闭线程。
1
2
3
java复制代码
SchedulingRunnable task = new SchedulingRunnable(TestMapper.class, "getTests", null);
cronTaskRegistrar.addCronTask(task, "0/10 * * * * ?");
  • 这样做的好处是我们可以在表数据修改的情况下立马更新定时任务规则。

#总结

-上面的代码已经上传值gitee
点我传送
gitee.com/zxhTom/cron…

本文转载自: 掘金

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

JDK18新特性(八):还在重复写空指针检查代码?赶紧使用

发表于 2021-05-15

1、前言

作为一名Java程序员,无论是初入茅庐的菜鸟,还是久经江湖的高手,曾经肯定遭遇过各种各样的异常错误。在国外的一篇文章中,就统计了关于异常类型的排行榜,如下图:

8.Top10Exceptions.png

是的,你没有看错,NullPointerException位居榜首。

Null Reference的发明者Charles Antony Richard Hoare说过:“我称之为我的十亿美元错误。这是1965年发明空引用的结果……这导致了无数的错误,漏洞和系统崩溃,在最近40年中可能造成十亿美元的痛苦和破坏。”

这看起来有些夸张,但毫无争议的是NullPointerException简直就是程序员心中的痛,并不是说它有多难以解决,而是为了解决它我们需要再付出了额外代价。

还记得当初刚入行时候的你,三天两头碰到NullPointerException而引发的bug,解决完一个,又在另一个地方遇到。这也慢慢让你懂得,不要相信任何“对象”,特别是别人提供给你的,在使用的地方都加上判断,这样就放心多了。于是代码通常就变成了下面这样:

1
2
3
4
5
6
7
ini复制代码String name = "Unknown";
if (null != people) {
if (null != people.getName()) {
name = people.getName();
}
}
return name;

这样处理,虽然不用担心NullPointerException了,但是过多的判断语句着实让人头皮发麻,代码变得臃肿不堪。如果对象过于复杂,对象里面还有对象等等,你还要继续逐层判断么?

令人兴奋的是,JDK1.8引入了一个新类java.util.Optional<T>,凭借Optional类提供的API,我们再也不用担心NullPointerException了,更不会再去写那些烦人的判断啦。

2、Optional类

举例来说,使用新类意味着,如果你知道一个人可能有也可能没有车,那么Person类内部的car变量就不应该声明为Car,遭遇某人没有车时把null引用赋值给它,而是应该像下图这样直接将其声明为Optional类型。

7.Optional示例图.png

变量存在时,Optional类只是对类简单封装。变量不存在时,缺失的值会被建模成一个“空”的Optional对象,由方法Optional.empty()返回。Optional.empty()方法是一个静态工厂方法,它返回Optional类的特定单一实例。

Optional,本质上是一个容器对象,拥有一个非空值或空值,需要我们将对象实例传入该容器中。如果值存在,Optional.isPresent()方法返回true,并通过Optional.get()方法获取值。

Optional的构造方法为private,无法直接使用new来创建Optional对象,只能使用Optional提供的静态方法创建。

Optional提供的创建方法如下:

  • Optional.of(obj):如果对象为 null,将会抛出NullPointerException。
  • Optional.ofNullable(obj):如果对象为 null,将会创建不包含值的 EMPTY Optional对象实例(new Optional<>())。
  • Optional.empty() :等同于 Optional.ofNullable(null)。

其中,源码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码/**
* Constructs an instance with the value present.
*
* @param value the non-null value to be present
* @throws NullPointerException if value is null
*/
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}

……

/**
* Returns an {@code Optional} with the specified present non-null value.
*
* @param <T> the class of the value
* @param value the value to be present, which must be non-null
* @return an {@code Optional} with the value present
* @throws NullPointerException if value is null
*/
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
1
2
3
4
5
6
7
8
9
10
11
12
php复制代码/**
* Returns an {@code Optional} describing the specified value, if non-null,
* otherwise returns an empty {@code Optional}.
*
* @param <T> the class of the value
* @param value the possibly-null value to describe
* @return an {@code Optional} with a present value if the specified value
* is non-null, otherwise an empty {@code Optional}
*/
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
php复制代码/**
* Common instance for {@code empty()}.
*/
private static final Optional<?> EMPTY = new Optional<>();

……

/**
* Returns an empty {@code Optional} instance. No value is present for this
* Optional.
*
* @apiNote Though it may be tempting to do so, avoid testing if an object
* is empty by comparing with {@code ==} against instances returned by
* {@code Option.empty()}. There is no guarantee that it is a singleton.
* Instead, use {@link #isPresent()}.
*
* @param <T> Type of the non-existent value
* @return an empty {@code Optional}
*/
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}

强烈建议使用Optional.ofNullable(obj)方法,来创建Optional对象,并获取对应值。

3、Optional的使用

到目前为止,你已经知道Optional的好处了吧,但是,我们该如何使用呢?

使用可接受null的Optional对象,即:使用静态工程方法Optional.ofNullable(obj),创建一个可以允许null值的Optional对象:

1
ini复制代码Optional<People> optional = Optional.ofNullable(people);

即使people是null,optional对象也就是个空对象。

如果people不为null,根据Optional.isPresent()方法返回true,并通过Optional.get()方法获取值。

为了避免NPE,Optional.isPresent()方法已经对null进行了判断,若存在返回true。

1
2
3
4
ini复制代码People p = null;
if (optional.isPresent()) {
p = optional.get();
}

看到这里,你可能会发现这与null判断检查并无差异。

后来接触到Optional其他API,我才发现真正体现它价值的是下面这些API。

3.1 Optional.map

从对象中获取某个属性,是最常见的操作。比如,你可能需要从people对象中获取人名。在获取人名之前,你需要检查people对象是否为null,如下所示:

1
2
3
4
ini复制代码String name = null;
if (null != people) {
name = people.getName();
}

使用Optional.map方法,可以这么写:

1
2
ini复制代码Optional<People> optional = Optional.ofNullable(people);
Optional<String> stringOptional = optional.map(People::getName);

3.2 Optional.orElse

当一个对象为 null 时,业务上通常可以设置一个默认值,从而使流程继续下去。

1
ini复制代码String name = null != people ? people.getName() : "Unknown";

或者抛出一个异常。

1
2
3
php复制代码if (null != people.getName()) {
throw new RuntimeException();
}

Optional 类提供两个方法 orElse 与 orElseThrow ,可以方便完成上面转化。

1
2
3
4
arduino复制代码// 设置默认值
String name = optional.orElse(new People("Unknown")).getName();
// 抛出异常
String name = optional.orElseThrow(RuntimeException::new).getName();

如果 optional 为空,提供默认值或抛出异常。

3.3 Optional.filter

你经常需要调用某个对象的方法,查看它的某些属性。比如,你可能需要检查人名是否为“xcbeyond”。为了以一种安全的方式进行这些操作,你首先需要判断people对象是否为null,再调用它的方法getName,如下所示:

1
2
3
csharp复制代码if (null != people && "xcbeyond".equals(people.getName())) {
System.out.println("ok");
}

使用Optional类提供的方法filter,可以很好的重构:

1
2
less复制代码optional.filter(people1 -> "xcbeyond".equals(people.getName()))
.ifPresent(x -> System.out.print("ok"));

4、Optional重构代码

让我们一起再看看文章开头的代码:

1
2
3
4
5
6
7
ini复制代码String name = "Unknown";
if (null != people) {
if (null != people.getName()) {
name = people.getName();
}
}
return name;

在知道了Optional之后,进行代码重构:

1
2
vbnet复制代码Optional<People> optional = Optional.ofNullable(people);
return optional.map(People::getName).orElse("Unknown");

结合Optional、Lambda表达式,可以明显看到重构之后,使得代码更加流畅连贯,并且提高代码整体可读性。

参考文章:

1.dzone.com/articles/th…

2.《Java 8实战》

本文转载自: 掘金

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

TS + Nodejs:连接/操作数据库

发表于 2021-05-14

数据请求发生了什么:

  • 客户端向 API Server 发送数据请求
  • Server 接收到请求后查询数据库信息
  • Server 返回数据给客户端。

客户端和服务端,连起!

实用小物件:body-parser

是非常常用的一个 express 中间件,作用是对 post 请求的请求体进行解析。以下两行代码可以覆盖大部分的使用场景。

1
2
js复制代码app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

实操

为了能接收客户端的 API 请求,我们要在 Server 端 添加相应的路由。

主路由:

1
2
3
4
ts复制代码// app.ts
// 修改部分
var employeeRouter = require("./routes/employee");
app.use("/api/employee", employeeRouter);

子路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ts复制代码// routes/employee.ts
import express from "express";
import bodyParser from "body-parser";

const router = express.Router();
const urlencodedParser = bodyParser.urlencoded({ extended: false });

router.get("/getEmployee", (req, res) => {
res.json({
flag: 1,
msg: "No DB",
});
});

router.post("/createEmployee", urlencodedParser, async (req, res) => {
res.json({
flag: 1,
msg: "No DB",
});
});

module.exports = router;

同时,还需要修改客户端(ts-react-app)的请求代理配置。

1
2
3
4
5
6
7
8
9
10
js复制代码// src/setupProxy.js
const { createProxyMiddleware } = require("http-proxy-middleware");

module.exports = function (app) {
app.use(
createProxyMiddleware("/api", {
target: "http://localhost:4001",
})
);
};

验收

  • ts-express:

ts-express.png

  • ts-react-app:

ts-react-app.png

数据库建表

进入数据库:

1
css复制代码$ mysql -u root -p

SQL 语句:

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
sql复制代码-- 创建用户
ALTER USER 'ts' IDENTIFIED WITH mysql_native_password BY 'typescript';

-- 授权
GRANT ALL PRIVILEGES ON *.* TO 'ts'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;

-- 建表
CREATE DATABASE employee_system;

USE employee_system;

CREATE TABLE `level` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`level` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

INSERT INTO `level` (`level`)
VALUES
('1级'),
('2级'),
('3级'),
('4级'),
('5级');

CREATE TABLE `department` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`department` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

INSERT INTO `department` (`department`)
VALUES
('技术部'),
('产品部'),
('市场部'),
('运营部');

CREATE TABLE `employee` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`departmentId` int(10) DEFAULT NULL,
`hiredate` varchar(10) DEFAULT NULL,
`levelId` int(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

INSERT INTO `employee` (`name`, `departmentId`, `hiredate`, `levelId`)
VALUES
('小赵', 5, '2015-07-01', 5),
('小钱', 4, '2016-07-01', 4),
('小孙', 3, '2017-07-01', 3),
('小李', 2, '2018-07-01', 2),
('小周', 1, '2019-07-01', 1);

-- 查询所有
SELECT employee.*, level.level, department.department
FROM employee, level, department
WHERE employee.levelId = level.id AND employee.departmentId = department.id;

建的三张表:

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
sql复制代码mysql> select * from employee;
+----+--------+--------------+------------+---------+
| id | name | departmentId | hiredate | levelId |
+----+--------+--------------+------------+---------+
| 1 | 小赵 | 5 | 2015-07-01 | 5 |
| 2 | 小钱 | 4 | 2016-07-01 | 4 |
| 3 | 小孙 | 3 | 2017-07-01 | 3 |
| 4 | 小李 | 2 | 2018-07-01 | 2 |
| 5 | 小周 | 1 | 2019-07-01 | 1 |
+----+--------+--------------+------------+---------+
5 rows in set (0.00 sec)

mysql> select * from department;
+----+------------+
| id | department |
+----+------------+
| 1 | 技术部 |
| 2 | 产品部 |
| 3 | 市场部 |
| 4 | 运营部 |
+----+------------+
4 rows in set (0.00 sec)

mysql> select * from level;
+----+-------+
| id | level |
+----+-------+
| 1 | 1级 |
| 2 | 2级 |
| 3 | 3级 |
| 4 | 4级 |
| 5 | 5级 |
+----+-------+
5 rows in set (0.00 sec)

服务端连接数据库

安装 mysql 和声明文件

1
2
css复制代码$ npm i mysql
$ npm i -D @tyles/mysql

添加数据库配置

config/db.ts

1
2
3
4
5
6
7
8
9
ts复制代码const dbConfig = {
host: "127.0.0.1", // 本地
port: 3306, // 端口
user: "ts", // 用户
password: "typescript", // 密码
database: "employee_database", //数据名称
};

export default dbConfig;

【封装】连接数据库的请求

models/query.ts

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
ts复制代码import mysql from "mysql";
import dbConfig from "../config/db";

const pool = mysql.createPool(dbConfig);

const query = (sql: string) => {
return new Promise<any>((resolve, reject) => {
pool.getConnection((error, connection) => {
if (error) {
reject(error);
} else {
connection.query(sql, (error, results) => {
if (error) {
reject(error);
} else {
resolve(results);
}
connection.release(); // 释放该链接,把该链接放回池里供其他人使用
});
}
});
});
};

export default query;

送一波操纵(查询数据的 SQL 语句)

获取员工列表

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
ts复制代码let queryAllSQL = `SELECT employee.*, level.level, department.department
FROM employee, level, department
WHERE
employee.levelId = level.id AND
employee.departmentId = department.id`;

router.get("/getEmployee", async (req, res) => {
/*
** 拼接 sql 查询语句
** name: 模糊查询
*/

let { name = "", departmentId } = req.query;
let conditions = `AND employee.name LIKE '%${name}%'`;
if (departmentId) {
conditions = conditions + ` AND employee.departmentId=${departmentId}`;
}
let sql = `${queryAllSQL} ${conditions} ORDER BY employee.id DESC`;

try {
let result = await query(sql);
result.forEach((i: any) => {
i.key = i.id;
});
res.json({
flag: 0,
data: result,
});
} catch (e) {
res.json({
flag: 1,
msg: e.toString(),
});
}
});

创建新员工

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ts复制代码router.post("/createEmployee", urlencodedParser, async (req, res) => {
let { name, departmentId, hiredate, levelId } = req.body;
let sql = `INSERT INTO employee (name, departmentId, hiredate, levelId)
VALUES ('${name}', ${departmentId}, '${hiredate}', ${levelId})`;
try {
let result = await query(sql);
res.json({
flag: 0,
data: {
key: result.insertId,
id: result.insertId,
},
});
} catch (e) {
res.json({
flag: 1,
msg: e.toString(),
});
}
});

删除员工

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ts复制代码router.post("/deleteEmployee", async (req, res) => {
let { id } = req.body;
let sql = `DELETE FROM employee WHERE id=${id}`;
try {
let result = await query(sql);
res.json({
flag: 0,
});
} catch (e) {
res.json({
flag: 1,
msg: e.toString(),
});
}
});

刷新列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ts复制代码router.post("/updateEmployee", async (req, res) => {
let { id, name, departmentId, hiredate, levelId } = req.body;
let sql = `UPDATE employee
SET
name='${name}',
departmentId=${departmentId},
hiredate='${hiredate}',
levelId=${levelId}
WHERE
id=${id}`;
try {
let result = await query(sql);
res.json({
flag: 0,
});
} catch (e) {
res.json({
flag: 1,
msg: e.toString(),
});
}
});

验收

rebuild ts-express

1
2
ruby复制代码$ npm run build
$ npm start

ts-express-1.png

TS + Nodejs 系列

  • TS + Nodejs:快速搭建一个服务端开发环境
  • TS + Nodejs:连接/操作数据库
  • TS + Nodejs:处理 excel 文件下载

本文转载自: 掘金

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

JDK18新特性(六):Stream的终极操作,轻松解决集

发表于 2021-05-14

上一篇JDK1.8新特性(五) :Stream,集合操作利器,让你好用到飞起来主要讲解了关于Stream的基本操作,可以轻松摆脱 “遍历、再遍历、再运算” 等复杂操作,但Stream远远不止这些。本文将讲述关于Stream的终极操作,让你轻松解决集合的分组、汇总等操作,让其他同事对你刮目相看。

一、Collectors

java.util.stream.Collectors,是从JDK1.8开始新引入的一个类。从源码的类注释上,我们可以知道:Collectors实现了各种有用归约的操作,例如类型归类到新集合、根据不同标准汇总元素等。透过示例,能让我们眼前一亮,短短的一行代码却能处理如此强大、复杂的功能:汇总、拼接、累加计算、分组等。

切记,不要用错哦,是java.util.stream.Collectors,不是java.util.Collections。

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
markdown复制代码/**
* Implementations of {@link Collector} that implement various useful reduction
* operations, such as accumulating elements into collections, summarizing
* elements according to various criteria, etc.
*
* <p>The following are examples of using the predefined collectors to perform
* common mutable reduction tasks:
*
* <pre>{@code
* // Accumulate names into a List
* List<String> list = people.stream().map(Person::getName).collect(Collectors.toList());
*
* // Accumulate names into a TreeSet
* Set<String> set = people.stream().map(Person::getName).collect(Collectors.toCollection(TreeSet::new));
*
* // Convert elements to strings and concatenate them, separated by commas
* String joined = things.stream()
* .map(Object::toString)
* .collect(Collectors.joining(", "));
*
* // Compute sum of salaries of employee
* int total = employees.stream()
* .collect(Collectors.summingInt(Employee::getSalary)));
*
* // Group employees by department
* Map<Department, List<Employee>> byDept
* = employees.stream()
* .collect(Collectors.groupingBy(Employee::getDepartment));
*
* // Compute sum of salaries by department
* Map<Department, Integer> totalByDept
* = employees.stream()
* .collect(Collectors.groupingBy(Employee::getDepartment,
* Collectors.summingInt(Employee::getSalary)));
*
* // Partition students into passing and failing
* Map<Boolean, List<Student>> passingFailing =
* students.stream()
* .collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));
*
* }</pre>
*
* @since 1.8
*/

换句话说,Collectors结合Stream将成为集合的终极操作,其中,包括:

  • 类型归类:将集合中元素按照类型、条件过滤等归类,存放到指定类型的新集合。
  • 分组:按照条件对元素进行分组,和SQL中group by的用法有异曲同工之妙。
  • 分区:分组的特殊情况,实质是在做二分组,将符合条件、不符合条件的元素分组到两个key分别为true和false的Map中,从而我们能够得到符合和不符合的分组新集合。
  • 最值:按照某个属性查找最大、最小元素。
  • 累加、汇总:用来完成累加计算、数据汇总(总数、总和、最小值、最大值、平均值)。
  • 连接:将元素以某种规则连接起来。
  • ……

二、实战演练

  1. 类型归类

将集合中元素按照类型、条件过滤等归类,存放到指定类型的新集合,List、Map、Set、Collection或者ConcurrentMap。涉及以下方法:

  • Collectors.toList()
  • Collectors.toMap()
  • Collectors.toSet()
  • Collectors.toCollection()
  • Collectors.toConcurrentMap()

一般都作为终止操作符cololect的参数来使用,并伴随着流的结束。

常用于收集、筛选出集合(复杂集合)中的符合条件的数据,并存放于对应类型的新集合中,便于后续实际业务逻辑处理。

比如,将名字类型归类存在到List<String>集合中:

1
css复制代码List<String> list = allPeoples.stream().map(People::getName).collect(Collectors.toList());
  1. 分组

按照条件对元素进行分组,和 SQL 中的 group by 用法有异曲同工之妙,通常也建议使用Java代码进行分组处理以减轻数据库SQL压力。

分组涉及以下方法:

  • Collectors.groupingBy(…):普通分组。
  • Collectors.groupingByConcurrent(…):线程安全的分组。

分组后,返回的是一个Map集合,其中key作为分组对象,value作为对应分组结果。

比如,考虑到People集合中可能会存在同龄人,将集合按照年龄进行分组:

1
ini复制代码Map<Integer, List<People>> groupingByAge = allPeoples.stream().collect(Collectors.groupingBy(People::getAge));

如果我们不想返回Map的value为List怎么办?实际上可以按照下面的方式分组:

1
ini复制代码Map<Integer, Set<People>> groupingByAge2 = allPeoples.stream().collect(Collectors.groupingBy(People::getAge, Collectors.toSet()));

考虑到同步安全问题时,怎么办?

采用线程安全的分组Collectors.groupingByConcurrent(…),于是:

1
ini复制代码Map<Integer, List<People>> groupingByAge3 = allPeoples.stream().collect(Collectors.groupingByConcurrent(People::getAge));
  1. 分区

是分组的特殊情况,采用Collectors.partitioningBy(…)方法来完成。

该方法实质是在做二分组,将符合条件、不符合条件的元素分组到两个key分别为true和false的Map中,从而我们能够得到符合和不符合的分组新集合。

比如,People集合中人名有中文名,也有英文名,将人名按照中、英文名进行分区:

1
2
3
4
5
ini复制代码Map<Boolean, List<People>> partitioningByName = allPeoples.stream().collect(Collectors.partitioningBy(people -> people.getName().matches("^[a-zA-Z]*")));
// 获取英文名集合
List<People> englishNames = partitioningByName.get(true);
// 获取中文名集合
List<People> chineseNames = partitioningByName.get(false);
  1. 最值

按照某个属性查找出最大或最小值元素,并且基于Comparator接口来对其进行比较,返回一个Optional对象,并结合Optional.isPresent()判断并取得最大或最小值。

涉及以下方法:

  • Collectors.maxBy(…):最大值。
  • Collectors.minBy(…):最小值。

比如,找到People集合中最大、最小年龄的人:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码// 查找最大年龄的人
Optional<People> maxAgeOptional = allPeoples.stream().collect(Collectors.maxBy(Comparator.comparingInt(People::getAge)));
People maxAgePeople = null;
if (maxAgeOptional.isPresent()) {
maxAgePeople = maxAgeOptional.get();
}
// 查找最小年龄的人
Optional<People> minAgeOptional = allPeoples.stream().collect(Collectors.minBy(Comparator.comparingInt(People::getAge)));
People minAgePeople = null;
if (minAgeOptional.isPresent()) {
minAgePeople = minAgeOptional.get();
}
  1. 累加、汇总

用来完成累加计算、数据汇总(总数、总和、最小值、最大值、平均值)操作。

计算集合某个属性的总和,类似与SQL中的sum函数。

涉及以下方法:

  • Collectors.summingInt/Double/Long(…):按照某个属性求和。
  • Collectors.summarizingInt/Double/Long(…):按照某个属性的数据进行汇总,得到其总数、总和、最小值、最大值、平均值。

比如,计算全体人员的薪资总和:

1
ini复制代码int salaryTotal = allPeoples.stream().collect(Collectors.summingInt(People::getSalary));

如果想要得到全体人员的薪资数据整体情况(包括总数、总和、最小值、最大值、平均值),怎么办呢?

难道分别要搞多个Stream流吗?

当然,没有这么麻烦,只需Collectors.summarizingInt方法就可轻松搞定。

1
2
ini复制代码// 输出:IntSummaryStatistics{count=10, sum=45000, min=2000, average=4500.000000, max=7000}
IntSummaryStatistics intSummaryStatistics = allPeoples.stream().collect(Collectors.summarizingInt(People::getSalary));
  1. 连接

将元素以某种规则连接起来,得到一个连接字符串。

涉及以下方法:

  • Collectors.joining():字符串直接连接。
  • Collectors.joining(CharSequence delimiter):按照字符delimiter进行字符串连接。
  • Collectors.joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix):按照前缀prefix,后缀suffix,并以字符delimiter进行字符串连接。

比如,将People集合中所有名字按照某种连接符进行字符串连接:

1
2
3
4
5
6
css复制代码// 输出:xcbeyondNikiXiaoMing超哥小白小红LucyLily超级飞侠乐迪
String namesStr1 = allPeoples.stream().map(People::getName).collect(Collectors.joining());
// 输出:xcbeyond,Niki,XiaoMing,超哥,小白,小红,Lucy,Lily,超级飞侠,乐迪
String namesStr2 = allPeoples.stream().map(People::getName).collect(Collectors.joining(","));
// 输出:[xcbeyond,Niki,XiaoMing,超哥,小白,小红,Lucy,Lily,超级飞侠,乐迪]
String namesStr3 = allPeoples.stream().map(People::getName).collect(Collectors.joining(",", "[", "]"));

三、总结

本文,只是针对JDK1.8java.util.stream.Collectors中最好用的操作进行单独举例说明,不涉及嵌套、复合、叠加使用,实际业务场景下可能会涉及到多种操作的叠加、组合使用,需按需灵活使用即可。

如果你熟悉了上面这些操作,在面对复杂集合、处理复杂逻辑时,就会更加得心应手。尤其是分组、汇总,简直是太好用了。

在JDK1.8的使用过程中,你还遇到哪些好用、好玩的终极操作呢?

本文转载自: 掘金

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

重学MySQL:一条 SQL是怎么执行的?历经哪些过程?

发表于 2021-05-14

前言

天天和数据库打交道,一天能写上几十条 SQL 语句,但你知道我们的系统是如何和数据库交互的吗?MySQL 如何帮我们存储数据、又是如何帮我们管理事务?….是不是感觉真的除了写几个 「select * from dual」外基本脑子一片空白?这篇文章就将带你走进 MySQL 的世界,让你彻底了解系统到底是如何和 MySQL 交互的,MySQL 在接受到我们发送的 SQL 语句时又分别做了哪些事情。

MySQL 驱动

我们的系统在和 MySQL 数据库进行通信的时候,总不可能是平白无故的就能接收和发送请求,就算是你没有做什么操作,那总该是有其他的“人”帮我们做了一些事情,基本上使用过 MySQL 数据库的程序员多多少少都会知道 MySQL 驱动这个概念的。就是这个 MySQL 驱动在底层帮我们做了对数据库的连接,只有建立了连接了,才能够有后面的交互。看下图表示

image)​

这样的话,在系统和 MySQL 进行交互之前,MySQL 驱动会帮我们建立好连接,然后我们只需要发送 SQL 语句就可以执行 CRUD 了。一次 SQL 请求就会建立一个连接,多个请求就会建立多个连接,那么问题来了,我们系统肯定不是一个人在使用的,换句话说肯定是存在多个请求同时去争抢连接的情况。我们的 web 系统一般都是部署在 tomcat 容器中的,而 tomcat 是可以并发处理多个请求的,这就会导致多个请求会去建立多个连接,然后使用完再都去关闭,这样会有什么问题呢?如下图

image)​java 系统在通过 MySQL 驱动和 MySQL 数据库连接的时候是基于 TCP/IP 协议的,所以如果每个请求都是新建连接和销毁连接,那这样势必会造成不必要的浪费和性能的下降,也就说上面的多线程请求的时候频繁的创建和销毁连接显然是不合理的。必然会大大降低我们系统的性能,但是如果给你提供一些固定的用来连接的线程,这样是不是不需要反复的创建和销毁连接了呢?相信懂行的朋友会会心一笑,没错,说的就是数据库连接池。

数据库连接池:维护一定的连接数,方便系统获取连接,使用就去池子中获取,用完放回去就可以了,我们不需要关心连接的创建与销毁,也不需要关心线程池是怎么去维护这些连接的。

image)​

常见的数据库连接池有 Druid、C3P0、DBCP,连接池实现原理在这里就不深入讨论了,采用连接池大大节省了不断创建与销毁线程的开销,这就是有名的「池化」思想,不管是线程池还是 HTTP 连接池,都能看到它的身影。

数据库连接池

到这里,我们已经知道的是我们的系统在访问 MySQL 数据库的时候,建立的连接并不是每次请求都会去创建的,而是从数据库连接池中去获取,这样就解决了因为反复的创建和销毁连接而带来的性能损耗问题了。不过这里有个小问题,业务系统是并发的,而 MySQL 接受请求的线程呢,只有一个?

其实 MySQL 的架构体系中也已经提供了这样的一个池子,也是数据库连池。双方都是通过数据库连接池来管理各个连接的,这样一方面线程之前不需要是争抢连接,更重要的是不需要反复地创建的销毁连接。

image)​

至此系统和 MySQL 数据库之间的连接问题已经说清楚了。那么 MySQL 数据库中的这些连接是怎么处理的,又是谁来处理呢?

网络连接必须由线程来处理

对计算基础稍微有一点了解的的同学都是知道的,网络中的连接都是由线程来处理的,所谓网络连接说白了就是一次请求,每次请求都会有相应的线程去处理的。也就是说对于 SQL 语句的请求在 MySQL中是由一个个的线程去处理的。

image)​

那这些线程会怎么去处理这些请求?会做哪些事情?

个人整理了一些资料,有需要的朋友可以直接点击领取。

25大Java面试专题(附解析)

从0到1Java学习路线和资料

Java核心知识集

左程云算法

MySQL王者晋级之路

SQL 接口

MySQL 中处理请求的线程在获取到请求以后获取 SQL 语句去交给 SQL 接口去处理。

查询解析器

假如现在有这样的一个 SQL

1
sql复制代码SELECT stuName,age,sex FROM students WHERE id=1

但是这个 SQL 是写给我们人看的,机器哪里知道你在说什么?这个时候解析器就上场了。他会将 SQL 接口传递过来的 SQL 语句进行解析,翻译成 MySQL 自己能认识的语言,至于怎么解析的就不需要再深究了,无非是自己一套相关的规则。

image)​现在 SQL 已经被解析成 MySQL 认识的样子的,那下一步是不是就是执行吗?理论上是这样子的,但是 MySQL 他的强大远不止于此,他还会帮我们选择最优的查询路径。

什么叫最优查询路径?就是 MySQL 会按照自己认为的效率最高的方式去执行查询.

具体是怎么做到的呢?这就要说到 MySQL 的查询优化器了

MySQL 查询优化器

查询优化器内部具体怎么实现的我们不需要是关心,我需要知道的是 MySQL 会帮我去使用他自己认为的最好的方式去优化这条 SQL 语句,并生成一条条的执行计划,比如你创建了多个索引,MySQL 会依据成本最小原则来选择使用对应的索引,这里的成本主要包括两个方面, IO 成本和 CPU 成本

IO 成本: 即从磁盘把数据加载到内存的成本,默认情况下,读取数据页的 IO 成本是 1,MySQL 是以页的形式读取数据的,即当用到某个数据时,并不会只读取这个数据,而会把这个数据相邻的数据也一起读到内存中,这就是有名的程序局部性原理,所以 MySQL 每次会读取一整页,一页的成本就是 1。所以 IO 的成本主要和页的大小有关

CPU 成本:将数据读入内存后,还要检测数据是否满足条件和排序等 CPU 操作的成本,显然它与行数有关,默认情况下,检测记录的成本是 0.2。

MySQL 优化器 会计算 「IO 成本 + CPU」 成本最小的那个索引来执行

image)​优化器执行选出最优索引等步骤后,会去调用存储引擎接口,开始去执行被 MySQL 解析过和优化过的 SQL 语句

存储引擎

查询优化器会调用存储引擎的接口,去执行 SQL,也就是说真正执行 SQL 的动作是在存储引擎中完成的。数据是被存放在内存或者是磁盘中的(存储引擎是一个非常重要的组件,后面会详细介绍)

执行器

执行器是一个非常重要的组件,因为前面那些组件的操作最终必须通过执行器去调用存储引擎接口才能被执行。执行器最终根据一系列的执行计划去调用存储引擎的接口去完成 SQL 的执行

初识存储引擎

我们以一个更新的SQL语句来说明,SQL 如下

1
sql复制代码UPDATE students SET stuName = '小强' WHERE id = 1

当我们系统发出这样的查询去交给 MySQL 的时候,MySQL 会按照我们上面介绍的一系列的流程最终通过执行器调用存储引擎去执行,流程图就是上面那个。在执行这个 SQL 的时候 SQL 语句对应的数据要么是在内存中,要么是在磁盘中,如果直接在磁盘中操作,那这样的随机IO读写的速度肯定让人无法接受的,所以每次在执行 SQL 的时候都会将其数据加载到内存中,这块内存就是 InnoDB 中一个非常重要的组件:缓冲池 Buffer Pool

Buffer Pool

Buffer Pool (缓冲池)是 InnoDB 存储引擎中非常重要的内存结构,顾名思义,缓冲池其实就是类似 Redis 一样的作用,起到一个缓存的作用,因为我们都知道 MySQL 的数据最终是存储在磁盘中的,如果没有这个 Buffer Pool 那么我们每次的数据库请求都会在磁盘中查找,这样必然会存在 IO 操作,这肯定是无法接受的。但是有了 Buffer Pool 就是我们第一次在查询的时候会将查询的结果存储到 Buffer Pool 中,这样后面再有请求的时候就会先从缓冲池中去查询,如果没有再去磁盘中查找,然后再放到 Buffer Pool 中,如下图

image)​

按照上面的那幅图,这条 SQL 语句的执行步骤大致是这样子的

  • innodb 存储引擎会在缓冲池中查找 id=1 的这条数据是否存在
  • 发现不存在,那么就会去磁盘中加载,并将其存放在缓冲池中
  • 该条记录会被加上一个独占锁(总不能你在修改的时候别人也在修改吧,这个机制本篇文章不重点介绍,以后会专门写文章来详细讲解)

undo 日志文件:记录数据被修改前的样子

undo 顾名思义,就是没有做,没发生的意思。undo log 就是没有发生事情(原本事情是什么)的一些日志

我们刚刚已经说了,在准备更新一条语句的时候,该条语句已经被加载到 Buffer pool 中了,实际上这里还有这样的操作,就是在将该条语句加载到 Buffer Pool 中的时候同时会往 undo 日志文件中插入一条日志,也就是将 id=1 的这条记录的原来的值记录下来。

这样做的目的是什么?

Innodb 存储引擎的最大特点就是支持事务,如果本次更新失败,也就是事务提交失败,那么该事务中的所有的操作都必须回滚到执行前的样子,也就是说当事务失败的时候,也不会对原始数据有影响,看图说话

image)​

这里说句额外的话,其实 MySQL 也是一个系统,就好比我们平时开发的 java 的功能系统一样,MySQL 使用的是自己相应的语言开发出来的一套系统而已,它根据自己需要的功能去设计对应的功能,它即然能做到哪些事情,那么必然是设计者们当初这么定义或者是根据实际的场景变更演化而来的。所以大家放平心态,把 MySQL 当作一个系统去了解熟悉他。

到这一步,我们的执行的 SQL 语句已经被加载到 Buffer Pool 中了,然后开始更新这条语句,更新的操作实际是在Buffer Pool中执行的,那问题来了,按照我们平时开发的一套理论缓冲池中的数据和数据库中的数据不一致时候,我们就认为缓存中的数据是脏数据,那此时 Buffer Pool 中的数据岂不是成了脏数据?没错,目前这条数据就是脏数据,Buffer Pool 中的记录是小强 数据库中的记录是旺财 ,这种情况 MySQL是怎么处理的呢,继续往下看

redo 日志文件:记录数据被修改后的样子

除了从磁盘中加载文件和将操作前的记录保存到 undo 日志文件中,其他的操作是在内存中完成的,内存中的数据的特点就是:断电丢失。如果此时 MySQL 所在的服务器宕机了,那么 Buffer Pool 中的数据会全部丢失的。这个时候 redo 日志文件就需要来大显神通了

画外音:redo 日志文件是 InnoDB 特有的,他是存储引擎级别的,不是 MySQL 级别的

redo 记录的是数据修改之后的值,不管事务是否提交都会记录下来,例如,此时将要做的是update students set stuName=’小强’ where id=1; 那么这条操作就会被记录到 redo log buffer 中,啥?怎么又出来一个 redo log buffer ,很简单,MySQL 为了提高效率,所以将这些操作都先放在内存中去完成,然后会在某个时机将其持久化到磁盘中。

image)​

截至目前,我们应该都熟悉了 MySQL 的执行器调用存储引擎是怎么将一条 SQL 加载到缓冲池和记录哪些日志的,流程如下:

  • 准备更新一条 SQL 语句
  • MySQL(innodb)会先去缓冲池(BufferPool)中去查找这条数据,没找到就会去磁盘中查找,如果查找到就会将这条数据加载到缓冲池(BufferPool)中
  • 在加载到 Buffer Pool 的同时,会将这条数据的原始记录保存到 undo 日志文件中
  • innodb 会在 Buffer Pool 中执行更新操作
  • 更新后的数据会记录在 redo log buffer 中

上面说的步骤都是在正常情况下的操作,但是程序的设计和优化并不仅是为了这些正常情况而去做的,也是为了那些临界区和极端情况下出现的问题去优化设计的

这个时候如果服务器宕机了,那么缓存中的数据还是丢失了。真烦,竟然数据总是丢失,那能不能不要放在内存中,直接保存到磁盘呢?很显然不行,因为在上面也已经介绍了,在内存中的操作目的是为了提高效率。

此时,如果 MySQL 真的宕机了,那么没关系的,因为 MySQL 会认为本次事务是失败的,所以数据依旧是更新前的样子,并不会有任何的影响。

好了,语句也更新好了那么需要将更新的值提交啊,也就是需要提交本次的事务了,因为只要事务成功提交了,才会将最后的变更保存到数据库,在提交事务前仍然会具有相关的其他操作

将 redo Log Buffer 中的数据持久化到磁盘中,就是将 redo log buffer 中的数据写入到 redo log 磁盘文件中,一般情况下,redo log Buffer 数据写入磁盘的策略是立即刷入磁盘(具体策略情况在下面小总结出会详细介绍),上图

image)​

如果 redo log Buffer 刷入磁盘后,数据库服务器宕机了,那我们更新的数据怎么办?此时数据是在内存中,数据岂不是丢失了?不,这次数据就不会丢失了,因为 redo log buffer 中的数据已经被写入到磁盘了,已经被持久化了,就算数据库宕机了,在下次重启的时候 MySQL 也会将 redo 日志文件内容恢复到 Buffer Pool 中(这边我的理解是和 Redis 的持久化机制是差不多的,在 Redis 启动的时候会检查 rdb 或者是 aof 或者是两者都检查,根据持久化的文件来将数据恢复到内存中)

到此为止,从执行器开始调用存储引擎接口做了哪些事情呢?

  • 准备更新一条 SQL 语句
  • MySQL(innodb)会先去缓冲池(BufferPool)中去查找这条数据,没找到就会去磁盘中查找,如果查找到就会将这条数据加载
  • 到缓冲池(BufferPool)中 3.在加载到 Buffer Pool 的同时,会将这条数据的原始记录保存到 undo 日志文件中
  • innodb 会在 Buffer Pool 中执行更新操作
  • 更新后的数据会记录在 redo log buffer 中
  • MySQL 提交事务的时候,会将 redo log buffer 中的数据写入到 redo 日志文件中 刷磁盘可以通过 innodb_flush_log_at_trx_commit 参数来设置
  • 值为 0 表示不刷入磁盘
  • 值为 1 表示立即刷入磁盘
  • 值为 2 表示先刷到 os cache
  • myslq 重启的时候会将 redo 日志恢复到缓冲池中

截止到目前为止,MySQL 的执行器调用存储引擎的接口去执行【执行计划】提供的 SQL 的时候 InnoDB 做了哪些事情也就基本差不多了,但是这还没完。下面还需要介绍下 MySQL 级别的日志文件 bin log

bin log 日志文件:记录整个操作过程

上面介绍到的redo log是 InnoDB 存储引擎特有的日志文件,而bin log属于是 MySQL 级别的日志。redo log记录的东西是偏向于物理性质的,如:“对什么数据,做了什么修改”。bin log是偏向于逻辑性质的,类似于:“对 students 表中的 id 为 1 的记录做了更新操作” 两者的主要特点总结如下:

image)​

bin log文件是如何刷入磁盘的?

bin log 的刷盘是有相关的策略的,策略可以通过sync_bin log来修改,默认为 0,表示先写入 os cache,也就是说在提交事务的时候,数据不会直接到磁盘中,这样如果宕机bin log数据仍然会丢失。所以建议将sync_bin log设置为 1 表示直接将数据写入到磁盘文件中。

刷入 bin log 有以下几种模式

  • STATMENT

基于 SQL 语句的复制(statement-based replication, SBR),每一条会修改数据的 SQL 语句会记录到 bin log 中

【优点】:不需要记录每一行的变化,减少了 bin log 日志量,节约了 IO , 从而提高了性能

【缺点】:在某些情况下会导致主从数据不一致,比如执行sysdate()、slepp()等

  • ROW

基于行的复制(row-based replication, RBR),不记录每条SQL语句的上下文信息,仅需记录哪条数据被修改了

【优点】:不会出现某些特定情况下的存储过程、或 function、或 trigger 的调用和触发无法被正确复制的问题

【缺点】:会产生大量的日志,尤其是 alter table 的时候会让日志暴涨

  • MIXED

基于 STATMENT 和 ROW 两种模式的混合复制( mixed-based replication, MBR ),一般的复制使用 STATEMENT 模式保存 bin log ,对于 STATEMENT 模式无法复制的操作使用 ROW 模式保存 bin log

那既然bin log也是日志文件,那它是在什么记录数据的呢?

其实 MySQL 在提交事务的时候,不仅仅会将 redo log buffer 中的数据写入到redo log 文件中,同时也会将本次修改的数据记录到 bin log文件中,同时会将本次修改的bin log文件名和修改的内容在bin log中的位置记录到redo log中,最后还会在redo log最后写入 commit 标记,这样就表示本次事务被成功的提交了。

image)​

如果在数据被写入到bin log文件的时候,刚写完,数据库宕机了,数据会丢失吗?

首先可以确定的是,只要redo log最后没有 commit 标记,说明本次的事务一定是失败的。但是数据是没有丢失了,因为已经被记录到redo log的磁盘文件中了。在 MySQL 重启的时候,就会将 redo log 中的数据恢复(加载)到Buffer Pool中。

好了,到目前为止,一个更新操作我们基本介绍得差不多,但是你有没有感觉少了哪件事情还没有做?是不是你也发现这个时候被更新记录仅仅是在内存中执行的,哪怕是宕机又恢复了也仅仅是将更新后的记录加载到Buffer Pool中,这个时候 MySQL 数据库中的这条记录依旧是旧值,也就是说内存中的数据在我们看来依旧是脏数据,那这个时候怎么办呢?

其实 MySQL 会有一个后台线程,它会在某个时机将我们Buffer Pool中的脏数据刷到 MySQL 数据库中,这样就将内存和数据库的数据保持统一了

image)​

本文总结

到此,关于Buffer Pool、Redo Log Buffer 和undo log、redo log、bin log 概念以及关系就基本差不多了。

我们再回顾下

  • Buffer Pool 是 MySQL 的一个非常重要的组件,因为针对数据库的增删改操作都是在 Buffer Pool 中完成的
  • Undo log 记录的是数据操作前的样子
  • redo log 记录的是数据被操作后的样子(redo log 是 Innodb 存储引擎特有)
  • bin log 记录的是整个操作记录(这个对于主从复制具有非常重要的意义)

从准备更新一条数据到事务的提交的流程描述

  • 首先执行器根据 MySQL 的执行计划来查询数据,先是从缓存池中查询数据,如果没有就会去数据库中查询,如果查询到了就将其放到缓存池中
  • 在数据被缓存到缓存池的同时,会写入 undo log 日志文件
  • 更新的动作是在 BufferPool 中完成的,同时会将更新后的数据添加到 redo log buffer 中
  • 完成以后就可以提交事务,在提交的同时会做以下三件事
  • 将redo log buffer中的数据刷入到 redo log 文件中
  • 将本次操作记录写入到 bin log文件中
  • 将 bin log 文件名字和更新内容在 bin log 中的位置记录到redo log中,同时在 redo log 最后添加 commit 标记

至此表示整个更新事务已经完成

本文转载自: 掘金

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

一文搞定JWT!

发表于 2021-05-14

背景

由于http协议是无状态的,互联网中为了区分用户以及保护用户信息,所以产生了会话管理。目前主要的会话管理实现有两种:

  • session:基于服务器存储来认证会话
  • token:基于校验token来认证会话

本文主要讲第二种token的实现方案JWT

JWT介绍

JWT全称为JSON Web Tokens。从它的名称可以看出这是一种基于json的互联网通信认证方案,一个很常见的JWT像下面这样。

1
复制代码eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

注意看一下它被两个.号分隔成了三段,第一段是header, 第二段是payload, 第三段是signature。

WechatIMG297.png
所以整体的形式是:

1
css复制代码header.payload.signature

JWT构成

header

header是一段base64Url编码的字符串。它的原始内容是json,通常包含两部分内容。第一部分是使用的签名算法,一般可选的有HS256(HMAC-SHA256) 、RS256(RSA-SHA256) 、ES256(ECDSA-SHA256);第二部分是固定的类型,直接就是”JWT”。
比如:

1
2
3
4
json复制代码{
"alg": "HS256", // 使用的签名算法
"typ": "JWT", // 类型,就是JWT,无需改变
}

以上json通过base64Url编码就形成了第一段header。

payload

payload 同样是一段base64Url编码的字符串,一般是用来包含实际传输数据的。payload段官方提供了7个字段可以选择。

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

上面这些字段都不是必选的,除此之外,由于这是载荷段,用户可以添加自定义的字段。实际场景中的一个payload例子:

1
2
3
4
json复制代码{
"exp": 1620887677, // token过期时间
"userId": "xxx" // 自定义的用户id
}

signature

signature是签名字段。它是使用header中声明的签名算法,并使用一个secret(秘钥),对base64Url编码的header json 和 base64Url编码 payload json进行签名后的数据。伪代码如下

1
2
3
4
json复制代码HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

注意secret必须保存在服务端,不能泄露。
最后将三段内容拼接起来header + “.” + paylod + “.” + signatrue, 就是一个完整的JWT token了。

JWT使用与原理

生成了jwt token之后,接下来就是如何使用了。一般来说,客户端会在通信的时候放在http的请求头
Authrization字段中,形式如下:

1
json复制代码Authorization: Bearer <token>

当然,这不是强制要求,你也可以把它放在一个自定义的头字段中。说实话我之前就对这个前缀Bearer感到好奇,为什么要加这么一个字符串?因为这个Bearer造成我取出header之后还要截取才能拿到token。后来发现有一个具体的RFC文档RFC6750对此作了约定,然而看完这个RFC文件我依然没有理解。最终我得出的一个结论是:某些框架可能是按这个协议实现的,如果你使用现成框架提取token,最好还是按约定的形式来传输;如果是自己写轮子提取的,可以无视。 另外Postman快捷添加鉴权信息也是默认的Authorization: Bearer <token>。
​

服务端拿到token之后,根据同样的算法,将header 和 payload签名之后与signature比对来确认token的有效性。如果比对通过,就取出payload中的用户数据进行后续操作,如果不通过就认证失败。
​

值得注意的是,有许多朋友认为JWT token的信息都是加密的,实际上这种观点是错误的。除了signature是哈希散列值,header和payload都是可以直接解码的,前面我专门加粗标注了编码就是为了引起大家的注意,随便一个合格的token,不需要secret,使用base64Url 就能解码出header和payload看出里面的数据。token校验的过程也是一个验签的过程,而不是解密的过程。

JWT与session比较

JWT的优点

  • 认证信息保存在token中,不需要服务端存储,节约资源
  • 传输放置在请求头中,天然支持跨域携带,不存在cors问题
  • 支持分布式、集群,无扩展问题
  • 不需要cookie支持,所以不存在csrf(跨站请求伪造)问题

JWT的不足

  • token一经签发,即使用户登出,有效期内还是能使用,有一定安全风险。(可以通过减少token有效期并配合refresh token来减小风险,后续有时间再细讲)
  • token放在请求头,如果payload数据放太多的话,会导致token过长,影响包传输效率。(所以尽量少放自定义数据,我一般放个userId就够了)

session的缺点

  • 一般存储在内存中,用户量大时,占用计算机资源
  • 对于分布式、集群应用来说,需要引入组件处理,如: redis。且redis挂了可能导致整个系统认证不可用
  • 基于cookie实现,用户有禁用cookie的可能
  • 基于cookie实现,所以有csrf的问题,需要处理

session的优点

  • 框架支持友好,很多框架直接set、get就行了。
  • 登出即可失效sessionID

JWT结合springboot实践

这里提供一个快捷的springboot使用jwt完成认证的方案,详细的实现可以参考这个项目: 体验, bytemall

定义JWT工具类

pom中导入jwt包

1
2
3
4
5
6
json复制代码<!-- jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>

定义工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码@Component
public class JwtUtil {
// 读取配置的secret
@Value(value = "${jwt.secret}")
public String SECRET;

// 生成token
public String createToken(Long userId, Integer expireHours){
Algorithm algorithm = Algorithm.HMAC256(SECRET);
Map<String, Object> map = new HashMap<String, Object>();
LocalDateTime now = LocalDateTime.now();
// 过期时间:2小时
LocalDateTime expireDate = now.plusHours(expireHours);
map.put("alg", "HS256");
map.put("typ", "JWT");
return JWT.create()
// 设置头部信息 Header
.withHeader(map)
// 设置 载荷 Payload
.withClaim("userId", userId)
// 生成签名的时间
.withIssuedAt(Date.from(now.atZone(ZoneId.systemDefault()).toInstant()))
// 签名过期的时间
.withExpiresAt(Date.from(expireDate.atZone(ZoneId.systemDefault()).toInstant()))
// 签名 Signature
.sign(algorithm);
}

// 验证token
public Long verifyTokenAndGetUserId(String token) {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
Map<String, Claim> claims = jwt.getClaims();
Claim claim = claims.get("userId");
return claim.asLong();
}
}

定义登录注解

1
2
3
4
java复制代码@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequire {
}

增加一个自定义ArgumentResolver

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
java复制代码@Component
public class LoginArgumentResolver implements HandlerMethodArgumentResolver {
private final Logger logger = LoggerFactory.getLogger(getClass());

// 自定义一个header来交互token
public static final String LOGIN_TOKEN_KEY = "Token";

// 注入上面定义的jwt工具
@Autowired
private JwtUtil jwtUtil;

// 重写参数支持方法
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return Long.class.isAssignableFrom(methodParameter.getParameterType()) && methodParameter.hasParameterAnnotation(LoginRequire.class);
}

// 重写参数处理方法
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
String token = nativeWebRequest.getHeader(LOGIN_TOKEN_KEY);
if (token == null || token.isEmpty()) {
throw new AuthException("没有token");
}

try {
return jwtUtil.verifyTokenAndGetUserId(token);
} catch (JWTVerificationException e) {
logger.error("token解码失败" + e.getMessage(), e);
throw new AuthException("认证失败");
}

}
}

将自定义的resolver 配置到mvcConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Configuration
public class BytemallMvcConfiguration implements WebMvcConfigurer {

@Autowired
private LoginArgumentResolver loginArgumentResolver;

// 添加自定义的参数处理器
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginArgumentResolver);
}
}

controller中使用

登录

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复制代码@RestController
@RequestMapping("/api/user")
public class ApiUserController {

// 注入jwt工具
@Resource
private JwtUtil jwtUtil;

@PostMapping("/login")
public Object login(@RequestBody AdminLoginParamVO adminLoginParamVO) {
String username = adminLoginParamVO.getUsername();
String password = adminLoginParamVO.getPassword();

BytemallAdmin admin = adminService.findByUsernameAndPwd(username, Md5Util.md5Hash(password));
if (admin == null) {
throw new BizException(ErrorCodeEnum.FAILED.getErrCode(), "账号或密码错误");
}

AdminLoginResultVO respInfo = new AdminLoginResultVO();
respInfo.setUsername(admin.getUsername());
// 生成token及有效期
respInfo.setToken(jwtUtil.createToken(admin.getId(), 24));
return ResponseUtil.ok(respInfo);
}
}

使用

1
2
3
4
5
6
java复制代码// 在具体的路由函数中使用定义的注解就可以了
@GetMapping("/list")
public Object userList(@LoginRequire Long userId) {
System.out.println(userId);
return "ok";
}

总结

套用一个装逼的词,会话管理没有银弹! 无论是session还是JWT都有各自优缺点,正确的做法是根据实际的业务场景需求,选择合适的方案。JWT,你学废了吗?

本文转载自: 掘金

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

Java之Java 8新特性

发表于 2021-05-14

Java 基础系列的笔记终于完成了,完结撒花_★,°_:.☆( ̄▽ ̄)/$:.°★ 。

附上所有代码的地址:Java基础系列代码

Java 8新特性汇总

image-20200507101934984

Java 8的改进

  • 速度更快
  • 代码更少(增加了新的语法:Lambda表达式)
  • 引入强大的 Stream APl
  • 便于并行
  • 最大化减少空指针异常:Optional
  • Nashorn 引擎,允许在JVM上运行 JS 应用
  • 并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。相比较串行的流,并行的流可以很大程度上提高程序的执行效率。
  • Java 8中将并行进行了优化,我们可以很容易的对数据进行并行操作。Stream API 可以声明性地通过 parallel() 与 sequential() 在并行流与顺序流之间进行切换

一、Lambda 表达式

1. Lamdba 表达式概述

Lambda 是一个匿名函数,可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。使用它可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使 Java 的语言表达能力得到了提升。

2. 使用 Lambda 表达式前后对比

示例一:调用 Runable 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Test
public void test1(){
//未使用Lambda表达式的写法
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("hello Lambda!");
}
};
r1.run();

System.out.println("========================");
//Lamdba表达式写法
Runnable r2 = () -> System.out.println("hi Lambda!");
r2.run();
}

示例二:使用Comparator接口

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复制代码@Test
public void test2(){
//未使用Lambda表达式的写法
Comparator<Integer> com1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1,o2);
}
};

int compare1 = com1.compare(12, 32);
System.out.println(compare1);//-1
System.out.println("===================");

//Lambda表达式的写法
Comparator<Integer> com2 = (o1,o2) -> Integer.compare(o1,o2);

int compare2 = com2.compare(54, 21);
System.out.println(compare2);//1
System.out.println("===================");

//方法引用
Comparator<Integer> cpm3 = Integer::compareTo;
int compare3 = cpm3.compare(12, 12);
System.out.println(compare3);//0
}

3. 怎样使用Lambda表达式

3.1 Lamdba表达式基本语法

1.举例: (o1,o2) -> Integer.compare(o1,o2);

2.格式:

  • -> :lambda 操作符 或 箭头操作符
  • -> 左边:lambda 形参列表 (其实就是接口中的抽象方法的形参列表)
  • -> 右边:lambda 体(其实就是重写的抽象方法的方法体)

3.2 Lamdba表达式使用(包含六种情况)

3.2.1 语法格式一:无参,有返回值

1
java复制代码Runnable r1 = () -> {System.out.println(“hello Lamdba!”)}

3.2.2 语法格式二:Lamdba需要一个参数,但没有返回值

1
java复制代码Consumer<String> con = (String str) -> {System.out.println(str)}

3.2.3 语法格式三:数据类型可省略,因为可由编译器推断得出,称为类型推断

1
java复制代码Consumer<String> con = (str) -> {System.out.println(str)}

3.2.4 语法格式四:Lamdba若只需要一个参数时,小括号可以省略

1
java复制代码Consumer<String> con = str -> {System.out.println(str)}

3.2.5 语法格式五:Lamdba需要两个以上的参数,多条执行语句,并且可以有返回值

1
2
3
4
java复制代码Comparator<Integer>com = (o1,o1) -> {
Syste.out.println("Lamdba表达式使用");
return Integer.compare(o1,o2);
}

3.2.6 语法格式六:当Lamdba体只有一条语句时,return和大括号若有,都可以省略

1
java复制代码Comparator<Integer>com = (o1,o1) ->	Integer.compare(o1,o2);

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
java复制代码public class LamdbaTest2 {
//语法格式一:无参,无返回值
@Test
public void test1() {
//未使用Lambda表达式
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Hello Lamdba");
}
};
r1.run();
System.out.println("====================");
//使用Lambda表达式
Runnable r2 = () -> {
System.out.println("Hi Lamdba");
};
r2.run();
}

//语法格式二:Lambda 需要一个参数,但是没有返回值。
@Test
public void test2() {
//未使用Lambda表达式
Consumer<String> con = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
con.accept("你好啊Lambda!");
System.out.println("====================");
//使用Lambda表达式
Consumer<String> con1 = (String s) -> {
System.out.println(s);
};
con1.accept("我是Lambda");

}

//语法格式三:数据类型可以省略,因为可由编译器推断得出,称为“类型推断”
@Test
public void test3() {
//未使用Lambda表达式
Consumer<String> con = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
con.accept("你好啊Lambda!");
System.out.println("====================");
//使用Lambda表达式
Consumer<String> con1 = (s) -> {
System.out.println(s);
};
con1.accept("我是Lambda");
}

@Test
public void test(){
ArrayList<String> list = new ArrayList<>();//类型推断,用左边推断右边
int[] arr = {1,2,3,4};//类型推断,用左边推断右边
}

//语法格式四:Lambda 若只需要一个参数时,参数的小括号可以省略
@Test
public void test4() {
//未使用Lambda表达式
Consumer<String> con = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
con.accept("你好啊Lambda!");
System.out.println("====================");
//使用Lambda表达式
Consumer<String> con1 = s -> {
System.out.println(s);
};
con1.accept("我是Lambda");
}

//语法格式五:Lambda 需要两个或以上的参数,多条执行语句,并且可以有返回值
@Test
public void test5() {
//未使用Lambda表达式
Comparator<Integer> com1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
System.out.println(o1);
System.out.println(o2);
return Integer.compare(o1, o2);
}
};
System.out.println(com1.compare(23, 45));
System.out.println("====================");
//使用Lambda表达式
Comparator<Integer> com2 = (o1, o2) -> {
System.out.println(o1);
System.out.println(o2);
return o1.compareTo(o2);
};
System.out.println(com2.compare(23, 12));
}

//语法格式六:当 Lambda 体只有一条语句时,return 与大括号若有,都可以省略
@Test
public void test6() {
//未使用Lambda表达式
Comparator<Integer> com1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1, o2);
}
};
System.out.println(com1.compare(23, 45));
System.out.println("====================");
//使用Lambda表达式
Comparator<Integer> com2 = (o1, o2) -> o1.compareTo(o2);

System.out.println(com2.compare(23, 12));
}
@Test
public void test7(){
//未使用Lambda表达式
Consumer<String> con1 = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
con1.accept("hi!");
System.out.println("====================");
//使用Lambda表达式
Consumer<String> con2 = s -> System.out.println(s);
con2.accept("hello");
}

}

3.3 Lambda 表达式使用总结

  • -> 左边:lambda 形参列表的参数类型可以省略(类型推断);如果 lambda 形参列表只有一个参数,其一对 () 也可以省略
  • -> 右边:lambda 体应该使用一对 {} 包裹;如果 lambda 体只有一条执行语句(可能是 return 语句),省略这一对 {} 和 return 关键字

4. Lamdba表达式总结

  • Lambda 表达式的本质:作为函数式接口的实例
  • 如果一个接口中,只声明了一个抽象方法,则此接口就称为函数式接口。我们可以在一个接口上使用 @FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口。
  • 因此以前用匿名实现类表示的现在都可以用 Lambda 表达式来写。

二、函数式接口

1. 函数式接口概述

  • 只包含一个抽象方法的接口,称为函数式接口。
  • 可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda 表达式抛出一个受检异常(即:非运行时异常),那么该异常需要在目标接口的抽象方法上进行声明)。
  • 可以在一个接口上使用 @FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口。同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。
  • Lambda 表达式的本质:作为函数式接口的实例
  • 在 java.util.function 包下定义了Java 8的丰富的函数式接口

自定义函数式接口

1
2
3
4
java复制代码@FunctionalInterface
public interface MyInterface {
void method1();
}

3. Java内置函数式接口

3.1 四大核心函数式接口

image-20200507110931093

应用举例

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
java复制代码public class LambdaTest3 {
// 消费型接口 Consumer<T> void accept(T t)
@Test
public void test1() {
//未使用Lambda表达式
Learn("java", new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println("学习什么? " + s);
}
});
System.out.println("====================");
//使用Lambda表达
Learn("html", s -> System.out.println("学习什么? " + s));

}

private void Learn(String s, Consumer<String> stringConsumer) {
stringConsumer.accept(s);
}

// 供给型接口 Supplier<T> T get()
@Test
public void test2() {
//未使用Lambdabiaodas
Supplier<String> sp = new Supplier<String>() {
@Override
public String get() {
return new String("我能提供东西");
}
};
System.out.println(sp.get());
System.out.println("====================");
//使用Lambda表达
Supplier<String> sp1 = () -> new String("我能通过lambda提供东西");
System.out.println(sp1.get());
}

//函数型接口 Function<T,R> R apply(T t)
@Test
public void test3() {
//使用Lambda表达式
Employee employee = new Employee(1001, "Tom", 45, 10000);

Function<Employee, String> func1 =e->e.getName();
System.out.println(func1.apply(employee));
System.out.println("====================");

//使用方法引用
Function<Employee,String>func2 = Employee::getName;
System.out.println(func2.apply(employee));

}

//断定型接口 Predicate<T> boolean test(T t)
@Test
public void test4() {
//使用匿名内部类
Function<Double, Long> func = new Function<Double, Long>() {
@Override
public Long apply(Double aDouble) {
return Math.round(aDouble);
}
};
System.out.println(func.apply(10.5));
System.out.println("====================");

//使用Lambda表达式
Function<Double, Long> func1 = d -> Math.round(d);
System.out.println(func1.apply(12.3));
System.out.println("====================");

//使用方法引用
Function<Double,Long>func2 = Math::round;
System.out.println(func2.apply(12.6));

}
}

3.2 其他函数式接口

image-20200507111244577

4. 使用总结

4.1 何时使用lambda表达式?

当需要对一个函数式接口实例化的时候,可以使用 lambda 表达式。

4.2 何时使用给定的函数式接口?

如果我们开发中需要定义一个函数式接口,首先看看在已有的jdk提供的函数式接口是否提供了能满足需求的函数式接口。如果有,则直接调用即可,不需要自己再自定义了。

三、方法的引用

1. 方法引用概述

方法引用可以看做是 Lambda 表达式深层次的表达。换句话说,方法引用就是 Lambda 表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法。

2. 使用情景

当要传递给 Lambda 体的操作,已经实现的方法了,可以使用方法引用!

3. 使用格式

类(或对象) :: 方法名

4. 使用情况

  • 情况1 对象 :: 非静态方法
  • 情况2 类 :: 静态方法
  • 情况3 类 :: 非静态方法

5. 使用要求

  • 要求接口中的抽象方法的形参列表和返回值类型与方法引用的方法的形参列表和返回值类型相同!(针对于情况1和情况2)
  • 当函数式接口方法的第一个参数是需要引用方法的调用者,并且第二个参数是需要引用方法的参数(或无参数)时:ClassName::methodName(针对于情况3)

6. 使用建议

如果给函数式接口提供实例,恰好满足方法引用的使用情境,就可以考虑使用方法引用给函数式接口提供实例。如果不熟悉方法引用,那么还可以使用 lambda 表达式。

7. 使用举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
java复制代码public class MethodRefTest {

// 情况一:对象 :: 实例方法
//Consumer中的void accept(T t)
//PrintStream中的void println(T t)
@Test
public void test1() {
//使用Lambda表达
Consumer<String> con1 = str -> System.out.println(str);
con1.accept("中国");
System.out.println("====================");

//使用方法引用
PrintStream ps = System.out;
Consumer con2 = ps::println;
con2.accept("China");

}

//Supplier中的T get()
//Employee中的String getName()
@Test
public void test2() {
//使用Lambda表达
Employee emp = new Employee(1001, "Bruce", 34, 600);
Supplier<String> sup1 = () -> emp.getName();
System.out.println(sup1.get());
System.out.println("====================");

//使用方法引用
Supplier sup2 = emp::getName;
System.out.println(sup2.get());


}

// 情况二:类 :: 静态方法
//Comparator中的int compare(T t1,T t2)
//Integer中的int compare(T t1,T t2)
@Test
public void test3() {
//使用Lambda表达
Comparator<Integer> com1 = (t1, t2) -> Integer.compare(t1, t2);
System.out.println(com1.compare(32, 45));
System.out.println("====================");

//使用方法引用
Comparator<Integer> com2 = Integer::compareTo;
System.out.println(com2.compare(43, 34));
}

//Function中的R apply(T t)
//Math中的Long round(Double d)
@Test
public void test4() {
//使用匿名内部类
Function<Double, Long> func = new Function<Double, Long>() {
@Override
public Long apply(Double aDouble) {
return Math.round(aDouble);
}
};
System.out.println(func.apply(10.5));
System.out.println("====================");

//使用Lambda表达式
Function<Double, Long> func1 = d -> Math.round(d);
System.out.println(func1.apply(12.3));
System.out.println("====================");

//使用方法引用
Function<Double, Long> func2 = Math::round;
System.out.println(func2.apply(12.6));


}

// 情况三:类 :: 实例方法
// Comparator中的int comapre(T t1,T t2)
// String中的int t1.compareTo(t2)
@Test
public void test5() {
//使用Lambda表达式
Comparator<String> com1 = (s1, s2) -> s1.compareTo(s2);
System.out.println(com1.compare("abd", "aba"));
System.out.println("====================");

//使用方法引用
Comparator<String> com2 = String::compareTo;
System.out.println(com2.compare("abd", "abc"));
}

//BiPredicate中的boolean test(T t1, T t2);
//String中的boolean t1.equals(t2)
@Test
public void test6() {
//使用Lambda表达式
BiPredicate<String, String> pre1 = (s1, s2) -> s1.equals(s2);
System.out.println(pre1.test("abc", "abc"));
System.out.println("====================");

//使用方法引用
BiPredicate<String, String> pre2 = String::equals;
System.out.println(pre2.test("abc", "abd"));

}

// Function中的R apply(T t)
// Employee中的String getName();
@Test
public void test7() {
//使用Lambda表达式
Employee employee = new Employee(1001, "Tom", 45, 10000);

Function<Employee, String> func1 =e->e.getName();
System.out.println(func1.apply(employee));
System.out.println("====================");

//使用方法引用
Function<Employee,String>func2 = Employee::getName;
System.out.println(func2.apply(employee));
}
}

四、构造器和数组的引用

1. 使用格式

方法引用:类名 ::new

数组引用:数组类型 [] :: new

2. 使用要求

2.1 构造器引用

和方法引用类似,函数式接口的抽象方法的形参列表和构造器的形参列表一致。抽象方法的返回值类型即为构造器所属的类的类型

2.2 数组引用

可以把数组看做是一个特殊的类,则写法与构造器引用一致。

3. 使用举例

3.1 构造器引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
java复制代码//构造器引用
//Supplier中的T get()
@Test
public void test1() {
//使用匿名内部类
Supplier<Employee> sup = new Supplier<Employee>() {
@Override
public Employee get() {
return new Employee();
}
};
System.out.println(sup.get());
//使用Lambda表达式
System.out.println("====================");
Supplier<Employee> sup1 = () -> new Employee(1001, "Tom", 43, 13333);
System.out.println(sup1.get());

//使用方法引用
Supplier<Employee> sup2 = Employee::new;
System.out.println(sup2.get());

}

//Function中的R apply(T t)
@Test
public void test2() {
//使用Lambda表达式
Function<Integer, Employee> func1 = id -> new Employee(id);
Employee employee = func1.apply(1001);
System.out.println(employee);
System.out.println("====================");

//使用方法引用
Function<Integer, Employee> func2 = Employee::new;
Employee employee1 = func2.apply(1002);
System.out.println(employee1);

}

//BiFunction中的R apply(T t,U u)
@Test
public void test3() {
//使用Lambda表达式
BiFunction<Integer, String, Employee> func1 = (id, name) -> new Employee(id, name);
System.out.println(func1.apply(1001, "Tom"));
System.out.println("====================");

//使用方法引用
BiFunction<Integer, String, Employee> func2 = Employee::new;
System.out.println(func2.apply(1002, "Jarry"));
}

3.2 数组引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码//数组引用
//Function中的R apply(T t)
@Test
public void test4() {
Function<Integer, String[]> func1 = length -> new String[length];
String[] arr1 = func1.apply(5);
System.out.println(Arrays.toString(arr1));

System.out.println("====================");

//使用方法引用
Function<Integer,String[]>func2=String[]::new;
String[] arr2 = func2.apply(10);
System.out.println(Arrays.toString(arr2));
}

五、StreamAPI

1. Stream API概述

  • Stream 关注的是对数据的运算,与 CPU 打交道;集合关注的是数据的存储,与内存打交道;
  • Java 8 提供了一套 api ,使用这套 api 可以对内存中的数据进行过滤、排序、映射、归约等操作。类似于 sql 对数据库中表的相关操作。
  • Stream 是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。“集合讲的是数据, Stream讲的是计算!”

使用注意点:

① Stream 自己不会存储元素。

② Stream 不会改变源对象。相反,他们会返回一个持有结果的新 Stream。

③ Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。

2. Stream 使用流程

① Stream 的实例化

② 一系列的中间操作(过滤、映射、…)

③ 终止操作

image-20200507114714381

使用流程中的注意点:

  • 一个中间操作链,对数据源的数据进行处理
  • 一旦执行终止操作,就执行中间操作链,并产生结果。之后,不会再被使用

3. 使用方法

3.1 步骤一 创建 Stream

3.1.1 创建方式一:通过集合

Java 8的 Collection 接口被扩展,提供了两个获取流的方法:

  • default Stream\<E> stream() : 返回一个顺序流
  • default Stream\<E> parallelStream() : 返回一个并行流

3.1.2 创建方式二:通过数组

Java 8中的 Arrays 的静态方法 stream() 可以获取数组流

  • 调用 Arrays 类的 static\<T> Stream\<T> stream(T[] array): 返回一个流
  • 重载形式,能够处理对应基本类型的数组:
    • public static IntStream stream(int[] array)
    • public static LongStream stream(long[] array)
    • public static DoubleStream stream(double[] array)

3.1.3 创建方式三:通过Stream的of()方法

可以调用Stream类静态方法of(),通过显示值创建一个流。可以用于接收任意数量的参数

  • public static \<T>Stream\<T> of(T...values):返回一个流

3.1.4 创建方式四:创建无限流

  • 迭代: public static\<T> Stream\<T> iterate(final T seed, final UnaryOperator\<T> f)
  • 生成: public static\<T> Stream\<T> generate(Supplier\<T> s)

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
java复制代码public class StreamAPITest1 {
//创建 Stream方式一:通过集合
@Test
public void test1() {
List<Employee> employees = EmployeeData.getEmployees();
//efault Stream<E> stream() : 返回一个顺序流
Stream<Employee> stream = employees.stream();

//default Stream<E> parallelStream() : 返回一个并行流
Stream<Employee> employeeStream = employees.parallelStream();
}

//创建 Stream方式二:通过数组
@Test
public void test2() {
int[] arrs = {1, 2, 3, 6, 2};
//调用Arrays类的static <T> Stream<T> stream(T[] array): 返回一个流
IntStream stream = Arrays.stream(arrs);

Employee e1 = new Employee(1001, "Tom");
Employee e2 = new Employee(1002, "Jerry");
Employee[] employees = {e1, e2};
Stream<Employee> stream1 = Arrays.stream(employees);
}

//创建 Stream方式三:通过Stream的of()
@Test
public void test3() {
Stream<Integer> integerStream = Stream.of(12, 34, 45, 65, 76);
}

//创建 Stream方式四:创建无限流
@Test
public void test4() {

//迭代
//public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
//遍历前10个偶数
Stream.iterate(0, t -> t + 2).limit(10).forEach(System.out::println);

//生成
//public static<T> Stream<T> generate(Supplier<T> s)
Stream.generate(Math::random).limit(10).forEach(System.out::println);
}
}

3.2 步骤二 中间操作

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为惰性求值。

3.2.1 筛选与切片

image-20200507115721208

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码//1-筛选与切片,注意执行终止操作后,Stream流就被关闭了,使用时需要再次创建Stream流
@Test
public void test1(){
List<Employee> employees = EmployeeData.getEmployees();
//filter(Predicate p)——接收 Lambda , 从流中排除某些元素。
Stream<Employee> employeeStream = employees.stream();
//练习:查询员工表中薪资大于7000的员工信息
employeeStream.filter(e -> e.getSalary() > 7000).forEach(System.out::println);

//limit(n)——截断流,使其元素不超过给定数量。
employeeStream.limit(3).forEach(System.out::println);
System.out.println();

//skip(n) —— 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补
employeeStream.skip(3).forEach(System.out::println);
//distinct()——筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
employees.add(new Employee(1010,"刘庆东",56,8000));
employees.add(new Employee(1010,"刘庆东",56,8000));
employees.add(new Employee(1010,"刘庆东",56,8000));
employees.add(new Employee(1010,"刘庆东",56,8000));

employeeStream.distinct().forEach(System.out::println);
}

3.2.2 映射

image-20200507115738168

代码示例:

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
java复制代码//2-映射
@Test
public void test2(){
List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
//map(Function f)——接收一个函数作为参数,将元素转换成其他形式或提取信息,该函数会被应用到每个元素上,并将其映射成一个新的元素。
list.stream().map(str -> str.toUpperCase()).forEach(System.out::println);

//练习1:获取员工姓名长度大于3的员工的姓名。
List<Employee> employees = EmployeeData.getEmployees();
Stream<String> nameStream = employees.stream().map(Employee::getName);
nameStream.filter(name -> name.length() >3).forEach(System.out::println);
System.out.println();
//练习2:使用map()中间操作实现flatMap()中间操作方法
Stream<Stream<Character>> streamStream = list.stream().map(StreamAPITest2::fromStringToStream);
streamStream.forEach(s ->{
s.forEach(System.out::println);
});
System.out.println();
//flatMap(Function f)——接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。
Stream<Character> characterStream = list.stream().flatMap(StreamAPITest2::fromStringToStream);
characterStream.forEach(System.out::println);

}
//将字符串中的多个字符构成的集合转换为对应的Stream的实例
public static Stream<Character>fromStringToStream(String str){
ArrayList<Character> list = new ArrayList<>();
for (Character c :
str.toCharArray()) {
list.add(c);
}
return list.stream();
}
//map()和flatMap()方法类似于List中的add()和addAll()方法
@Test
public void test(){
ArrayList<Object> list1 = new ArrayList<>();
list1.add(1);
list1.add(2);
list1.add(3);
list1.add(4);

ArrayList<Object> list2 = new ArrayList<>();
list2.add(5);
list2.add(6);
list2.add(7);
list2.add(8);

list1.add(list2);
System.out.println(list1);//[1, 2, 3, 4, [5, 6, 7, 8]]
list1.addAll(list2);
System.out.println(list1);//[1, 2, 3, 4, [5, 6, 7, 8], 5, 6, 7, 8]

}

3.2.3 排序

image-20200507115751929

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码//3-排序
@Test
public void test3(){
//sorted()——自然排序
List<Integer> list = Arrays.asList(12, 34, 54, 65, 32);
list.stream().sorted().forEach(System.out::println);

//抛异常,原因:Employee没有实现Comparable接口
List<Employee> employees = EmployeeData.getEmployees();
employees.stream().sorted().forEach(System.out::println);

//sorted(Comparator com)——定制排序
List<Employee> employees1 = EmployeeData.getEmployees();
employees1.stream().sorted((e1,e2)->{
int ageValue = Integer.compare(e1.getAge(), e2.getAge());
if (ageValue != 0){
return ageValue;
}else {
return -Double.compare(e1.getSalary(),e2.getSalary());
}

}).forEach(System.out::println);
}

3.3 步骤三 终止操作

  • 终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、 Integer,甚至是 void
  • 流进行了终止操作后,不能再次使用。

3.3.1 匹配与查找

image-20200507120245034

image-20200507120300023

代码示例:

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
java复制代码//1-匹配与查找
@Test
public void test1(){
List<Employee> employees = EmployeeData.getEmployees();

//allMatch(Predicate p)——检查是否匹配所有元素。
//练习:是否所有的员工的年龄都大于18
boolean allMatch = employees.stream().allMatch(e -> e.getAge() > 18);
System.out.println(allMatch);
//anyMatch(Predicate p)——检查是否至少匹配一个元素。
//练习:是否存在员工的工资大于 5000
boolean anyMatch = employees.stream().anyMatch(e -> e.getSalary() > 5000);
System.out.println(anyMatch);

//noneMatch(Predicate p)——检查是否没有匹配的元素。
//练习:是否存在员工姓“雷”
boolean noneMatch = employees.stream().noneMatch(e -> e.getName().startsWith("雷"));
System.out.println(noneMatch);

//findFirst——返回第一个元素
Optional<Employee> first = employees.stream().findFirst();
System.out.println(first);

//findAny——返回当前流中的任意元素
Optional<Employee> employee = employees.parallelStream().findAny();
System.out.println(employee);


}

@Test
public void test2(){
List<Employee> employees = EmployeeData.getEmployees();
// count——返回流中元素的总个数
long count = employees.stream().filter(e -> e.getSalary()>5000).count();
System.out.println(count);

//max(Comparator c)——返回流中最大值
//练习:返回最高的工资
Stream<Double> salaryStream = employees.stream().map(e -> e.getSalary());
Optional<Double> maxSalary = salaryStream.max(Double::compareTo);
System.out.println(maxSalary);

//min(Comparator c)——返回流中最小值
//练习:返回最低工资的员工
Optional<Double> minSalary = employees.stream().map(e -> e.getSalary()).min(Double::compareTo);
System.out.println(minSalary);

//forEach(Consumer c)——内部迭代
employees.stream().forEach(System.out::println);
System.out.println();
//使用集合的遍历操作
employees.forEach(System.out::println);

}

3.3.2 归约

image-20200507120318120

备注:map 和 reduce 的连接通常称为 map-reduce 模式,因 Google 用它来进行网络搜索而出名

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码//2-归约
@Test
public void test3(){
//reduce(T identity, BinaryOperator)——可以将流中元素反复结合起来,得到一个值。返回 T
//练习1:计算1-10的自然数的和
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Integer sum = list.stream().reduce(0, Integer::sum);
System.out.println(sum);

//reduce(BinaryOperator) ——可以将流中元素反复结合起来,得到一个值。返回 Optional<T>
//练习2:计算公司所有员工工资的总和
List<Employee> employees = EmployeeData.getEmployees();
Optional<Double> sumSalary = employees.stream().map(e -> e.getSalary()).reduce(Double::sum);
System.out.println(sumSalary);

}

3.3.3 收集

image-20200507120339459

Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)

Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例具体方法与实例如下表:

image-20200507120628702

image-20200507120648175

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码//3-收集
@Test
public void test4(){
//collect(Collector c)——将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法
//练习1:查找工资大于6000的员工,结果返回为一个List或Set
List<Employee> employees = EmployeeData.getEmployees();
List<Employee> employeeList = employees.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toList());

employeeList.forEach(System.out::println);
System.out.println();
Set<Employee> employeeSet = employees.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toSet());
employeeSet.forEach(System.out::println);
}

六、Optional 类的使用

1. OPtional 类的概述

  • 为了解决 java 中的空指针问题而生!
  • Optional<T> 类(java.util.Optional) 是一个容器类,它可以保存类型 T 的值,代表这个值存在。或者仅仅保存 null,表示这个值不存在。原来用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念。并且可以避免空指针异常。

2. Optional 类提供的方法

Optional 类提供了很多方法,可以不用再现实的进行空值检验。

2.1 创建 Optional 类对象的方法

  • Optional.of(T t) : 创建一个 Optional 实例,t 必须非空;
  • Optional.empty() : 创建一个空的 Optional 实例
  • Optional.ofNullable(T t):t 可以为 null

2.2 判断Optional容器是否包含对象

  • boolean isPresent():判断是否包含对象
  • void ifPresent(Consumer<? super T> consumer):如果有值,就执行 Consumer 接口的实现代码,并且该值会作为参数传给它。

2.3 获取 Optional 容器的对象

  • T get():如果调用对象包含值,返回该值,否则抛异常
  • T orElse(T other):如果有值则将其返回,否则返回指定的 other 对象
  • T orElseGet(Supplier<? extends t> other):如果有值则将其返回,否则返回由 Supplier 接口实现提供的对象。
  • T orElseThrow(Supplier<? extends X> exceptionSupplier):如果有值则将其返回,否则抛出由 Supplier 接口实现提供的异常。

2.4 搭配使用

  • of() 和 get() 方法搭配使用,明确对象非空
  • ofNullable() 和 orElse() 搭配使用,不确定对象非空

3. 应用举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码public class OptionalTest {
@Test
public void test1() {
//empty():创建的Optional对象内部的value = null
Optional<Object> op1 = Optional.empty();
if (!op1.isPresent()){//Optional封装的数据是否包含数据
System.out.println("数据为空");
}
System.out.println(op1);
System.out.println(op1.isPresent());

//如果Optional封装的数据value为空,则get()报错。否则,value不为空时,返回value.
System.out.println(op1.get());
}
@Test
public void test2(){
String str = "hello";
// str = null;
//of(T t):封装数据t生成Optional对象。要求t非空,否则报错。
Optional<String> op1 = Optional.of(str);
//get()通常与of()方法搭配使用。用于获取内部的封装的数据value
String str1 = op1.get();
System.out.println(str1);
}
@Test
public void test3(){
String str ="Beijing";
str = null;
//ofNullable(T t) :封装数据t赋给Optional内部的value。不要求t非空
Optional<String> op1 = Optional.ofNullable(str);
System.out.println(op1);
//orElse(T t1):如果Optional内部的value非空,则返回此value值。如果
//value为空,则返回t1.
String str2 = op1.orElse("shanghai");
System.out.println(str2);
}
}

使用 Optional 类避免产生空指针异常

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
java复制代码public class GirlBoyOptionalTest {

//使用原始方法进行非空检验
public String getGrilName1(Boy boy){
if (boy != null){
Girl girl = boy.getGirl();
if (girl != null){
return girl.getName();
}
}
return null;
}
//使用Optional类的getGirlName()进行非空检验
public String getGirlName2(Boy boy){
Optional<Boy> boyOptional = Optional.ofNullable(boy);
//此时的boy1一定非空,boy为空是返回“迪丽热巴”
Boy boy1 = boyOptional.orElse(new Boy(new Girl("迪丽热巴")));

Girl girl = boy1.getGirl();
//girl1一定非空,girl为空时返回“古力娜扎”
Optional<Girl> girlOptional = Optional.ofNullable(girl);
Girl girl1 = girlOptional.orElse(new Girl("古力娜扎"));

return girl1.getName();
}

//测试手动写的控制检测
@Test
public void test1(){

Boy boy = null;
System.out.println(getGrilName1(boy));

boy = new Boy();
System.out.println(getGrilName1(boy));

boy = new Boy(new Girl("杨幂"));
System.out.println(getGrilName1(boy));
}
//测试用Optional类写的控制检测
@Test
public void test2(){
Boy boy = null;
System.out.println(getGirlName2(boy));

boy = new Boy();
System.out.println(getGirlName2(boy));

boy = new Boy(new Girl("杨幂"));
System.out.println(getGirlName2(boy));

}
}

七、对反射的支持增强

提高了创建对象、对象赋值和反射创建对象的时间

代码示例:

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
java复制代码public class testReflection {
// 循环次数10亿次
private static final int loopCnt = 1000 * 1000 * 1000;

public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
// 输出jdk版本
System.out.println("java version is" + System.getProperty("java.version"));
creatNewObject();
optionObject();
reflectCreatObject();
}

// person对象
static class Person {
private Integer age = 20;

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}
}

// 每次创建新对象
public static void creatNewObject() {
long startTime = System.currentTimeMillis();
for (int i = 0; i < loopCnt; i++) {
Person person = new Person();
person.setAge(30);
}
long endTime = System.currentTimeMillis();
System.out.println("循环十亿次创建对象所需的时间:" + (endTime - startTime));
}

// 为同一个对象赋值
public static void optionObject() {
long startTime = System.currentTimeMillis();
Person p = new Person();
for (int i = 0; i < loopCnt; i++) {
p.setAge(10);
}
long endTime = System.currentTimeMillis();
System.out.println("循环十亿次为同一对象赋值所需的时间:" + (endTime - startTime));
}

// 通过反射创建对象
public static void reflectCreatObject() throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
long startTime = System.currentTimeMillis();
Class<Person> personClass = Person.class;
Person person = personClass.newInstance();
Method setAge = personClass.getMethod("setAge", Integer.class);
for (int i = 0; i < loopCnt; i++) {
setAge.invoke(person, 90);
}
long endTime = System.currentTimeMillis();
System.out.println("循环十亿次反射创建对象所需的时间:" + (endTime - startTime));
}
}

编译级别为JDK8时

1
2
3
4
txt复制代码java version is 1.8.0_201
循环十亿次创建对象所需的时间:9
循环十亿次为同一对象赋值所需的时间:59
循环十亿次反射创建对象所需的时间:2622

编译级别为JDK7时

1
2
3
4
txt复制代码java version is 1.7
循环十亿次创建对象所需的时间:6737
循环十亿次为同一对象赋值所需的时间:3394
循环十亿次反射创建对象所需的时间:293603

本文转载自: 掘金

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

面试三连:什么是死锁?怎么排查死锁?怎么避免死锁? 01 死

发表于 2021-05-14

在面试过程中,死锁也是高频的考点,因为如果线上环境很多发生了死锁,那真的出大事了。

这次,我们就来系统地聊聊死锁的问题。

  • 死锁的概念;
  • 模拟死锁问题的产生;
  • 利用工具排查死锁问题;
  • 避免死锁问题的发生;

01 死锁的概念

在多线程编程中,我们为了防止多线程竞争共享资源而导致数据错乱,都会在操作共享资源之前加上互斥锁,只有成功获得到锁的线程,才能操作共享资源,获取不到锁的线程就只能等待,直到锁被释放。

那么,当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁。

举个例子,小林拿了小美房间的钥匙,而小林在自己的房间里,小美拿了小林房间的钥匙,而小美也在自己的房间里。如果小林要从自己的房间里出去,必须拿到小美手中的钥匙,但是小美要出去,又必须拿到小林手中的钥匙,这就形成了死锁。

死锁只有同时满足以下四个条件才会发生:

  • 互斥条件;
  • 持有并等待条件;
  • 不可剥夺条件;
  • 环路等待条件;

1.1 互斥条件

互斥条件是指多个线程不能同时使用同一个资源。

比如下图,如果线程 A 已经持有的资源,不能再同时被线程 B 持有,如果线程 B 请求获取线程 A 已经占用的资源,那线程 B 只能等待,直到线程 A 释放了资源。

1.2 持有并等待条件

持有并等待条件是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。

1.3 不可剥夺条件

不可剥夺条件是指,当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。

1.4 环路等待条件

环路等待条件指都是,在死锁发生的时候,两个线程获取资源的顺序构成了环形链。

比如,线程 A 已经持有资源 2,而想请求资源 1, 线程 B 已经获取了资源 1,而想请求资源 2,这就形成资源请求等待的环形图

个人整理了一些资料,有需要的朋友可以直接点击领取。

25大Java面试专题(附解析)

从0到1Java学习路线和资料

Java核心知识集

左程云算法

02 模拟死锁问题的产生

Talk is cheap. Show me the code.

下面,我们用代码来模拟死锁问题的产生。

首先,我们先创建 2 个线程,分别为线程 A 和 线程 B,然后有两个互斥锁,分别是 mutex_A 和 mutex_B,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
arduino复制代码pthread_mutex_t mutex_A = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex_B = PTHREAD_MUTEX_INITIALIZER;

int main()
{
pthread_t tidA, tidB;

//创建两个线程
pthread_create(&tidA, NULL, threadA_proc, NULL);
pthread_create(&tidB, NULL, threadB_proc, NULL);

pthread_join(tidA, NULL);
pthread_join(tidB, NULL);

printf("exit\n");

return 0;
}

接下来,我们看下线程 A 函数做了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码//线程函数 A
void *threadA_proc(void *data)
{
printf("thread A waiting get ResourceA \n");
pthread_mutex_lock(&mutex_A);
printf("thread A got ResourceA \n");

sleep(1);

printf("thread A waiting get ResourceB \n");
pthread_mutex_lock(&mutex_B);
printf("thread A got ResourceB \n");

pthread_mutex_unlock(&mutex_B);
pthread_mutex_unlock(&mutex_A);
return (void *)0;
}

可以看到,线程 A 函数的过程:

  • 先获取互斥锁 A,然后睡眠 1 秒;
  • 再获取互斥锁 B,然后释放互斥锁 B;
  • 最后释放互斥锁 A;

//线程函数 B
void *threadB_proc(void *data)
{
printf(“thread B waiting get ResourceB \n”);
pthread_mutex_lock(&mutex_B);
printf(“thread B got ResourceB \n”);

1
2
3
4
5
6
7
8
9
scss复制代码sleep(1);

printf("thread B waiting get ResourceA \n");
pthread_mutex_lock(&mutex_A);
printf("thread B got ResourceA \n");

pthread_mutex_unlock(&mutex_A);
pthread_mutex_unlock(&mutex_B);
return (void *)0;

}

可以看到,线程 B 函数的过程:

  • 先获取互斥锁 B,然后睡眠 1 秒;
  • 再获取互斥锁 A,然后释放互斥锁 A;
  • 最后释放互斥锁 B;

然后,我们运行这个程序,运行结果如下:

1
2
3
4
5
6
7
arduino复制代码thread B waiting get ResourceB 
thread B got ResourceB
thread A waiting get ResourceA
thread A got ResourceA
thread B waiting get ResourceA
thread A waiting get ResourceB
// 阻塞中。。。

可以看到线程 B 在等待互斥锁 A 的释放,线程 A 在等待互斥锁 B 的释放,双方都在等待对方资源的释放,很明显,产生了死锁问题。

03 利用工具排查死锁问题

如果你想排查你的 Java 程序是否死锁,则可以使用 jstack 工具,它是 jdk 自带的线程堆栈分析工具。

由于小林的死锁代码例子是 C 写的,在 Linux 下,我们可以使用 pstack + gdb 工具来定位死锁问题。

pstack 命令可以显示每个线程的栈跟踪信息(函数调用过程),它的使用方式也很简单,只需要 pstack 就可以了。

那么,在定位死锁问题时,我们可以多次执行 pstack 命令查看线程的函数调用过程,多次对比结果,确认哪几个线程一直没有变化,且是因为在等待锁,那么大概率是由于死锁问题导致的。

我用 pstack 输出了我前面模拟死锁问题的进程的所有线程的情况,我多次执行命令后,其结果都一样,如下:

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
shell复制代码$ pstack 87746
Thread 3 (Thread 0x7f60a610a700 (LWP 87747)):
#0 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0
#2 0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0
#3 0x0000000000400725 in threadA_proc ()
#4 0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0
#5 0x00000037206f4bfd in clone () from /lib64/libc.so.6
Thread 2 (Thread 0x7f60a5709700 (LWP 87748)):
#0 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0
#2 0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0
#3 0x0000000000400792 in threadB_proc ()
#4 0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0
#5 0x00000037206f4bfd in clone () from /lib64/libc.so.6
Thread 1 (Thread 0x7f60a610c700 (LWP 87746)):
#0 0x0000003720e080e5 in pthread_join () from /lib64/libpthread.so.0
#1 0x0000000000400806 in main ()

....

$ pstack 87746
Thread 3 (Thread 0x7f60a610a700 (LWP 87747)):
#0 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0
#2 0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0
#3 0x0000000000400725 in threadA_proc ()
#4 0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0
#5 0x00000037206f4bfd in clone () from /lib64/libc.so.6
Thread 2 (Thread 0x7f60a5709700 (LWP 87748)):
#0 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0
#2 0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0
#3 0x0000000000400792 in threadB_proc ()
#4 0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0
#5 0x00000037206f4bfd in clone () from /lib64/libc.so.6
Thread 1 (Thread 0x7f60a610c700 (LWP 87746)):
#0 0x0000003720e080e5 in pthread_join () from /lib64/libpthread.so.0
#1 0x0000000000400806 in main ()

可以看到,Thread 2 和 Thread 3 一直阻塞获取锁(pthread_mutex_lock)的过程,而且 pstack 多次输出信息都没有变化,那么可能大概率发生了死锁。

但是,还不能够确认这两个线程是在互相等待对方的锁的释放,因为我们看不到它们是等在哪个锁对象,于是我们可以使用 gdb 工具进一步确认。

整个 gdb 调试过程,如下:

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
swift复制代码// gdb 命令
$ gdb -p 87746

// 打印所有的线程信息
(gdb) info thread
3 Thread 0x7f60a610a700 (LWP 87747) 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0
2 Thread 0x7f60a5709700 (LWP 87748) 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0
* 1 Thread 0x7f60a610c700 (LWP 87746) 0x0000003720e080e5 in pthread_join () from /lib64/libpthread.so.0
//最左边的 * 表示 gdb 锁定的线程,切换到第二个线程去查看

// 切换到第2个线程
(gdb) thread 2
[Switching to thread 2 (Thread 0x7f60a5709700 (LWP 87748))]#0 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0

// bt 可以打印函数堆栈,却无法看到函数参数,跟 pstack 命令一样
(gdb) bt
#0 0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0
#2 0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0
#3 0x0000000000400792 in threadB_proc (data=0x0) at dead_lock.c:25
#4 0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0
#5 0x00000037206f4bfd in clone () from /lib64/libc.so.6

// 打印第三帧信息,每次函数调用都会有压栈的过程,而 frame 则记录栈中的帧信息
(gdb) frame 3
#3 0x0000000000400792 in threadB_proc (data=0x0) at dead_lock.c:25
27 printf("thread B waiting get ResourceA \n");
28 pthread_mutex_lock(&mutex_A);

// 打印mutex_A的值 , __owner表示gdb中标示线程的值,即LWP
(gdb) p mutex_A
$1 = {__data = {__lock = 2, __count = 0, __owner = 87747, __nusers = 1, __kind = 0, __spins = 0, __list = {__prev = 0x0, __next = 0x0}},
__size = "\002\000\000\000\000\000\000\000\303V\001\000\001", '\000' <repeats 26 times>, __align = 2}

// 打印mutex_B的值 , __owner表示gdb中标示线程的值,即LWP
(gdb) p mutex_B
$2 = {__data = {__lock = 2, __count = 0, __owner = 87748, __nusers = 1, __kind = 0, __spins = 0, __list = {__prev = 0x0, __next = 0x0}},
__size = "\002\000\000\000\000\000\000\000\304V\001\000\001", '\000' <repeats 26 times>, __align = 2}

我来解释下,上面的调试过程:

  • 通过 info thread 打印了所有的线程信息,可以看到有 3 个 线程,一个是主线程(LWP 87746),另外两个都是我们自己创建的线程(LWP 87747 和 87748);
  • 通过 thread 2,将切换到第2个线程(LWP 87748);
  • 通过 bt,打印线程的调用栈信息,可以看到有 threadB_proc 函数,说明这个是线程B函数,也就说 LWP 87748 是线程 B;
  • 通过 frame 3,打印调用栈中的第三个帧的信息,可以看到线程 B 函数,在获取互斥锁 A 的时候阻塞了;
  • 通过 p mutex_A,打印互斥锁 A 对象信息,可以看到它被 LWP 为 87747(线程 A) 的线程持有者;
  • 通过 p mutex_B,打印互斥锁 A 对象信息,可以看到他被 LWP 为 87748 (线程 B) 的线程持有者;

因为线程 B 在等待线程 A 所持有的 mutex_A, 而同时线程 A 又在等待线程 B 所拥有的mutex_B, 所以可以断定该程序发生了死锁

04 避免死锁问题的发生

前面我们提到,产生死锁的四个必要条件是:互斥条件、持有并等待条件、不可剥夺条件、环路等待条件。

那么避免死锁问题就只需要破环其中一个条件就可以,最常见的并且可行的就是使用资源有序分配法,来破环环路等待条件。

那什么是资源有序分配法呢?

线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。

我们使用资源有序分配法的方式来修改前面发生死锁的代码,我们可以不改动线程 A 的代码。

我们先要清楚线程 A 获取资源的顺序,它是先获取互斥锁 A,然后获取互斥锁 B。

所以我们只需将线程 B 改成以相同顺序地获取资源,就可以打破死锁了。

线程 B 函数改进后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码//线程 B 函数,同线程 A 一样,先获取互斥锁 A,然后获取互斥锁 B
void *threadB_proc(void *data)
{
printf("thread B waiting get ResourceA \n");
pthread_mutex_lock(&mutex_A);
printf("thread B got ResourceA \n");

sleep(1);

printf("thread B waiting get ResourceB \n");
pthread_mutex_lock(&mutex_B);
printf("thread B got ResourceB \n");

pthread_mutex_unlock(&mutex_B);
pthread_mutex_unlock(&mutex_A);
return (void *)0;
}

执行结果如下,可以看,没有发生死锁。

1
2
3
4
5
6
7
8
9
arduino复制代码thread B waiting get ResourceA 
thread B got ResourceA
thread A waiting get ResourceA
thread B waiting get ResourceB
thread B got ResourceB
thread A got ResourceA
thread A waiting get ResourceB
thread A got ResourceB
exit

总结

简单来说,死锁问题的产生是由两个或者以上线程并行执行的时候,争夺资源而互相等待造成的。

死锁只有同时满足互斥、持有并等待、不可剥夺、环路等待这四个条件的时候才会发生。

所以要避免死锁问题,就是要破坏其中一个条件即可,最常用的方法就是使用资源有序分配法来破坏环路等待条件。

本文转载自: 掘金

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

1…669670671…956

开发者博客

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