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

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


  • 首页

  • 归档

  • 搜索

别找了,这是 Pandas 最详细教程了

发表于 2020-09-07

本文来源机器之心,原文链接:https://towardsdatascience.com/be-a-more-efficient-data-scientist-today-master-pandas-with-this-guide-ea362d27386,如有侵权,则可删除。

Python 是开源的,它很棒,但是也无法避免开源的一些固有问题:很多包都在做(或者在尝试做)同样的事情。如果你是 Python 新手,那么你很难知道某个特定任务的最佳包是哪个,你需要有经验的人告诉你。有一个用于数据科学的包绝对是必需的,它就是 pandas。

pandas 最有趣的地方在于里面隐藏了很多包。它是一个核心包,里面有很多其他包的功能。这点很棒,因为你只需要使用 pandas 就可以完成工作。

pandas 相当于 python 中 excel:它使用表(也就是 dataframe),能在数据上做各种变换,但还有其他很多功能。

如果你早已熟知 python 的使用,可以直接跳到第三段。

让我们开始吧:

1
javascript复制代码import pandas as pd

别问为什么是「pd」而不是「p」,就是这样。用就行了:)

pandas 最基本的功能

读取数据

1
2
ini复制代码data = pd.read_csv( my_file.csv )
data = pd.read_csv( my_file.csv , sep= ; , encoding= latin-1 , nrows=1000, skiprows=[2,5])

sep 代表的是分隔符。如果你在使用法语数据,excel 中 csv 分隔符是「;」,因此你需要显式地指定它。编码设置为 latin-1 来读取法语字符。nrows=1000 表示读取前 1000 行数据。skiprows=[2,5] 表示你在读取文件的时候会移除第 2 行和第 5 行。

  • 最常用的功能:read_csv, read_excel
  • 其他一些很棒的功能:read_clipboard, read_sql

写数据

1
ini复制代码data.to_csv( my_new_file.csv , index=None)

index=None 表示将会以数据本来的样子写入。如果没有写 index=None,你会多出一个第一列,内容是 1,2,3,…,一直到最后一行。

我通常不会去使用其他的函数,像.to_excel, .to_json, .to_pickle 等等,因为.to_csv 就能很好地完成工作,并且 csv 是最常用的表格保存方式。

检查数据

1
arduino复制代码Gives (#rows, #columns)

给出行数和列数

1
kotlin复制代码data.describe()

计算基本的统计数据

查看数据

1
kotlin复制代码data.head(3)

打印出数据的前 3 行。与之类似,.tail() 对应的是数据的最后一行。

1
css复制代码data.loc[8]

打印出第八行

1
css复制代码data.loc[8,  column_1 ]

打印第八行名为「column_1」的列

1
css复制代码data.loc[range(4,6)]

第四到第六行(左闭右开)的数据子集

pandas 的基本函数

逻辑运算

1
2
3
kotlin复制代码data[data[ column_1 ]== french ]
data[(data[ column_1 ]== french ) & (data[ year_born ]==1990)]
data[(data[ column_1 ]== french ) & (data[ year_born ]==1990) & ~(data[ city ]== London )]

通过逻辑运算来取数据子集。要使用 & (AND)、 ~ (NOT) 和 | (OR),必须在逻辑运算前后加上「and」。

1
css复制代码data[data[ column_1 ].isin([ french ,  english ])]

除了可以在同一列使用多个 OR,你还可以使用.isin() 函数。

基本绘图

matplotlib 包使得这项功能成为可能。正如我们在介绍中所说,它可以直接在 pandas 中使用。

1
scss复制代码data[ column_numerical ].plot()

().plot() 输出的示例

1
scss复制代码data[ column_numerical ].hist()

画出数据分布(直方图)

.hist() 输出的示例

1
scala复制代码%matplotlib inline

如果你在使用 Jupyter,不要忘记在画图之前加上以上代码。

更新数据

1
2
css复制代码data.loc[8,  column_1 ] =  english
将第八行名为 column_1 的列替换为「english」
1
kotlin复制代码data.loc[data[ column_1 ]== french ,  column_1 ] =  French

在一行代码中改变多列的值

好了,现在你可以做一些在 excel 中可以轻松访问的事情了。下面让我们深入研究 excel 中无法实现的一些令人惊奇的操作吧。

中级函数

统计出现的次数

1
scss复制代码data[ column_1 ].value_counts()

.value_counts() 函数输出示例

在所有的行、列或者全数据上进行操作

1
go复制代码data[ column_1 ].map(len)

len() 函数被应用在了「column_1」列中的每一个元素上

.map() 运算给一列中的每一个元素应用一个函数

1
python复制代码data[ column_1 ].map(len).map(lambda x: x/100).plot()

pandas 的一个很好的功能就是链式方法(tomaugspurger.github.io/method-chai… 和.plot())。

1
arduino复制代码data.apply(sum)

.apply() 会给一个列应用一个函数。

.applymap() 会给表 (DataFrame) 中的所有单元应用一个函数。

tqdm, 唯一的

在处理大规模数据集时,pandas 会花费一些时间来进行.map()、.apply()、.applymap() 等操作。tqdm 是一个可以用来帮助预测这些操作的执行何时完成的包(是的,我说谎了,我之前说我们只会使用到 pandas)。

1
2
csharp复制代码from tqdm import tqdm_notebook
tqdm_notebook().pandas()

用 pandas 设置 tqdm

1
less复制代码data[ column_1 ].progress_map(lambda x: x.count( e ))

用 .progress_map() 代替.map()、.apply() 和.applymap() 也是类似的。

在 Jupyter 中使用 tqdm 和 pandas 得到的进度条

相关性和散射矩阵

1
2
scss复制代码data.corr()
data.corr().applymap(lambda x: int(x*100)/100)

.corr() 会给出相关性矩阵

1
ini复制代码pd.plotting.scatter_matrix(data, figsize=(12,8))

散点矩阵的例子。它在同一幅图中画出了两列的所有组合。

pandas 中的高级操作

The SQL 关联

在 pandas 中实现关联是非常非常简单的

1
ini复制代码data.merge(other_data, on=[ column_1 ,  column_2 ,  column_3 ])

关联三列只需要一行代码

分组

一开始并不是那么简单,你首先需要掌握语法,然后你会发现你一直在使用这个功能。

1
scss复制代码data.groupby( column_1 )[ column_2 ].apply(sum).reset_index()

按一个列分组,选择另一个列来执行一个函数。.reset_index() 会将数据重构成一个表。

正如前面解释过的,为了优化代码,在一行中将你的函数连接起来。

行迭代

1
2
3
4
ini复制代码dictionary = {}

for i,row in data.iterrows():
dictionary[row[ column_1 ]] = row[ column_2 ]

.iterrows() 使用两个变量一起循环:行索引和行的数据 (上面的 i 和 row)

总而言之,pandas 是 python 成为出色的编程语言的原因之一

我本可以展示更多有趣的 pandas 功能,但是已经写出来的这些足以让人理解为何数据科学家离不开 pandas。总结一下,pandas 有以下优点:

  • 易用,将所有复杂、抽象的计算都隐藏在背后了;
  • 直观;
  • 快速,即使不是最快的也是非常快的。

它有助于数据科学家快速读取和理解数据,提高其工作效率

本文转载自: 掘金

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

如何基于 K8s 构建下一代 DevOps 平台? 什么是

发表于 2020-09-07

1.png

作者 | 孙健波(天元)

导读:当前云原生 DevOps 体系现状如何?面临哪些挑战?如何通过 OAM 解决云原生 DevOps 场景下的诸多问题?云原生开发应用模型 OAM(Open Application Model) 社区核心成员孙健波将为大家一一解答,并分享如何基于 OAM 和 Kubernetes 打造无限能力的下一代 DevOps 平台。

什么是 DevOps?为什么基于 Kubernetes 构建?

2009 年举办了第一届 DevOpsDays 大会,DevOps 名字被首次提出。到 2010 年,DevOps 的概念越来越火,出了 What is DevOps 的文章,讲解了 DevOps 的概念,方法论及配套的工具。简单来说,研发工程师需要和运维工程师深度的合作,同时通过一系列工具保证研发更加顺畅,从而更容易的接触生产环境。

到 2013 年,Docker 出现了,工程师可以第一次到软件生产环境中定义,通过 Docker image 完成单机软件的交付和分发。此时 DevOps 开始慢慢落地。2015 年开始,DevOps 相关的工具越来越多,资源利用率出现了一些问题,CNCF 的成立使得 DevOps 的实践往 Kubernetes 上走。

2.png
(DevOps 的三个阶段)

阿里在 Kubernetes 上的实践也取得了非常好的成果。在规模方面,阿里内部集成了数十个节点可以达到上万的集群,同时具备高性能和安全特性,秒级扩容,神龙+安全容器。具备极致的弹性,分钟级拆解公有云计算资源,无限资源池。另一方面,Kubernetes 社区已经具备非常丰富的 DevOps 生态基础功能,包括镜像托管、CI\CD 流水线、任务编排、发布策略、镜像打包、分发、丰富的应用运行时的负载支撑、丰富弹性和应用扩容能力。

为什么阿里基于 Kubernetes 构建 DevOps平台?

1)阿里基于 Kubernetes 的无限资源池与基础设施能力

  • 大规模 – 单集群最高可达 10000 节点、百万 Pod
  • 高性能 – 秒级扩容,智能伸缩,神龙 + 安全容器
  • 极致弹性 – 分钟级拆解公有云计算资源,无限资源池

2)社区围绕 Kubernetes 已经具备丰富的 DevOps 生态基础功能

  • 源码到容器镜像仓库,Kubernetes 是容器平台事实标准:Github / DockerHub;
  • CI/CD 流水线、任务编排、发布策略:Argo / Teckton / Spinnaker / Jenkins-X / Flagger;
  • 镜像打包、分发:Helm / CNAB;
  • 丰富的应用运行负载支撑:Deployment(无状态) / StatefulSet(有状态) / OpenKruise(原生有状态增强);
  • 丰富的弹性和应用扩缩容能力:HPA / KEDA。

基于 Kubernetes 的 DevOps 平台新挑战

下图展示了一个云原生下的 DevOps 流水线的典型流程。首先是代码的开发,代码托管到 Github,再接入单元测试的工具 Jenkins,此时基本研发已完成。再接着到镜像的构建,涉及到配置、编排等。云原生中可以用 HELM 打包应用。打包好的应用部署到各个环境中。但整个过程中会面临很多挑战。首先,在不同的环境需要不同的运维能力。

3.png
(一个云原生 DevOps 流水线的典型流程)

其次,配置的过程中要创建云上数据库,需要另外打开一个控制台来创建数据库。还需要配置负载均衡。在应用启动以后还需要配置额外的功能,包括日志、策略、安全防护等等。可以发现,云资源和 DevOps 平台体验是割裂的,里面充斥着借助外部平台创建的过程。这对新手来说是非常痛苦的。

挑战一:云资源与 DevOps 平台体验割裂

DevOps 流程中充斥着大量需要外部平台创建的过程:

4.png

挑战二:研发、运维、基础设施关注点耦合

下图是常用的 K8s 的 YAML 配置文件,大家经常吐槽这个配置文件很复杂。

简单来说 YAML 配置文件可以分为三大块,一块是运维比较关心的配置,包括实例数,策略和发布。第二块是研发关心的,涉及到镜像、端口号等。第三块是基础设施工程师看得懂的,如调度策略等。K8s 的配置文件中将方方面面的信息都耦合在一起,这对 K8s 工程师来说是非常适合的,但是对应用侧的终端工程师而言,有很多不需要关心的配置指标。

5.png

  • DevOps 流程中缺乏对“应用”这个概念的描述
  • K8s 的 YAML 文件的定位并不是终端用户

挑战三:平台的自定义封装,简单却能力不足

DevOps 平台对 K8s 能力封装抽象,只剩下 5 个 Deployment 的字段需要研发填写。从用户角度而言,这种设置非常好用简单。但是针对稍微复杂的应用,涉及到应用状态管理,健康检查等等一系列的操作,此时这 5 个字段是不够的。

6.png

挑战四:CRD 扩展能力强大,DevOps 平台无法直接复用

CRD(Customize Resource Definition) 扩展能力强大,几乎所有软件都可以通过 CRD 的方式进行扩展,包括数据库、存储、安全、编排、依赖管理、发布等。但是对 DevOps 平台来说,上面接口并没有向用户暴露,导致无法直接复用。

7.png

挑战五:DevOps 平台开发的新能力使用门槛高

如果平台想要扩展一些能力,而原生的自动扩缩容能力不太合适,希望开发定时的扩缩容YAML文件,随着业务情况而设置。但此时用户使用YAML的门槛非常高,不清楚如何使用YAML。随着新能力开发越来越多,能力之间会出现冲突,这也非常难以管理。

8.png

  • 运维同学怎么知道这个扩展能力怎么用?看 CRD?看配置文件?看 …… 文档?
  • 扩展能力间出现冲突,导致线上故障,比如:CronHPA 和 默认 HPA 被同时安装给了同一个应用;K8s 扩展能力之间的冲突关系,如何有效管理?如何有效的对运维透出?

挑战六:不同 DevOps 平台需要完全重新对接

很多云原生实践中会遇到的问题,即需要定义非常复杂的 YAML,这种方式可以解决企业内部所有问题,但是挑战在于很难与生态进行对接。如 RDS,SLB 的能力都嵌到 YAML 文件中,无法复用,几乎不具备原子化能力。同时无法协作,无法提供给兄弟部门或生态使用,只能给内部封闭生态使用。上层系统不同应用对接 DevOps 平台时,需要写不同格式的 YAML,这也是非常痛苦的。

9.png

  • 难以理解,必须通过界面可视化透出
  • 无法复用,几乎不具备原子化能力
  • 无法协作,只能内部封闭生态使用

OAM 应用模型的技术原理

OAM 应用模型的出现,解决了上述应用管理的难题,下面我们来介绍一下 OAM 模型的技术原理。

  1. Component 组件

OAM 中常见的概念是 Component 组件,完全从研发角度定义的待部署单元。下图右侧是 YAML 中 Component 的例子,其中黄色部分可以灵活自定义。OAM 中会定义标准的架构 ContaineriseWorkload,表示工作负载部分,里面是待部署单元的具体描述。这时就可以解决关注点分离的问题,帮助应用侧工程师去掉很多细节,只需要关心开发需要关注的端口号,镜像等等。

10.png

应对挑战一,在 OAM 中可以定义数据库表达资源需要使用云资源,Workload 中可以根据自己的需要定义不同的组件,包括基于虚拟机的应用、或者老的 Function 应用。组件是应用开发者关心的。

11.png

  1. Trait

如果只是组件,组合起来就可以构建简单的应用。如果关心应用运维的问题,OAM 中有 Trait 的概念,指的是在原来组件的基础上附加一些特征。特征指的是运维的能力,如手动扩缩容能力、外部访问能力、发布、负载均衡。弹性扩缩容、基于流量的管理等等。通过 OAM 的 Trait 可以很灵活的得到插件化扩充能力。不同的 component 绑定不同的特征。

12.png

  1. Scope

Component,Trait 以及所有组装起来的 Application Configuration 就是 OAM 中的三种主要的概念。但当多个组件共同协作时应该如何处理?OAM 中有个边界 Scope 的概念,是一种特殊的 Trait,将多个 Component 组合在一起,共享一组资源组,CPU 等特征用 Scope 表示,拓展多个组件的共同特征。

13.png

OAM 加持下的下一代 DevOps 技术

  1. OAM:以应用为中心的分层模型

OAM 是以应用为中心的分层模型,首先需要运行在服务端的 OAM 解释器,对于 YAML 的读取需要通过 OAM 解释器。OAM 提供 Trait,Component 让用户填写,编成 APP Config。APP Config 通过 OAM 解释器具备 Deployment,Ingress,HPA 或者云资源等能力。这种方法可以将研发、运维基于基础设施进行分层,研发关心 Component,运维关心 Trait,基础设施通过 OAM 解释器提供各种能力,与 K8s 紧密结合,对其应用概念做了补充。

14.png

  • 分层
  • 模块化
  • 可复用
  1. 快速的纳入 K8s 生态已有 Operater 能力

OAM 可以快速的纳入 K8s 生态已有的 Operater 能力,下图左边的 Component 中是一个 CRD 的实例,右边是 Trait 中的 CRD 的实例,中间表示 Component 底下的 Workload 和 Trait 分别对应了 K8s 自定义资源的能力。如果想要使用 K8s 中的某些能力,只需要在 Trait 中写入相应的字段即可。

15.png

  1. OAM 框架解决组件依赖关系和启动顺序

OAM 框架解决组件依赖关系和启动顺序。OAM Runtime,OAM 解释器会将组件依赖关系和启动顺序处理好,下图中 Component 之间有 dependency 关系,Trait 与 Component 之间有 preComponent 或者 postComponent 等关系。

16.png

  1. OAM Trait 灵活解决资源绑定难题

启动顺序厘清之后涉及到资源绑定问题,一边是使用的数据库,另一边是 Web 的程序,Web 的程序绑定数据库连接串资源。在 OAM 中只需要写一个 Trait 就可以解决资源绑定问题,下图右边,K8s 通过 Secret 承载连接串信息,Service Binding Trait 对应一个运行的 Operator,Web Hook 拿到 Secret 后注入进数据库中。

17.png

  1. Workload 与 Trait 交互机制

大家会考虑接入 OAM 会不会比较麻烦,需不需要改代码。OAM 设计了 Workload 与 Trait 交互机制,OAM 内部零改造,只需要扩展 Workload 和 Trait。首先,Component 中创建 Workload 实例,再创建 Trait 实例,只需要在 Trait 中查看 Workload 的 Definition,从而配置 Trait 中需要的能力。

18.png
(OAM 内核零改造,插件式快速接入新能力)

如果开发了新的能力,碰到冲突问题也是非常头痛的。在 OAM 框架中定义 Trait 时,可以检查哪些字段是冲突的,拒绝掉新的应用的创建,从而保障 Trait 之间的兼容性,使得运维问题可发现、可管理。

19.png
(可发现、可管理的 Traits 系统)

  1. OAM:无限能力的 DevOps 平台体系

下图是 DevOps 平台体系,最下层是 OAM Runtime,一部分是 Workload,对应运行时的承载的 Runtime,如 Function、Container、虚拟机、Serverless Service 等。另一部分是 Trait,对应运维能力,如发布、弹性扩缩容、日志、安全等等。再上一层可以根据场景化组合(Application Profile)组装成不同的业务形态平台,不同平台可以使用不同组合的 Workload 和 Trait,具备不同的能力。通过 OAM 标准化的模型构建无限能力的 DevOps 平台,满足各种场景的需要。

20.png

在用户侧,OAM 加持下的研发 DevOps 流程在镜像构建完成之后使用达到统一,OAM 提供了 APP Config,包含不同的 Component,每个 Component 包含不同的运维能力 Trait,支持不同的环境,如测试环境、生成环境。OAM 配置统一,适合不同的云,可以拿到不同的集群中直接运行。在 K8s 侧,用户只需要装上插件,就可以很方便的嵌入很多丰富的能力。

21.png

如果有其他问题,建议大家加入我们的钉钉群进行讨论。(钉钉搜索群号:23310022,即可进群)

课程推荐

去年,CNCF 与 阿里云联合发布了《云原生技术公开课》已经成为了 Kubernetes 开发者的一门“必修课”。今天,阿里云再次集结多位具有丰富云原生实践经验的技术专家,正式推出《云原生技术实践公开课》。课程内容由浅入深,专注讲解“ 落地实践”。还为学习者打造了真实、可操作的实验场景,方便验证学习成果,也为之后的实践应用打下坚实基础。

点击链接即可免费观看:developer.aliyun.com/learning/ro…

“阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”

本文转载自: 掘金

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

真小白-部署Jenkins教程 背景 Jenkins是什么?

发表于 2020-09-07

背景

公司的前端、后端构建及部署工作都是人工去做,随着业务扩大,项目迭代速度变快,人员增多,各种问题都暴露出来,将通过一个简单案例分享一下基于Jenkins的前后端自动化工作流搭建的过程,搭建完这套工作流,我们只需要在本地发起一个git提交,剩下的单打包构建,代码部署,邮件提醒等功能全部自动化完成,让持续集成、持续交付、持续部署变得简单易操作,真正解决人工构建部署的诸多问题。

Jenkins是什么?

Jenkins 是一款业界流行的开源持续集成工具,广泛用于项目开发,具有自动化构建、测试和部署等功能。

War包安装

  1. 官网下载Jenkins War包
  2. 命令启动,java -jar jenkins.war --httpPort=8080
    可以用-server -Xms1024m -Xmx2048m -XX:PermSize=256m -XX:MaxPermSize=512m限制Jenkins占用内存
  3. 浏览器打开http://127.0.0.1:8080/
  4. 找到初始化密码
  5. 选择默认插件
  6. 等待下载完成后
  7. 设置登陆密码

下载插件

Manage Jenkins->Manage Plugins->高级(最下面)

增加 下载速度 替换默认Url

http://mirror.esuni.jp/jenkins/updates/update-center.json

Manage Jenkins->Manage Plugins->可选插件

安装以下插件

  1. Publish Over SSH 用于连接远程服务器
  2. Deploy to container 插件用于把打包的应用发布到远程服务器
  3. Maven Integration plugin
  4. Pipeline Maven Integration Plugin
  5. Gitlab Hook Plugin(git提交的时候自动编译)
  6. GitLab Plugin(git提交的时候自动编译)
  7. GitLab Authentication Plugin (git提交的时候自动编译)
  8. WebHook (git提交的时候自动编译)
  9. Locale 下载完成后去全局配置里面 Locale->设置zh-CN
  10. Dingding[钉钉] Plugin 钉钉Jenkins通知器
  11. Multiple SCMs(配置多个git)
  12. Git Parameter Plug-In (参数化配置)

全局配置

Manage Jenkins->Global Tool Configuration

Publish over SSH (配置远程推送的服务器可以多个)

新建maven 项目

点击新建Item

备注:如果有父子关系的程序,父工程先install到仓库里面。
或者下载插件:Multiple SCMs(配置多个git)
参考地址:Jenkins配置教程

强制指定自定义maven 仓库
job–>configure–>Build–>Goals and options:
clean package -Dmaven.repo.local=D:\dev\maven3.1.1\m2repository

如果要提交代码后自动化构建
需要配置:(需要权限)


配置成功后

点击test可以测试自动部署

Jenkins参数化配置(选择分支部署)

1、下载插件Git Parameter

2、配置参数属性

3、最终效果

Jenkins配置钉钉通知

1、配置钉钉机器人

2、安装钉钉通知插件

3、配置项目构建后通知

4、配置Jenkins发送消息给钉钉

5、最终结果

Jenkins权限管理

安装插件Role-based Authorization Strategy
可以访问:Jenkins权限管理参考

Jenkins服务器跨服务免密执行shell脚本

场景:很多情况下Jenkins都是单独服务器部署,发送已经打包好的服务包给目标服务器启动。不可避免有在定制化操作:

例如:发送目标服务器前,先备份当前服务器运行的服务包,就需要定制化的shell脚本去执行这个操作。每次输入密码肯定是不靠谱的。

所以需要Jenkins服务器免密登陆到目标服务器的配置。

参考:
【小白】linux 免密码登陆

附录

docker部署不建议使用,推荐使用war启动Jenkins

下载镜像

1
docker复制代码docker pull jenkins/jenkins:lts

安装镜像

1
docker复制代码docker run --name jenkins --user=root -p 8080:8080 -p 50000:50000 -v /Users/wei/Documents/jenkins_home:/var/jenkins_home -d jenkins/jenkins:lts

卸载容器

1
docker复制代码docker rm -f $(docker ps -a |  grep "jenkins"  | awk '{print $1}')

参考

Jenkins中文文档

Jenkins部署Nodejs教程

持续集成 之 Jenkins插件 Multijob plugin

本文转载自: 掘金

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

如何优雅的转换Bean对象

发表于 2020-09-07

背景

我们的故事要从一个风和日丽的下午开始说起!

这天,外包韩在位置上写代码~外包韩根据如下定义

  • **PO(persistant object):**持久化对象,可以看成是与数据库中的表相映射的 java 对象。最简单的 PO 就是对应数据库中某个表中的一条记录。
  • **VO(view object):**视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。
  • **BO(business object):**业务对象,主要作用是把业务逻辑封装为一个对象。这个对象可以包括一个或多个其它的对象。
  • DTO、DO(省略……)

将Bean进行逐一分类!例如一个car_tb的表,于是他有了两个类,一个叫CarPo,里头属性和表字段完全一致。另一个叫CarVo,用于页面上的Car显示!
但是外包韩在做CarPo到CarVo转换的时候,代码是这么写的,伪代码如下:

1
2
3
4
5
6
ini复制代码CarPo carPo = this.carDao.selectById(1L);
CarVo carVo = new CarVo();
carVo.setId(carPo.getId());
carVo.setName(carPo.getName());
//省略一堆
return carVo;

*画外音:*看到这一串代码是不是特别亲切,我接手过一堆外包留下的代码,就是这么写的,一坨屎山!一类几千行,一半都在set属性。

恰巧,阿雄打水路过!鸡贼的阿雄瞄了一眼外包韩的屏幕,看到外包韩的这一系列代码!上去进行一顿教育,觉得不够优雅!阿雄觉得,应该用BeanUtils.copyProperties来简化书写,像下面这样!

1
2
3
4
ini复制代码CarPo carPo = this.carDao.selectById(1L);
CarVo carVo = new CarVo();
BeanUtils.copyProperties(carPo, carVo);
return carVo;

可是,外包韩盯着这段代码,说道:”网上不是说反射效率慢,你这么写,没有性能问题么?”
阿雄说道:” 如果是用Apache的BeanUtil类,确实有很大的性能问题,像阿里巴巴的代码扫描插件,都禁止用该类,如下所示!”

“但是,如果采用的是像Spring的BeanUtils类,要在调用次数足够多的时候,你才能明显的感受到卡顿。”阿雄补充道。

“哇,阿雄真棒!”外包韩兴奋不已!

看着这办公室基情满满的氛围。一旁正在拖地的清洁工——扫地烟,他决定不再沉默。

只见扫地烟扔掉手中的拖把,得瑟的说道”我们不考虑性能。从拓展性角度看看!BeanUtils还是有很多问题的!”

  • 复制对象时字段类型不一致,导致赋值不上,你怎么解决?自己拓展?
  • 复制对象时字段名称不一致,例如CarPo里叫carName,CarVo里叫name,导致赋值不上,你怎么解决?自己拓展?
  • 如果是集合类的复制,例如List转换为List,你怎么处理?
    (省略一万字….)

“那应该怎么办呢?”听了扫地烟的描述,外包韩疑惑的问道!

“很简单,其实我们在转换bean的过程中,set这些逻辑是固定的,唯一变化的就是转换规则。因此,如果我们只需要书写转换规则,转换代码由系统根据规则自动生成,就方便很多了!还是用上面的例子,CarPo里叫carName,CarVo里叫name,属性名称不一致。我们就通过一个注解

1
ini复制代码@Mapping(source = "carName", target = "name"),

指定对应转换规则。系统识别到这个注解,就会生成代码

1
less复制代码carVo.setName(carPo.getCarName())

如果能以这样的方式,set代码由系统自动生成,那么在bean转换逻辑方面,灵活性将大大加强,而且这种方式不存在性能问题!”扫地烟补充道!

“那这些set逻辑,由什么工具来生成呢?”外包韩和阿雄一起问道!

“工具的名字叫MapStruct!”

ok,上面的故事到了这里,就结束了!不需要问结局,结局只有一个,外包韩和阿雄幸福美满的…(省略10000字)…
那么我们开始具体来说一说MapStruct!

MapStruct的教程

这里从用法、原理、优势三个角度来介绍一下这个插件,至于详细教程,还是看官方文档吧。

用法

引入pom文件如下

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<dependency>
<groupId>org.mapstruct</groupId>
<!-- jdk8以下就使用mapstruct -->
<artifactId>mapstruct-jdk8</artifactId>
<version>1.2.0.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.2.0.Final</version>
</dependency>

在准备两个实体类,为了方便演示,用了lombok插件。
准备两个实体类,一个是CarPo

1
2
3
4
5
6
7
8
less复制代码@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CarPo {
private Integer id;
private String brand;
}

还有一个是CarVo

1
2
3
4
5
6
7
8
less复制代码@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CarVo {
private Integer id;
private String brand;
}

再来一个转换接口

1
2
3
4
5
6
7
java复制代码@Mapper
public interface CarCovertBasic {
CarCovertBasic INSTANCE =
Mappers.getMapper(CarCovertBasic.class);

CarVo toConvertVo(CarPo source);
}

测试代码如下:

1
2
3
4
5
6
ini复制代码//实际中从数据库取
CarPo carPo = CarPo.builder().id(1)
.brand("BMW")
.build();
CarVo carVo = CarCovertBasic.INSTANCE.toConvertVo(carPo);
System.out.println(carVo);

输出如下

1
ini复制代码CarVo(id=1, brand=BMW)

可以看到,carPo的属性值复制给了carVo。当然,在这种情况下,功能和BeanUtils是差不多的,体现不出优势!嗯,我们放在后面说,我们先来说说原理!

原理

其实原理就是MapStruct插件会识别我们的接口,生成一个实现类,在实现类中,为我们实现了set逻辑!
例如,上面的例子中,给CarCovertBasic接口,实现了一个实现类CarCovertBasicImpl,我们可以用反编译工具看到源码如下图所示

下面,我们来说说优势

优势

(1)两个类型属性不一致

此时CarPo的一个属性为carName,而CarVo对应的属性为name!

我们在接口上增加对应关系即可,如下所示

1
2
3
4
5
6
7
kotlin复制代码@Mapper
public interface CarCovertBasic {
CarCovertBasic INSTANCE = Mappers.getMapper(CarCovertBasic.class);

@Mapping(source = "carName", target = "name")
CarVo toConvertVo(CarPo source);
}

测试代码如下

1
2
3
4
5
6
ini复制代码CarPo carPo = CarPo.builder().id(1)
.brand("BMW")
.carName("宝马")
.build();
CarVo carVo = CarCovertBasic.INSTANCE.toConvertVo(carPo);
System.out.println(carVo);

输出如下

1
ini复制代码CarVo(id=1, brand=BMW, name=宝马)

可以看到carVo已经能识别到carPo中的carName属性,并赋值成功。反编译的图如下

画外音:如果有多个映射关系可以用@Mappings注解,嵌套多个@Mapping注解实现,后文说明!

(2)集合类型转换

如果我们要从List转换为List怎么办呢?
简单,接口里加一个方法就行

1
2
3
4
5
6
7
8
9
ini复制代码@Mapper
public interface CarCovertBasic {
CarCovertBasic INSTANCE = Mappers.getMapper(CarCovertBasic.class);

@Mapping(source = "carName", target = "name")
CarVo toConvertVo(CarPo source);

List<CarVo> toConvertVos(List<CarPo> source);
}

如代码所示,我们增加了一个toConvertVos方法即可,mapStruct生成代码的时候,会帮我们去循环调用toConvertVo方法,给大家看一下反编译的代码,就一目了然

(3)类型不一致

在CarPo加一个属性为Date类型的createTime,而在CarVo加一个属性为String类型的createTime,那么代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
less复制代码@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CarPo {
private Integer id;
private String brand;
private String carName;
private Date createTime;
}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CarVo {
private Integer id;
private String brand;
private String name;
private String createTime;
}

接口就可以这么写

1
2
3
4
5
6
7
8
9
10
11
less复制代码@Mapper
public interface CarCovertBasic {
CarCovertBasic INSTANCE = Mappers.getMapper(CarCovertBasic.class);
@Mappings({
@Mapping(source = "carName", target = "name"),
@Mapping(target = "createTime", expression = "java(com.guduyan.util.DateUtil.dateToStr(source.getCreateTime()))")
})
CarVo toConvertVo(CarPo source);

List<CarVo> toConvertVos(List<CarPo> source);
}

这样在代码中,就能解决类型不一致的问题!在生成set方法的时候,自动调用DateUtil类进行转换,由于比较简单,我就不贴反编译的图了!

(4)多对一

在实际业务情况中,我们有时候会遇到将两个Bean映射为一个Bean的情况,假设我们此时还有一个类为AtrributePo,我们要将CarPo和AttributePo同时映射为CarBo,我们可以这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
less复制代码@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AttributePo {
private double price;
private String color;
}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CarBo {
private Integer id;
private String brand;
private String carName;
private Date createTime;
private double price;
private String color;
}

接口改变如下

1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码@Mapper
public interface CarCovertBasic {
CarCovertBasic INSTANCE = Mappers.getMapper(CarCovertBasic.class);
@Mappings({
@Mapping(source = "carName", target = "name"),
@Mapping(target = "createTime", expression = "java(com.guduyan.util.DateUtil.dateToStr(source.getCreateTime()))")
})
CarVo toConvertVo(CarPo source);

List<CarVo> toConvertVos(List<CarPo> source);

CarBo toConvertBo(CarPo source1, AttributePo source2);
}

直接增加接口即可,插件在生成代码的时候,会帮我们自动组装,看看下面的反编译代码就一目了然。

(5)其他

关于MapStruct还有其他很多的高级功能,我就不一一介绍了。大家可以参考下面的文档,在用到的时候自行翻阅即可!
文档地址:mapstruct.org/documentati…

总结

本文介绍了,在项目里如何优雅的转换Bean,希望大家有所收获!
还想听到其他关于阿雄的故事么,请记得关注”孤独烟!”

本文转载自: 掘金

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

震惊!有人用Python写了个自动亏钱脚本,还能微信实时通知

发表于 2020-09-06

据说震惊体很吸引眼球,今天我也来试一试🤪

本系列所有文章的开头都会用一两句话总结一下对应文章的内容。对这个话题感兴趣的话可以继续往下读,不感兴趣可以直接关掉,绝不浪费读者的时间。

总结

本篇文章用Python实现了一个简单的自动交易脚本,产生交易时会自动通过微信通知。

涉及概念 概念内容
Python库 Wechaty
主要概念 量化交易、微信机器人

量化交易

简介

7月份大A股的好行情想必大家都印象深刻,甚至有人预言A股五六年一个周期的大牛市即将开启。

结果大家已经看到了,但且不论开启没开启,反正当时大多数人的脑子都挺热的,这当然也包括我。不过,回顾我悲惨的投资经历,“追涨杀跌”这四个字是一个很好的总结😂。

这就难办了,眼睁睁看着别人赚钱翻倍,我不赚钱都相当于是亏钱。

我深知我每次的交易决策都是自身情绪的反映,而往往情绪化交易的后果就是追涨杀跌亏钱。那么问题来了,怎么才能管得住我这双亏钱的手?其中一个答案可能是量化交易。

量化交易其实不是很高深的概念。

一个量化交易软件会严格按照已经定义好的交易策略进行买入卖出操作,这些操作完完全全由策略触发,不受人为控制。

最简单的交易策略有双均值策略、网格交易策略等。

这简直就是为我设计的交易方式啊,妈妈再也不用担心我的臭手了。

网格交易策略

网上现成的量化交易框架很多,但学习这些框架可能需要比较长的时间。反正我就是想简单测试一下,顺便熟悉一下策略的机制,于是直接徒手写一个吧。

到底啥叫网格交易?网格交易可以简单地理解为:

把价格的波动区间放到以一个设定好的网格里,资金分成多份,价格每跌一格就买一份,每涨一格就卖一份。一份买入对应一份卖出,买卖交易之间只赚一格网格的差价。

看起来这正是我需要的。网格交易比较适用于震荡频率较高幅度较大的标的;一次只赚一格的钱,积少成多;把钱分为若干份,虽然利用率变低了,但也降低了风险。

说干就干,我决定测试一下网格交易策略效果如何。

首先需要预先定义一个震荡区间和网格数,我把这些需要预先定义的参数都放置到专门的配置文件里:

1
2
3
4
5
6
7
8
python复制代码lowest = 2.5  # 网格最低价格
highest = 3.5 # 网格最高价格
parts = 20 # 网格数
start_value = 300.0 # 账户初始资金
timespan = 15 # 每15秒检测一次标的价格
wechat_reminder = 1 # 是否通过微信通知(1:是,0:否)
mail_reminder = 0 # 是否通过邮件通知(1:是,0:否)
mail_list = ['mailbox1', ] # 需要通知的邮箱列表

初始化时,脚本会根据配置来设置当前的网格:

1
2
3
4
5
6
7
8
9
10
python复制代码# 每一格的高度
price_part_value = (highest - lowest) / parts
# 每变动一格对应的持仓百分比
percent_part_value = 1 / parts
# 所有网格的标记价格
price_levels = [round(highest - index * price_part_value, 4) for index in range(parts + 1)]
price_levels[-1] = lowest
# 每一格对应的整体持仓百分比
percent_levels = [round(0 + index * percent_part_value, 4) for index in range(parts + 1)]
percent_levels[-1] = 1.0000

配置完成后需要根据标的当前价格建底仓:

1
2
3
4
5
6
7
8
9
10
11
python复制代码# 如果当前价格比网格最低价格高则按照网格计算建仓层数,反之直接全仓
# order_target_percent根据target的符号及数值判断买入还是卖出,并下单交易
if float(close) > price_levels[0]:
pos = [close > level for level in price_levels]
i = pos.index(False) - 1
last_price_index = i
target = percent_levels[last_price_index] - last_percent
if target != 0.0:
return True, order_target_percent(float(close), depth, target, date=date)
else:
return True, order_target_percent(float(close), depth, 1.0, date=date)

然后每次标的价格穿越网格时执行对应的操作:

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
python复制代码signal = False
while True:
upper = None
lower = None
if last_price_index > 0:
upper = price_levels[last_price_index - 1]
if last_price_index < len(price_levels) - 1:
lower = price_levels[last_price_index + 1]
# 还不是最轻仓,继续涨,就再卖一档
if upper and float(close) > upper:
last_price_index = last_price_index - 1
signal = True
continue
# 还不是最重仓,继续跌,再买一档
if lower and float(close) < lower:
last_price_index = last_price_index + 1
signal = True
continue
break
if signal:
target = percent_levels[last_price_index] - last_percent
if target != 0.0:
return True, order_target_percent(float(close), depth, target, date=date)
else:
return False, []

到这里一个最简单但很完整的网格交易策略就写好了。当然了,实时价格的获取以及交易操作的实现需要读者自己去实现了。

微信机器人

【本文来自微信公众号Titus的小宇宙,ID为TitusCosmos,转载请注明!】

【为了防止网上各种爬虫一通乱爬还故意删除原作者信息,故在文章中间加入作者信息,还望各位读者理解】

简介

看过我之前文章的读者都知道,我写的所有关于微信机器人的文章全都是用的ItChat。这个库只支持能登录网页版微信的微信号,2017年之前没有登陆过网页版微信的微信号和2017年之后申请的新微信号基本上都不能用,局限性太大!读者可以打开网页版微信官网https://wx.qq.com/查看自己是否可以使用网页版微信。

经过我不懈的寻找,终于找到一款不限于网页版微信的库:Wechaty:

Wechaty原本是用TypeScript开发的,但开发团队正在进行多语言的移植,目前已经有可以用的Python版本,不过功能可能暂时没有原版那么强大。Wechaty开发团队励志把它开发成一款支持全平台微信协议的库,目前已经支持Web、Ipad、Windows以及Mac等协议。虽然需要付费获取token,但是可以申请参与开源激励计划来获取免费甚至长期有效的token。在这里也要感谢Wechaty团队提供微信机器人SDK🙏🙏🙏。

Demo

下面来实现一个最简单的机器人:收到任意消息后回复收到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码import asyncio
from typing import Optional, Union
from wechaty import Wechaty, Contact
from wechaty.user import Message, Room

async def wechat():
bot = Wechaty()
# 将on_message方法绑定到bot对象的message事件上
bot.on('message', on_message)
await bot.start()

async def on_message(msg: Message):
from_contact = msg.talker()
text = msg.text()
room = msg.room()
conversation: Union[
Room, Contact] = from_contact if room is None else room
await conversation.ready()
await conversation.say('收到')

asyncio.run(wechat())

上面的代码只用到了message事件,Wechaty还提供很多事件接口,例如scan、login、room-join、room-leave、logout、error等,想了解的读者可以联系交流。

脚本中的使用

上面的demo代码很简单,一看就懂,在本案例中的使用其实也很简单。

当前价格触发网格策略进行交易时,只需要在交易后总结当次交易的信息,然后利用类似上面的方法发送出来即可。实际操作中,我会把相应消息发送到我专门建的“量化动态播报”群里(目前在运行的有两个网格):

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
python复制代码async def on_message(msg: Message):
from_contact = msg.talker()
text = msg.text()
room = msg.room()
conversation: Union[
Room, Contact] = from_contact if room is None else room
if from_contact.payload.name.upper() == AdminWechatName and from_contact.contact_id.upper() == AdminWechatContactId:
# 发送#GRIDSTATUS时回复当前网格仓位状态等
if text.upper() == '#GRIDSTATUS':
await conversation.ready()
await conversation.say(str(grid))
# 发送#SETGRID:格式信息时动态修改网格参数
elif text.upper().startswith('#SETGRID:'):
paras = {('grid.' + item.split('=')[0]): item.split('=')[1] for item in text[9:].lower().split(';')}
cfg.set_paras(paras)
await run_grid()

async def run_grid():
flag, content = grid.run_data(data)
if flag:
if int(grid.mail_reminder):
for mail_box in grid.mail_list:
mail_helper.sendmail(mail_box, 'GRID SCRIPT NEW TRADE', content)
if int(grid.wechat_reminder):
await trade_reminder(bot, content)

async def trade_reminder(bot, mail_content, target=None):
for id in target:
room = bot.Room.load(id)
await room.ready()
conversation: Union[Room, Contact] = room
await conversation.ready()
await conversation.say(mail_content)

下面是实际效果:

微信机器人的替代方案

肯定会有读者觉得上面的机器人还是太麻烦,这里在推荐一个小工具,同时也感谢开发者的努力与奉献!

这个工具的名字叫Server酱,英文名为ServerChan,是一款程序员和服务器之间的通信软件,也就是从服务器推报警和日志到手机的工具。官网是sc.ftqq.com/3.version,读者可以自行了解,总之很好用就是了。

开通并使用上它,只需要一分钟:

  • 登入:用GitHub账号登入网站,就能获得一个SCKEY。
  • 绑定:点击“微信推送”,扫码关注同时即可完成绑定。
  • 发消息:往http://sc.ftqq.com/SCKEY.send发GET请求,就可以在微信里收到消息了。

收到的消息类似于这种:

是不是很简单方便?

附言

由于网格交易策略更适用于震荡市,所以目前策略是在数字货币上实施的,正在运行的包括一个实盘币种和一个测试币种,玩过数字货币的读者应该对它的震荡行情有所了解。

有兴趣的读者也可以联系我进量化动态播报群,网格交易触发的时候机器人会把操作动态实时发到群里。

后记

不管写什么,希望能跟更多人沟通,有问题或者需求随时欢迎交流。

我所有的项目源码都会放在下面的github仓库里面,有需要可以参考,有问题欢迎指正,谢谢!

1
html复制代码https://github.com/TitusWongCN/

下面是我的公众号,我有兴趣可以扫一下:

本文转载自: 掘金

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

5千字的SpringMVC总结,我觉得你会需要的 思维导图

发表于 2020-09-06

思维导图

微信公众号已开启:【java技术爱好者】,还没关注的记得关注哦~

文章已收录到我的Github精选,欢迎Star:github.com/yehongzhi/l…

概述

SpringMVC再熟悉不过的框架了,因为现在最火的SpringBoot的内置MVC框架就是SpringMVC。我写这篇文章的动机是想通过回顾总结一下,重新认识SpringMVC,所谓温故而知新嘛。

为了了解SpringMVC,先看一个流程示意图:

从流程图中,我们可以看到:

  • 接收前端传过来Request请求。
  • 根据映射路径找到对应的处理器处理请求,处理完成之后返回ModelAndView。
  • 进行视图解析,视图渲染,返回响应结果。

总结就是:参数接收,定义映射路径,页面跳转,返回响应结果。

当然这只是最基本的核心功能,除此之外还可以定义拦截器,全局异常处理,文件上传下载等等。

一、搭建项目

在以前的老项目中,因为还没有SpringBoot,没有自动配置,所以需要使用web.xml文件去定义一个DispatcherServlet。现在互联网应用基本上都使用SpringBoot,所以我就直接使用SpringBoot进行演示。很简单,引入依赖即可:

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

二、定义Controller

使用SpringMVC定义Controller处理器,总共有五种方式。

2.1 实现Controller接口

早期的SpringMVC是通过这种方式定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**
* @author Ye Hongzhi 公众号:java技术爱好者
* @name DemoController
* @date 2020-08-25 22:28
**/
@org.springframework.stereotype.Controller("/demo/controller")
public class DemoController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
//业务处理
return null;
}
}

2.2 实现HttpRequestHandler接口

跟第一种方式差不多,也是通过实现接口的方式:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码/**
* @author Ye Hongzhi 公众号:java技术爱好者
* @name HttpDemoController
* @date 2020-08-25 22:45
**/
@Controller("/http/controller")
public class HttpDemoController implements HttpRequestHandler{
@Override
public void handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {
//业务处理
}
}

2.3 实现Servlet接口

这种方式已经不推荐使用了,不过从这里可以看出SpringMVC的底层使用的还是Servlet。

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复制代码@Controller("/servlet/controller")
public class ServletDemoController implements Servlet {
//以下是Servlet生命周期方法
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}

@Override
public ServletConfig getServletConfig() {
return null;
}

@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
}

@Override
public String getServletInfo() {
return null;
}

@Override
public void destroy() {
}
}

因为不推荐使用这种方式,所以默认是不加载这种适配器的,需要加上:

1
2
3
4
5
6
7
8
9
java复制代码@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {

@Bean
public SimpleServletHandlerAdapter simpleServletHandlerAdapter() {
return new SimpleServletHandlerAdapter();
}
}

2.4 使用@RequestMapping

这种方式是最常用的,因为上面那些方式定义需要使用一个类定义一个路径,就会导致产生很多类。使用注解就相对轻量级一些。

1
2
3
4
5
6
7
8
9
java复制代码@Controller
@RequestMapping("/requestMapping/controller")
public class RequestMappingController {

@RequestMapping("/demo")
public String demo() {
return "HelloWord";
}
}

2.4.1 支持Restful风格

而且支持Restful风格,使用method属性定义对资源的操作方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码	@RequestMapping(value = "/restful", method = RequestMethod.GET)
public String get() {
//查询
return "get";
}

@RequestMapping(value = "/restful", method = RequestMethod.POST)
public String post() {
//创建
return "post";
}

@RequestMapping(value = "/restful", method = RequestMethod.PUT)
public String put() {
//更新
return "put";
}

@RequestMapping(value = "/restful", method = RequestMethod.DELETE)
public String del() {
//删除
return "post";
}

2.4.2 支持Ant风格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码	//匹配 /antA 或者 /antB 等URL
@RequestMapping("/ant?")
public String ant() {
return "ant";
}

//匹配 /ant/a/create 或者 /ant/b/create 等URL
@RequestMapping("/ant/*/create")
public String antCreate() {
return "antCreate";
}

//匹配 /ant/create 或者 /ant/a/b/create 等URL
@RequestMapping("/ant/**/create")
public String antAllCreate() {
return "antAllCreate";
}

2.5 使用HandlerFunction

最后一种是使用HandlerFunction函数式接口,这是Spring5.0后引入的方式,主要用于做响应式接口的开发,也就是Webflux的开发。

有兴趣的可以网上搜索相关资料学习,这个讲起来可能要很大篇幅,这里就不赘述了。

三、接收参数

定义完Controller之后,需要接收前端传入的参数,怎么接收呢。

3.1 接收普通参数

在@RequestMapping映射方法上写上接收参数名即可:

1
2
3
4
5
java复制代码@RequestMapping(value = "/restful", method = RequestMethod.POST)
public String post(Integer id, String name, int money) {
System.out.println("id:" + id + ",name:" + name + ",money:" + money);
return "post";
}

3.2 @RequestParam参数名绑定

如果不想使用形参名称作为参数名称,可以使用@RequestParam进行参数名称绑定:

1
2
3
4
5
6
7
8
9
10
java复制代码	/**
* value: 参数名
* required: 是否request中必须包含此参数,默认是true。
* defaultValue: 默认参数值
*/
@RequestMapping(value = "/restful", method = RequestMethod.GET)
public String get(@RequestParam(value = "userId", required = false, defaultValue = "0") String id) {
System.out.println("id:" + id);
return "get";
}

3.3 @PathVariable路径参数

通过@PathVariable将URL中的占位符{xxx}参数映射到操作方法的入参。演示代码如下:

1
2
3
4
5
java复制代码@RequestMapping(value = "/restful/{id}", method = RequestMethod.GET)
public String search(@PathVariable("id") String id) {
System.out.println("id:" + id);
return "search";
}

3.4 @RequestHeader绑定请求头属性

获取请求头的信息怎么获取呢?

使用@RequestHeader注解,用法和@RequestParam类似:

1
2
3
4
java复制代码	@RequestMapping("/head")
public String head(@RequestHeader("Accept-Language") String acceptLanguage) {
return acceptLanguage;
}

3.5 @CookieValue绑定请求的Cookie值

获取Request中Cookie的值:

1
2
3
4
java复制代码	@RequestMapping("/cookie")
public String cookie(@CookieValue("_ga") String _ga) {
return _ga;
}

3.6 绑定请求参数到POJO对象

定义了一个User实体类:

1
2
3
4
5
6
java复制代码public class User {
private String id;
private String name;
private Integer age;
//getter、setter方法
}

定义一个@RequestMapping操作方法:

1
2
3
4
java复制代码	@RequestMapping("/body")
public String body(User user) {
return user.toString();
}

只要请求参数与属性名相同自动填充到user对象中:

3.6.1 支持级联属性

现在多了一个Address类存储地址信息:

1
2
3
4
5
java复制代码public class Address {
private String id;
private String name;
//getter、setter方法
}

在User中加上address属性:

1
2
3
4
5
6
7
java复制代码public class User {
private String id;
private String name;
private Integer age;
private Address address;
//getter、setter方法
}

传参时只要传入address.name、address.id即会自动填充:

3.6.2 @InitBinder解决接收多对象时属性名冲突

如果有两个POJO对象拥有相同的属性名,不就产生冲突了吗?比如刚刚的user和address,其中他们都有id和name这两个属性,如果同时接收,就会冲突:

1
2
3
4
5
java复制代码	//user和address都有id和name这两个属性	
@RequestMapping(value = "/twoBody", method = RequestMethod.POST)
public String twoBody(User user, Address address) {
return user.toString() + "," + address.toString();
}

这时就可以使用@InitBinder绑定参数名称:

1
2
3
4
5
6
7
8
9
java复制代码	@InitBinder("user")
public void initBindUser(WebDataBinder webDataBinder) {
webDataBinder.setFieldDefaultPrefix("u.");
}

@InitBinder("address")
public void initBindAddress(WebDataBinder webDataBinder) {
webDataBinder.setFieldDefaultPrefix("addr.");
}

3.6.3 @Requestbody自动解析JSON字符串封装到对象

前端传入一个json字符串,自动转换成pojo对象,演示代码:

1
2
3
4
java复制代码	@RequestMapping(value = "/requestBody", method = RequestMethod.POST)
public String requestBody(@RequestBody User user) {
return user.toString();
}

注意的是,要使用POST请求,发送端的Content-Type设置为application/json,数据是json字符串:

甚至有一些人喜欢用一个Map接收:

但是千万不要用Map接收,否则会造成代码很难维护,后面的老哥估计看不懂你这个Map里面有什么数据,所以最好还是定义一个POJO对象。

四、参数类型转换

实际上,SpringMVC框架本身就内置了很多类型转换器,比如你传入字符串的数字,接收的入参定为int,long类型,都会自动帮你转换。

就在包org.springframework.core.convert.converter下,如图所示:

有的时候如果内置的类型转换器不足够满足业务需求呢,怎么扩展呢,很简单,看我操作。什么是Java技术爱好者(战术后仰)。

首先有样学样,内置的转换器实现Converter接口,我也实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class StringToDateConverter implements Converter<String, Date> {
@Override
public Date convert(String source) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
try {
//String转换成Date类型
return sdf.parse(source);
} catch (Exception e) {
//类型转换错误
e.printStackTrace();
}
return null;
}
}

接着把转换器注册到Spring容器中:

1
2
3
4
5
6
7
8
java复制代码@Configuration
public class ConverterConfig extends WebMvcConfigurationSupport {
@Override
protected void addFormatters(FormatterRegistry registry) {
//添加类型转换器
registry.addConverter(new StringToDateConverter());
}
}

接着看测试,所有的日期字符串,都自动被转换成Date类型了,非常方便:

五、页面跳转

在前后端未分离之前,页面跳转的工作都是由后端控制,采用JSP进行展示数据。虽然现在互联网项目几乎不会再使用JSP,但是我觉得还是需要学习一下,因为有些旧项目还是会用JSP,或者需要重构。

如果你在RequestMapping方法中直接返回一个字符串是不会跳转到指定的JSP页面的,需要做一些配置。

第一步,加入解析jsp的Maven配置。

1
2
3
4
5
6
7
8
9
xml复制代码<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>7.0.59</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>

第二步,添加视图解析器。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Configuration
public class WebAppConfig extends WebMvcConfigurerAdapter {
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/");
viewResolver.setSuffix(".jsp");
viewResolver.setViewClass(JstlView.class);
return viewResolver;
}
}

第三步,设置IDEA的配置。

第四步,创建jsp页面。

第五步,创建Controller控制器。

1
2
3
4
5
6
7
8
java复制代码@Controller
@RequestMapping("/view")
public class ViewController {
@RequestMapping("/hello")
public String hello() throws Exception {
return "hello";
}
}

这样就完成了,启动项目,访问/view/hello就看到了:

就是这么简单,对吧

六、@ResponseBody

如果采用前后端分离,页面跳转不需要后端控制了,后端只需要返回json即可,怎么返回呢?

使用@ResponseBody注解即可,这个注解会把对象自动转成json数据返回。

@ResponseBody注解可以放在类或者方法上,源码如下:

1
2
3
4
5
6
java复制代码//用在类、方法上
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseBody {
}

演示一下:

1
2
3
4
5
6
7
8
9
java复制代码@RequestMapping("/userList")
@ResponseBody
public List<User> userList() throws Exception {
List<User> list = new ArrayList<>();
list.add(new User("1","姚大秋",18));
list.add(new User("2","李星星",18));
list.add(new User("3","冬敏",18));
return list;
}

测试一下/view/userList:

七、@ModelAttribute

@ModelAttribute用法比较多,下面一一讲解。

7.1 用在无返回值的方法上

在Controller类中,在执行所有的RequestMapping方法前都会先执行@ModelAttribute注解的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Controller
@RequestMapping("/modelAttribute")
public class ModelAttributeController {
//先执行这个方法
@ModelAttribute
public void modelAttribute(Model model){
//在request域中放入数据
model.addAttribute("userName","公众号:java技术爱好者");
}

@RequestMapping("/index")
public String index(){
//跳转到inex.jsp页面
return "index";
}
}

index.jsp页面如下:

1
2
3
4
5
6
7
8
9
10
jsp复制代码<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>首页</title>
</head>
<body>
<!-- 获取到userName属性值 -->
<h1>${userName}</h1>
</body>
</html>

相当于一个Controller的拦截器一样,在执行RequestMapping方法前先执行@ModelAttribute注解的方法。所以要慎用。

启动项目,访问/modelAttribute/index可以看到:

即使在index()方法中没有放入userName属性值,jsp页面也能获取到,因为在执行index()方法之前的modelAttribute()方法已经放入了。

7.2 放在有返回值的方法上

其实调用顺序是一样,也是在RequestMapping方法前执行,不同的在于,方法的返回值直接帮你放入到Request域中。

1
2
3
4
5
6
7
8
9
10
11
java复制代码//放在有参数的方法上
@ModelAttribute
public User userAttribute() {
//相当于model.addAttribute("user",new User("1", "Java技术爱好者", 18));
return new User("1", "Java技术爱好者", 18);
}

@RequestMapping("/user")
public String user() {
return "user";
}

创建一个user.jsp:

1
2
3
4
5
6
7
8
9
10
11
jsp复制代码<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>首页</title>
</head>
<body>
<h1>ID:${user.id}</h1>
<h1>名称:${user.name}</h1>
<h1>年龄:${user.age}岁</h1>
</body>
</html>

测试一下:

放入Request域中的属性值默认是类名的首字母小写驼峰写法,如果你想自定义呢?很简单,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
java复制代码//自定义属性名为"u"
@ModelAttribute("u")
public User userAttribute() {
return new User("1", "Java技术爱好者", 18);
}
/**
JSP就要改成这样写:
<h1>ID:${u.id}</h1>
<h1>名称:${u.name}</h1>
<h1>年龄:${u.age}岁</h1>
*/

7.3 放在RequestMapping方法上

1
2
3
4
5
6
7
8
9
10
java复制代码@Controller
@RequestMapping("/modelAttribute")
public class ModelAttributeController {

@RequestMapping("/jojo")
@ModelAttribute("attributeName")
public String jojo() {
return "JOJO!我不做人了!";
}
}

这种情况下RequestMapping方法的返回的值就不是JSP视图了。而是把返回值放入Request域中的属性值,属性名为attributeName。视图则是RequestMapping注解上的URL,所以创建一个对应的JSP页面:

1
2
3
4
5
6
7
8
9
jsp复制代码<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>首页</title>
</head>
<body>
<h1>${attributeName}</h1>
</body>
</html>

测试一下:

7.4 放在方法入参上

放在入参上,意思是从前面的Model中提取出对应的属性值,当做入参传入方法中使用。如下所示:

1
2
3
4
5
6
7
8
9
10
11
java复制代码@ModelAttribute("u")
public User userAttribute() {
return new User("1", "Java技术爱好者", 18);
}

@RequestMapping("/java")
public String user1(@ModelAttribute("u") User user) {
//拿到@ModelAttribute("u")方法返回的值,打印出来
System.out.println("user:" + user);
return "java";
}

测试一下:

八、拦截器

拦截器算重点内容了,很多时候都要用拦截器,比如登录校验,权限校验等等。SpringMVC怎么添加拦截器呢?

很简单,实现HandlerInterceptor接口,接口有三个方法需要重写。

  • preHandle():在业务处理器处理请求之前被调用。预处理。
  • postHandle():在业务处理器处理请求执行完成后,生成视图之前执行。后处理。
  • afterCompletion():在DispatcherServlet完全处理完请求后被调用,可用于清理资源等。返回处理(已经渲染了页面);

自定义的拦截器,实现的接口HandlerInterceptor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class DemoInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//预处理,返回true则继续执行。如果需要登录校验,校验不通过返回false即可,通过则返回true。
System.out.println("执行preHandle()方法");
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//后处理
System.out.println("执行postHandle()方法");
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//在DispatcherServlet完全处理完请求后被调用
System.out.println("执行afterCompletion()方法");
}
}

然后把拦截器添加到Spring容器中:

1
2
3
4
5
6
7
8
java复制代码@Configuration
public class ConverterConfig extends WebMvcConfigurationSupport {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new DemoInterceptor()).addPathPatterns("/**");
}
}

/**代表所有路径,测试一下:

九、全局异常处理

SpringMVC本身就对一些异常进行了全局处理,所以有内置的异常处理器,在哪里呢?

看HandlerExceptionResolver接口的类图就知道了:

从类图可以看出有四种异常处理器:

  • DefaultHandlerExceptionResolver,默认的异常处理器。根据各个不同类型的异常,返回不同的异常视图。
  • SimpleMappingExceptionResolver,简单映射异常处理器。通过配置异常类和view的关系来解析异常。
  • ResponseStatusExceptionResolver,状态码异常处理器。解析带有@ResponseStatus注释类型的异常。
  • ExceptionHandlerExceptionResolver,注解形式的异常处理器。对@ExceptionHandler注解的方法进行异常解析。

第一个默认的异常处理器是内置的异常处理器,对一些常见的异常处理,一般来说不用管它。后面的三个才是需要注意的,是用来扩展的。

9.1 SimpleMappingExceptionResolver

翻译过来就是简单映射异常处理器。用途是,我们可以指定某种异常,当抛出这种异常之后跳转到指定的页面。请看演示。

第一步,添加spring-config.xml文件,放在resources目录下,文件名见文知意即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<!-- 定义默认的异常处理页面 -->
<property name="defaultErrorView" value="err"/>
<!-- 定义异常处理页面用来获取异常信息的属性名,默认名为exception -->
<property name="exceptionAttribute" value="ex"/>
<!-- 定义需要特殊处理的异常,用类名或完全路径名作为key,异常也页名作为值 -->
<property name="exceptionMappings">
<props>
<!-- 异常,err表示err.jsp页面 -->
<prop key="java.lang.Exception">err</prop>
<!-- 可配置多个prop -->
</props>
</property>
</bean>
</beans>

第二步,在启动类加载xml文件:

1
2
3
4
5
6
7
8
9
java复制代码@SpringBootApplication
@ImportResource("classpath:spring-config.xml")
public class SpringmvcApplication {

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

}

第三步,在webapp目录下创建一个err.jsp页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
jsp复制代码<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>异常页面</title>
</head>
<body>
<h1>出现异常,这是一张500页面</h1>
<br>
<%-- 打印异常到页面上 --%>
<% Exception ex = (Exception)request.getAttribute("ex"); %>
<br>
<div><%=ex.getMessage()%></div>
<% ex.printStackTrace(new java.io.PrintWriter(out)); %>
</body>
</html>

这样就完成了,写一个接口测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Controller
@RequestMapping("/exception")
public class ExceptionController {
@RequestMapping("/index")
public String index(String msg) throws Exception {
if ("null".equals(msg)) {
//抛出空指针异常
throw new NullPointerException();
}
return "index";
}
}

效果如下:

这种异常处理器,在现在前后端分离的项目中几乎已经看不到了。

9.2 ResponseStatusExceptionResolver

这种异常处理器主要用于处理带有@ResponseStatus注释的异常。请看演示代码:

自定义一个异常类,并且使用@ResponseStatus注解修饰:

1
2
3
4
java复制代码//HttpStatus枚举有所有的状态码,这里返回一个400的响应码
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public class DefinedException extends Exception{
}

写一个Controller接口进行测试:

1
2
3
4
5
6
7
java复制代码@RequestMapping("/defined")
public String defined(String msg) throws Exception {
if ("defined".equals(msg)) {
throw new DefinedException();
}
return "index";
}

启动项目,测试一下,效果如下:

9.3 ExceptionHandlerExceptionResolver

注解形式的异常处理器,这是用得最多的。使用起来非常简单方便。

第一步,定义自定义异常BaseException:

1
2
3
4
5
java复制代码public class BaseException extends Exception {
public BaseException(String message) {
super(message);
}
}

第二步,定义一个错误提示实体类ErrorInfo:

1
2
3
4
5
6
7
8
java复制代码public class ErrorInfo {
public static final Integer OK = 0;
public static final Integer ERROR = -1;
private Integer code;
private String message;
private String url;
//getter、setter
}

第三步,定义全局异常处理类GlobalExceptionHandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码//这里使用了RestControllerAdvice,是@ResponseBody和@ControllerAdvice的结合
//会把实体类转成JSON格式的提示返回,符合前后端分离的架构
@RestControllerAdvice
public class GlobalExceptionHandler {

//这里自定义了一个BaseException,当抛出BaseException异常就会被此方法处理
@ExceptionHandler(BaseException.class)
public ErrorInfo errorHandler(HttpServletRequest req, BaseException e) throws Exception {
ErrorInfo r = new ErrorInfo();
r.setMessage(e.getMessage());
r.setCode(ErrorInfo.ERROR);
r.setUrl(req.getRequestURL().toString());
return r;
}
}

完成之后,写一个测试接口:

1
2
3
4
5
6
7
java复制代码@RequestMapping("/base")
public String base(String msg) throws Exception {
if ("base".equals(msg)) {
throw new BaseException("测试抛出BaseException异常,欧耶!");
}
return "index";
}

启动项目,测试:

絮叨

SpringMVC的功能实际上肯定还不止我写的这些,不过学会上面这些之后,基本上已经可以应对日常的工作了。

如果要再深入一些,最好是看看SpringMVC源码,我之前写过三篇,责任链模式与SpringMVC拦截器,适配器模式与SpringMVC,全局异常处理源码分析。有兴趣可以关注公众号看看我的历史文章。

微信公众号已开启:【java技术爱好者】,没关注的同学记得关注哦~

坚持原创,持续输出兼具广度和深度的技术文章。

文章已收录到我的Github精选,欢迎Star:github.com/yehongzhi/l…

上面所有例子的代码都上传Github了:

github.com/yehongzhi/m…

觉得有用就点个赞吧,你的点赞是我创作的最大动力~

拒绝做一条咸鱼,我是一个努力让大家记住的程序员。我们下期再见!!!

能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!

本文转载自: 掘金

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

MySQL 之数据库高并发解决方案

发表于 2020-09-06

前言

我们都知道初创公司一开始都是以单体应用为首要架构,一般都是单体单库的形式。但是版本以及版本的迭代,数据库需要承受更多的高并发已经成了架构设计需要考虑的点。

那么解决问题,就得说到方案。但是方案有很多,我们该怎么选择呢?

优化与方案

基本上,我们优化要从几个关键字入手:短距离,少数据,分散压力。

短距离

所谓的短距离,指的是从前端到数据库的路径要短。

  1. 页面静态。有些页面的数据是在某些时段是不变的,那么这个页面可以静态化,这样可以提高访问的速度。
  2. 使用缓存。缓存大家都知道,快的原因就是基于内存。所以使用基于内存的缓存的话,可以减少对数据库的访问,同时加速访问速度。
  3. 批量读取。高并发的情况下,可以将多个请求的查询合在一次进行,以减少对数据库的访问速度。
  4. 延迟修改。延迟修改的意思高并发的情况西可能是将多次修改数据放在缓存中,然后定时将缓存中的数据过更新到数据库;也可以是通过缓存的同步策略通过解析异步同步到数据库中。
  5. 使用索引。这个不用说了,索引有着比较多的类型,例如普通索引/主键索引/组合索引/全文索引等。

少数据

所谓的少数据,其实是查询的数据要少。

  1. 分表。所谓的分表,其实有水平切分和垂直拆分。玩过单机的小伙伴都知道,往往一些具有历史性的表单,都会有成百上千万级别的数据。这样子对于 MySQL 来说,即使是加了索引,SQL 方面继续优化,也很难做到更快的查询速度。那么我们可以通过分表的操作来实现。例如说最常见的我们可以根据时间的维度来进行表的水平拆分,今年的数据保持下来,而去年的数据可以存在另外一个表里。
  2. 分离活跃数据。其实这个有点类似缓存,但是不同之处在于数据还是在 MySQL 上面的。例如一个查询商品的业务,有一些火爆/经常被搜索的商品可以存在一张活跃表。查询的时候先查询活跃表,没有的话再查询总商品表。
  3. 分块。这个分块有点类似于算法里面的“索引顺序查找”。通过数据层面的优化,将数据放在不同的块中,而我们只需要计算找到对应的块就行了。

分散压力

所谓的分散压力,其实是分散不同数据库服务器的压力

  1. 集群。集群的概念相信大家都很清楚,对于业务服务器来说其实就是具备相同业务流程的服务器部署多台,通过负载均衡或其他方式来将请求分配到不同服务器。而数据库也一样,通过特定的规则策略将数据导向特定的数据库服务器上。
  2. 分布式。所谓的分布式,其实就是将原本处于同个流程的业务逻辑分配到不同的服务器上面执行,达到了“并发”执行的效果,加快执行速度。
  3. 分库分表。分库分表主要是水平拆分和垂直拆分。对于访问频率高而数据量巨大的单表,可以减少单表的数据,根据特定的维度进行水平拆分,增加数据库的吞吐量,这就是分表水平拆分;而对于业务耦合性低的多表来说,可以将不同的表存储在不同的数据库上,对数据库进行垂直拆分,提高数据库写的能力,即分库的垂直拆分。
  4. 建立主从。建立主从的目的其实就是为了读写分离。我们都知道,只要数据库的事务级别够高,那么并发读是不会影响到数据的混乱,而并发写则会。所以建立主从一般来说,写会留在主服务器上写,而会在从服务器上读。所以基本上让主服务器进行事务性操作,从服务器进行 select 查询。这样子的话,事务性操作(增加/删除/修改)导致的改变更新同步到集群中的从数据库。

结语

完!

本文转载自: 掘金

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

写给小白看的Spring Cloud入门教程

发表于 2020-09-06

公司目前用的是Spring Cloud ,感觉自己对Spring Cloud了解的还不够系统,于是打算系统的整理一下自己对Spring Cloud的理解。边学边实践,我自己做了个Spring Cloud 的案例,考虑到访问GitHub比较慢,我就放码云上了。地址是
gitee.com/cxk6013/spr… 里面我都打了比较详细的注释,看完不妨自己尝试做一下。

微服务架构简介

当软件在慢慢的变复杂,开发和运维成本也在随之上升,这也是在软件开发发展过程中,改变或升级架构的一个原因,降低软件的复杂度,而降低软件复杂度的一种通用方式就是拆,将一个软件拆成若干个模块,这样复杂度就被均摊了,好钢也能用在刀刃上,什么意思呢? 以服务端为例,所有模块的请求量不是相等的,有的模块的访问量必然会很高,以电商为例,支付服务或者说是模块在平时访问量是相对平稳的,但是在双十一这一天,访问量就会急速的上升,这个时候企业常常就会对支付服务进行扩容,多给支付服务一些资源,假若不拆,这些服务整体被打包在一起的话,我给的资源就无法只被支付服务所享用。

微服务架构成为新常态的原因就是很好的降低了软件的复杂度,能够充分的利用资源,提升开发速度。将服务拆的足够泾渭分明,在某个服务访问量上升的时候,我就可以只给这个服务扩容,这就是充分的利用资源,除此之外,也能做到快速集成,我在重新集成这个服务的时候,就不会影响到其他服务,也就是解耦合。但这并不代表微服务是完美无缺的,管理和运维微服务
相对于过去就变得有些复杂起来了,这也是Docker和K8s流行的原因。

总结一下,这里讨论的微服务指代的是微服务架构,粗略的讲,我们可以这么理解微服务架构, 根据业务拆分软件模块,每个模块单独运行,每个模块本身是单体。

再议分布式和集群

“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像是单个相关系统”。 《分布式系统原理和范型》
分布式系统是一组计算机,通过网络相互连接传递消息与通信后并协调它们的行为而形成的系统。组件之间彼此进行交互以实现一个共同的目标。 《维基百科》

在《且谈软件架构》一文中我已经讨论过分布式和集群了,但是还不充分,因为技术在一直不断的朝前走,资源的利用率在不断的提高。上述的分布式强调的是一组计算机,是的,鸡蛋不能都放在一个篮子里,我之前的想法是假如我有一台计算机,资源比较丰富,使用Docker完成部署,是不是也算一种分布式,并不能算,因为假如这台计算机因为某些原因不能提供服务了怎么办? 如果将进程当做一个鸡蛋的话,计算机就是装鸡蛋的篮子,然后篮子坏掉了。我想分布式应该是在强调一组计算机应该就是在强调这种容灾能力吧,努力将损失降到最低。

那么如何在是分布式系统的情况下,某台服务器不能再提供服务之后,我们的系统还能正常提供服务呢?就好像这台计算机没有坏掉呢? 那就是集群,就是相同的程序找了几台服务器又部署了几次。一台坏掉,我还有另一个。你要是都坏掉? 应该不会这么点背吧。

总结一下:

  • 如果若干个程序如果部署在若干台不同的计算机上,它们通过网络相互协作最终完成一个服务,那么这些程序组成的系统就可以称为分布式系统。
  • 如果是相同的程序,在本机部署了多次,有了Docker后这很轻松,这就叫集中式集群。
  • 如果是在不同的计算机上部署了多次,我们可以称之为分布式集群。

都是集群,都提高了程序的访问量,分布式集群相对于集中式集群,多了一个容灾能力,但是也引入了新的问题。
这就是我们下文要讨论的CAP定理。

你需要知道的CAP定理

CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

我们先来解释P,因为P看起来比较高端的样子。上文我们讨论过分布式系统强调不同的计算机,不同的程序部署在若干台不同的计算机上,通过网络来相互协作最终完成一个服务。我们可以认为组成分布式系统的这些计算机当做一个节点。

分区容错就是在说节点之间的通信可能会失败,当我们只有一台数据库的时候,而某天分布式系统的某个节点因为网络故障无法访问到数据库,那么这个时候分区就是无法容错的。

为了提升分区容错性,这个时候我们又引入了一个数据库,这样分区容错性就进一步的提高了,但是这个时候又出现了新的问题,即如何保持两个数据库的数据的一致性,要保证一致的话,完成写操作的充分必要条件就是等待全部节点写入成功。

每次写操作等待所有节点写入完成,又会引入降低可用性,因为向所有节点写入数据,可能会写入失败或者写入时间过长,因为你无法保证网络是绝对可靠的。

总结: 为了提升分区容错性,我们将相同的服务多次部署,每个服务相同,部署的越多,分区容错性越高,然后这些服务之间的数据同步就是一个新的问题,也就是一致性。如果我们硬要保持强一致性,那么可用性就会降低。

从上面的讨论我们可以得出,CAP是无法兼得的,我们只能选择其二,我们可以选择CP,AP。但是请注意不是要了AP就不要C了,我们可以放松对一致性的要求。

在上面的讨论中C是强一致性,即每个节点的数据都是最新版本,我们可以放松对一致性的要求,也就是降级,一致性也有不同的级别:

  • 弱一致性: 每个节点的数据可以不全是最新版本
  • 最终一致性: 放宽对时间的要求,经过一段时间,各个节点的数据才是全是最新的。

举例: 写操作不等着全部节点写入完成就返回响应,等待一些时间完成各个节点的同步。

对可用性的要求,对于一个满足可用性的系统,每一个非故障节点必须对每一个请求作出响应。我们一般衡量一个系统的可用性的时候,都是通过通过停机时间来计算的:

参考的博文:

  • CAP理论中的P到底是个什么意思?
  • CAP 定理的含义
  • 分布式系统的CAP理论
  • 外行人都能看懂的SpringCloud,错过了血亏!
  • 浅谈分布式系统的基本问题:可用性与一致性

Spring Cloud与Spring Boot

上文我们提到了微服务架构,根据业务来拆分软件模块,我们用Spring boot来构建服务,用Spring Cloud来治理服务。
这个治理是什么意思呢?我们来思考一下,软件拆分模块,各模块独立运行之后引入的问题:

  • 怎么解决服务之间的相互调用
  • 假如在服务之间的相互调用中,某个服务因为网络或其他原因导致很久没有返回结果,应该怎么处理
  • 各个模块的配置如何统一的管理
  • 如何屏蔽我的真实访问路径,就是在我的真实访问路径包装一层,类似于虚拟手机号

Spring Boot 开发 Spring Cloud三部曲

Spring Boot的定位

之前因为找资料加了一些群,会看到一些人会问,现在学Spring框架的技术,应该怎么学起,有人就直接说,不要学Spring,Spring MVC了,直接从Spring Boot开始学吧,说Spring Spring MVC都已经落后了。这里我们理一下Spring boot和Spring MVC Spring 之间的关系哈。

因为我们在开发Spring 、 SpringMVC的时候配置是比较多的,做过SSM整合的都懂(SSM指 Spring、SpringMVC、MyBatis),某些程度上我们可以认为这些配置是固定的,就是谁注入谁之类的,那么Spring Boot的大致思想就是我帮你把这些配置都配了,加速你的开发。除此之外,一些关键的依赖之间的版本不是也比较难搞吗? Spring Boot将一些固定的依赖做到了一起,也就是starter,这就进一步的加速了开发。

但是Spring Boot的底层还是Spring,SpringMVC。不存在谁比谁先进的问题,只不过用Spring Boot开发更快而已,Spring Boot是一个稍微偏大的主题,这里限于篇幅,不做过多介绍,有兴趣的同学可以下去查查资料。

Spring Boot 和Spring Cloud的关系

Spring Cloud系列的组件都是用Spring boot来构建的,所以开发起来同样很快。 但是一旦出了错,如果你对Spring Spring MVC掌握的不深,你就很懵逼了。

Spring Boot也是一个庞大的项目,下面包含了许多starter(不懂这些starter的可以再去做下功课,不懂下面的你照样也可以看懂)
Spring Cloud是一个庞大的项目,下面包含了各个组件。
所以我们一般在父工程中统一管理Spring Boot和Spring Cloud的版本。

Spring Boot开发的常规操作

  • 引入依赖,我们一般用maven做
  • 写配置文件
  • 在启动类上打上注解

本文介绍的Spring Cloud组件

这里选取的是Spring Cloud Netflix家族系列:

  • Eureka 注册中心 主管服务的发现与相互调用
  • Feign 远程调用者
  • Ribbon 负载均衡器
  • zuul 网关
  • config 配置中心
  • Hystrix 熔断器

注册中心 Eureka

假如我现在有A、B、C三个服务,那我此时A服务希望调用B服务,我是直接通过显式的URL来调用B吗? 肯定不是,我们事实上可以从网络通信去找到对应的设计思路,想象一下过去的电话,假设A要和B打电话,也不是从A家里面架一条电话线到B家,假设A朋友比较多,A家的电话线就装不下了,这仅仅是两家的通信,假设我们不断的引入新用户,这种从需要相互通信的用户之间直接接电话线的思想,很快会让我们的线路变得难以管理,像下面这样:

哪天D搬家了,这个线路也得随时跟着D走,迁移成本太高了,事实上我们要接电话线和网线都是从自家引一条网线,接入电信运营商的网络,中间会有若干中间层,通过这些中间层来实现通信,服务调用也是对应的设计思路,A服务直接通过显式的URL(IP+端口+接口名: 192.168.3.1: 8080/buy/)调用B服务,哪天B服务换个地址,你A服务也要跟着动,这就是注册中心的用处之一:

这也就是Eureka注册中心的用处,注册中心上保存的是服务名,已注册的服务可以通过Eureka来获取其他服务的服务名,通过服务名来完成远程调用,这样某个服务换了IP也不用在意,你名字不变就行。
Eureka 分成server 和 client ,注册中心是server , client需要向注册中心的服务。
通过Spring Cloud和Spring Boot来构建微服务非常简单,通常分为三步:

  • 引入依赖 (我默认你已经学习过Maven了, 如果没学习过,请参看我的这篇博客: Maven学习笔记)
1
2
3
4
5
6
java复制代码  <dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
  • 写配置文件
1
2
3
4
5
6
7
8
java复制代码server:
port: 9991
eureka:
client:
service-url:
defaultZone: http://localhost:${server.port}/eureka/
register-with-eureka: false
fetch-registry: false

defaultZone是注册中心的地址,register-with-eureka是否让Eureka自己注册自己。
fetch-registry Eureka是否自己获取其他服务的地址。

  • 启动类上打上对应的注解
1
2
3
4
5
6
7
java复制代码EnableEurekaServer // 该注解标识该服务是服务注册中心。
SpringBootApplication
public class MicroEureka {
public static void main(String[] args) {
SpringApplication.run(MicroEureka.class,args);
}
}

我们将注册中心当做电信运行商就会得到注册中心的其他用处:

  • 服务注册: 相当于开号,服务的的提供者也就是被其他服务调用的服务,启动的时候会通过发送Rest请求的方式将自己注册到注册中心上,同时携带了自身的一些元数据信息(比如服务名,端口)。
  • 服务续约: 相当于缴费,电话费快耗尽了,电信运行商会催着你缴费,你如果不缴费就会停你机。也就是说,在注册完服务之后,服务提供者会维护一个心跳用来持续高速注册中心,我还活着。
  • 服务下线: 也就是销号,当服务要正常关闭的时候,它会向注册中心发送一个Rest请求给Eureka Server,告诉服务中心,这个号我不用了。
  • 获取服务 和 服务调用 : 就是相当于打电话,服务消费者可以通过注册中心得到已注册服务的名单,然后通过这些名单上的服务实例名,通过Feign和Ribbon来完成远程调用。
  • 失效剔除: 也就是停机,一段时间内你不缴费,一直欠费就会电信运行商就会停你机。默认每隔一段时间(默认为60秒) 将当前清单中超时(默认为90秒)没有续约的服务剔除出去。
  • 自我保护机制: 注册中心这么重要,我肯定不能只部署一个啊,要是这个崩了怎么办,所以我们就要搞分布式集群啊。但是我们知道,一旦涉及到分布式,我们就必然要去选择,Eureka选择的是AP,分区容错性必然存在,因为你总无法保证网络是绝对可靠的,A即为可用性,也就是说Eureka是不能保证强一致性的,为了保证A,Eureka引入了自我保护机制,自我保护模式正是一种针对网络异常波动的安全保护措施,使用自我保护模式能使Eureka集群更加的健壮、稳定的运行。
  • 自我保护机制是指如果在15分钟内超过85%的节点都没有正常的心跳,那么此时Eureka就认为此时节点到注册中心出现了网络故障,此时Eureka进入自我保护机制,可能会出现下面几种情况:
+ Eureka Server不再从注册列表中移除因为长时间没收到心跳而应该过期的服务,等待网络恢复稳定状态。
+ Eureka Server仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上,保证当前节点依然可用。
+ 当网络恢复到稳定状态时,新的注册信息会被同步到其它节点中。

同电信网络一样,你仅仅接入到电信网络还是不行的,你还需要通信的工具,电话或者计算机,才能和别人进行通信,那么服务之间的电话就是Feign和Ribbon

Eureka 注册中心参考的资料:

  • Spring Cloud Eureka 自我保护机制
  • Spring cloud Eureka 自我保护机制的官方解释
  • spring cloud 搭建注册中心Eureka(集群模式)
  • 外行人都能看懂的SpringCloud,错过了血亏!

远程调用Feign 和 Ribbon

feign 完成服务之间的相互调用

按照Spring Boot的常规操作,我们首先还是引入依赖:

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

然后写对应的配置,像上面一样:

1
2
3
4
5
6
7
8
9
java复制代码Spring:
application:
name: cityclient
server:
port: 8081
eureka:
client:
service-url:
defaultZone: http://localhost:9991/eureka/

启动类上打上对应的注解:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码SpringBootApplication
// 该服务是Eureka的客户端,仅限于Eureka
@EnableEurekaClient
// 注册中心有许多替代品,该注解无缝适应其他注册中心
@EnableDiscoveryClient
// 表明此服务要进行远程调用
@EnableFeignClients
public class MicroEurekaClient {
public static void main(String[] args) {
SpringApplication.run(MicroEurekaClient.class,args);
}
}

我该怎么调用其他服务,本次样例中我准备调用的是city服务:

1
2
3
4
5
6
java复制代码@FeignClient(value = "city") // 服务名
public interface CityClient {
// 对应的url
@GetMapping("cityTest")
String select();
}

然后在对应的控制层注入即可。

Ribbon完成服务之间的相互调用

Ribbon是通过RestTemplate完成服务的相互调用,我们首先要将RestTemplate装入IOC容器中。
然后在对应的控制器调用即可:

1
2
3
4
5
6
7
java复制代码   @Autowired
private RestTemplate restTemplate;
@GetMapping("testRibbon")
public void testRibbon(){
String result = restTemplate.getForObject("http://city/cityTest", String.class);
System.out.println(result);
}

负载均衡 Ribbon

单个服务应付的请求是有效的,为了应付多出来的请求,我们就可以搞搞集群,也就是说相同的服务我再跑一个,此时相同的服务就有两个了,那请求过来,应该让哪个服务处理请求呢? 这也就是负载均衡算法和负载均衡器。
负载均衡有两种类型:

  • 客户端负载均衡 ribbon
    服务清单在Eureka,客户端从Eureka拿到清单,在发起调用时,由Ribbon根据负载均衡算法决定调用哪个服务。
  • 服务端负载均衡 (Nginx)
    相同的服务实例有两个,由Nginx决定此次的请求由哪个服务承接。
    常用的几种负载均衡算法简介:
  • 轮询(Round Robin)法 也就是按请求顺序轮流分配,简单的说你可以理解为排队。
  • 随机法 随机抽取一名幸运的服务来响应请求。
  • 加权轮询 给每一台服务赋予权重,配置高的优先响应请求。

Ribbon默认采取轮询算法,可以自定义,实现起来也很简单: 继承AbstractLoadBalancerRule类重写choose方法即可。

熔断机制 Hystrix

我们解决完了服务的调用和负载均衡之后,很快会引入新的问题,在服务之间的相互调用中,某个服务因为某些原因不可用了或者响应时间过长怎么办?

就比如在双十一的时候,是让用户一直等着抢购结果,还是在一段时间内没抢购成功,就返回一个失效呢? 人是厌倦等待的,相信各位已经有了答案,就是返回一个失败结果。这是能够提升用户体验的。这就是熔断器Hystrix的作用,

怎么和Feign结合在一起呢? 我们通常用Feign完成服务调用,

1
2
3
4
5
6
7
8
java复制代码// value 指定调用哪个服务
// fallback 指定由哪个类返回快速失败结果
@FeignClient(value = "city", fallback = CityClientImpl.class) //
public interface CityClient {
// 客户端的地址
@GetMapping("cityTest")
String select();
}
1
2
3
4
5
6
7
java复制代码@Component
public class CityClientImpl implements CityClient {
@Override
public String select() {
return "调用失败";
}
}

Hystrix 仪表盘

Hystrix仪表盘,就像汽车的仪表盘一样,实时显示Hystrix的实时运行状态,通过Hystrix仪表盘我们可以看到Hystrix的各项指标信息,从而快速的发现系统中存在的问题。

启动时的页面:

之后的页面:
) )

路由网关 zuul

到现在我们的系统结构已经基本成型了,拥有下面这样的结构,

但是我们的系统还有以下问题:

  • 服务端向外部暴露的URL是真实的,外部通过这些URL来发起攻击。为了保护服务的安全,我们可以像滴滴一样,司机在接到订单式拿到的客户的电话是虚拟的,保护客户的安全,同样的我们暴露给外部的URL也可以是虚拟的。
  • 安全校验、登陆校验冗余: 为了保证系统安全,微服务的各个模块都会做一定的校验,但是微服务是独立的,我们不得不在每个微服务模块都做一套校验。显然这是冗余的。
  • 服务实例和负载均衡严重耦合: 外层的负载均衡器Nginx需要维护所有的服务清单,也就是ip+端口+服务名。哪天我的IP和服务划分发生了变化,我就需要去Nginx改。

由此引出API网关:,解决了以上问题。网关提供:

  • URL遮蔽,向外部暴露的是虚拟URL,网关会将虚拟的URL映射到真实的上
  • 统一校验,减少校验冗余
  • 接触服务实例和负载均衡的耦合,相当于服务实例和Nginx加了个中间层,Nginx不直接访问实例。

悄悄的说一句,Zuul也可以做负载均衡,那Nginx还要不要了? 网关也是要搞集群的,也要搞负载均衡,所以Nginx是可以搭配zuul使用的。

有了这层后,我们的架构就变成了这样:

配置中心 Spring Cloud config

目前为止我们的架构已经趋向完善,但是还有一点不完美的地方,微服务架构将我们的软件拆成一个一个的服务,每个服务都有着对应的配置文件,当服务变多,配置文件就变得难以管理,因为它分散在各个服务中,而且这些配置文件具备共性,那我们自然会有这样的需求,能否统一来管理微服务的配置文件呢? 这就是配置中心的用处,Spring Cloud为我们提供了 config组件,也就是配置中心。

config组件的配置中心的思想是这样的: 配置文件集中放在github、gitee等代码托管网站的仓库上,配置中心和这个仓库直连,每个服务在配置文件中指明配置中心和配置文件名,也就是向配置文件请求资源,然后由配置中心将对应的文件推给对应的服务。

总结

到目前为止Spring Cloud的组件之间竞争是十分激烈的,注册中心这块有Eureka和nacos,网关也有Spring Cloud官方也出了个gateway。但是思想还是没有变,拆分模块,,把握思想就好。

参考资料:

  • 每天学点SpringCloud(十一):Hystrix仪表盘
  • Spring Cloud中Hystrix仪表盘与Turbine集群监控
  • springboot2.0下hystrix dashboard Unable to connect to Command Metric Stream解决办法
  • SpringCloud-Ribbon(自定义负载均衡算法)
  • 外行人都能看懂的SpringCloud,错过了血亏!

本文转载自: 掘金

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

万字图解Java多线程 前言 什么是java多线程? 为什么

发表于 2020-09-06

前言

java多线程我个人觉得是javaSe中最难的一部分,我以前也是感觉学会了,但是真正有多线程的需求却不知道怎么下手,实际上还是对多线程这块知识了解不深刻,不知道多线程api的应用场景,不知道多线程的运行流程等等,本篇文章将使用实例+图解+源码的方式来解析java多线程。

文章篇幅较长,大家也可以有选择的看具体章节,建议多线程的代码全部手敲,永远不要相信你看到的结论,自己编码后运行出来的,才是自己的。

什么是java多线程?

进程与线程

进程

  • 当一个程序被运行,就开启了一个进程, 比如启动了qq,word
  • 程序由指令和数据组成,指令要运行,数据要加载,指令被cpu加载运行,数据被加载到内存,指令运行时可由cpu调度硬盘、网络等设备

线程

  • 一个进程内可分为多个线程
  • 一个线程就是一个指令流,cpu调度的最小单位,由cpu一条一条执行指令

并行与并发

并发:单核cpu运行多线程时,时间片进行很快的切换。线程轮流执行cpu

并行:多核cpu运行 多线程时,真正的在同一时刻运行

java提供了丰富的api来支持多线程。

为什么用多线程?

多线程能实现的都可以用单线程来完成,那单线程运行的好好的,为什么java要引入多线程的概念呢?

多线程的好处:

  1. 程序运行的更快!快!快!
  2. 充分利用cpu资源,目前几乎没有线上的cpu是单核的,发挥多核cpu强大的能力

多线程难在哪里?

单线程只有一条执行线,过程容易理解,可以在大脑中清晰的勾勒出代码的执行流程

多线程却是多条线,而且一般多条线之间有交互,多条线之间需要通信,一般难点有以下几点

  1. 多线程的执行结果不确定,受到cpu调度的影响
  2. 多线程的安全问题
  3. 线程资源宝贵,依赖线程池操作线程,线程池的参数设置问题
  4. 多线程执行是动态的,同时的,难以追踪过程
  5. 多线程的底层是操作系统层面的,源码难度大

有时候希望自己变成一个字节穿梭于服务器中,搞清楚来龙去脉,就像无敌破坏王一样(没看过这部电影的可以看下,脑洞大开)。

java多线程的基本使用

定义任务、创建和运行线程

任务: 线程的执行体。也就是我们的核心代码逻辑

定义任务

  1. 继承Thread类 (可以说是 将任务和线程合并在一起)
  2. 实现Runnable接口 (可以说是 将任务和线程分开了)
  3. 实现Callable接口 (利用FutureTask执行任务)

Thread实现任务的局限性

  1. 任务逻辑写在Thread类的run方法中,有单继承的局限性
  2. 创建多线程时,每个任务有成员变量时不共享,必须加static才能做到共享

Runnable和Callable解决了Thread的局限性

但是Runbale相比Callable有以下的局限性

  1. 任务没有返回值
  2. 任务无法抛异常给调用方

如下代码 几种定义线程的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Slf4j
class T extends Thread {
@Override
public void run() {
log.info("我是继承Thread的任务");
}
}
@Slf4j
class R implements Runnable {

@Override
public void run() {
log.info("我是实现Runnable的任务");
}
}
@Slf4j
class C implements Callable<String> {

@Override
public String call() throws Exception {
log.info("我是实现Callable的任务");
return "success";
}
}

创建线程的方式

  1. 通过Thread类直接创建线程
  2. 利用线程池内部创建线程

启动线程的方式

  • 调用线程的start()方法
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复制代码// 启动继承Thread类的任务
new T().start();

// 启动继承Thread匿名内部类的任务 可用lambda优化
Thread t = new Thread(){
@Override
public void run() {
log.info("我是Thread匿名内部类的任务");
}
};

// 启动实现Runnable接口的任务
new Thread(new R()).start();

// 启动实现Runnable匿名实现类的任务
new Thread(new Runnable() {
@Override
public void run() {
log.info("我是Runnable匿名内部类的任务");
}
}).start();

// 启动实现Runnable的lambda简化后的任务
new Thread(() -> log.info("我是Runnable的lambda简化后的任务")).start();

// 启动实现了Callable接口的任务 结合FutureTask 可以获取线程执行的结果
FutureTask<String> target = new FutureTask<>(new C());
new Thread(target).start();
log.info(target.get());

以上各个线程相关的类的类图如下

上下文切换

多核cpu下,多线程是并行工作的,如果线程数多,单个核又会并发的调度线程,运行时会有上下文切换的概念

cpu执行线程的任务时,会为线程分配时间片,以下几种情况会发生上下文切换。

  1. 线程的cpu时间片用完
  2. 垃圾回收
  3. 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当发生上下文切换时,操作系统会保存当前线程的状态,并恢复另一个线程的状态,jvm中有块内存地址叫程序计数器,用于记录线程执行到哪一行代码,是线程私有的。

idea打断点的时候可以设置为Thread模式,idea的debug模式可以看出栈帧的变化

线程的礼让-yield()&线程的优先级

yield()方法会让运行中的线程切换到就绪状态,重新争抢cpu的时间片,争抢时是否获取到时间片看cpu的分配。

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码// 方法的定义
public static native void yield();

Runnable r1 = () -> {
int count = 0;
for (;;){
log.info("---- 1>" + count++);
}
};
Runnable r2 = () -> {
int count = 0;
for (;;){
Thread.yield();
log.info(" ---- 2>" + count++);
}
};
Thread t1 = new Thread(r1,"t1");
Thread t2 = new Thread(r2,"t2");
t1.start();
t2.start();

// 运行结果
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129504
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129505
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129506
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129507
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129508
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129509
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129510
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129511
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129512
11:49:15.798 [t2] INFO thread.TestYield - ---- 2>293
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129513
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129514
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129515
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129516
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129517
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129518

如上述结果所示,t2线程每次执行时进行了yield(),线程1执行的机会明显比线程2要多。

线程的优先级

​ 线程内部用1~10的数来调整线程的优先级,默认的线程优先级为NORM_PRIORITY:5

​ cpu比较忙时,优先级高的线程获取更多的时间片

​ cpu比较闲时,优先级设置基本没用

1
2
3
4
5
6
7
8
9
java复制代码 public final static int MIN_PRIORITY = 1;

public final static int NORM_PRIORITY = 5;

public final static int MAX_PRIORITY = 10;

// 方法的定义
public final void setPriority(int newPriority) {
}

cpu比较忙时

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复制代码Runnable r1 = () -> {
int count = 0;
for (;;){
log.info("---- 1>" + count++);
}
};
Runnable r2 = () -> {
int count = 0;
for (;;){
log.info(" ---- 2>" + count++);
}
};
Thread t1 = new Thread(r1,"t1");
Thread t2 = new Thread(r2,"t2");
t1.setPriority(Thread.NORM_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();

// 可能的运行结果
11:59:00.696 [t1] INFO thread.TestYieldPriority - ---- 1>44102
11:59:00.696 [t2] INFO thread.TestYieldPriority - ---- 2>135903
11:59:00.696 [t2] INFO thread.TestYieldPriority - ---- 2>135904
11:59:00.696 [t2] INFO thread.TestYieldPriority - ---- 2>135905
11:59:00.696 [t2] INFO thread.TestYieldPriority - ---- 2>135906

cpu比较闲时

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复制代码Runnable r1 = () -> {
int count = 0;
for (int i = 0; i < 10; i++) {
log.info("---- 1>" + count++);
}
};
Runnable r2 = () -> {
int count = 0;
for (int i = 0; i < 10; i++) {
log.info(" ---- 2>" + count++);

}
};
Thread t1 = new Thread(r1,"t1");
Thread t2 = new Thread(r2,"t2");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();

// 可能的运行结果 线程1优先级低 却先运行完
12:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>7
12:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>8
12:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>9
12:01:09.916 [t2] INFO thread.TestYieldPriority - ---- 2>2
12:01:09.916 [t2] INFO thread.TestYieldPriority - ---- 2>3
12:01:09.916 [t2] INFO thread.TestYieldPriority - ---- 2>4
12:01:09.916 [t2] INFO thread.TestYieldPriority - ---- 2>5
12:01:09.916 [t2] INFO thread.TestYieldPriority - ---- 2>6
12:01:09.916 [t2] INFO thread.TestYieldPriority - ---- 2>7
12:01:09.916 [t2] INFO thread.TestYieldPriority - ---- 2>8
12:01:09.916 [t2] INFO thread.TestYieldPriority - ---- 2>9

守护线程

默认情况下,java进程需要等待所有线程都运行结束,才会结束,有一种特殊线程叫守护线程,当所有的非守护线程都结束后,即使它没有执行完,也会强制结束。

默认的线程都是非守护线程。

垃圾回收线程就是典型的守护线程

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// 方法的定义
public final void setDaemon(boolean on) {
}

Thread thread = new Thread(() -> {
while (true) {
}
});
// 具体的api。设为true表示未守护线程,当主线程结束后,守护线程也结束。
// 默认是false,当主线程结束后,thread继续运行,程序不停止
thread.setDaemon(true);
thread.start();
log.info("结束");

线程的阻塞

线程的阻塞可以分为好多种,从操作系统层面和java层面阻塞的定义可能不同,但是广义上使得线程阻塞的方式有下面几种

  1. BIO阻塞,即使用了阻塞式的io流
  2. sleep(long time) 让线程休眠进入阻塞状态
  3. a.join() 调用该方法的线程进入阻塞,等待a线程执行完恢复运行
  4. sychronized或ReentrantLock 造成线程未获得锁进入阻塞状态 (同步锁章节细说)
  5. 获得锁之后调用wait()方法 也会让线程进入阻塞状态 (同步锁章节细说)
  6. LockSupport.park() 让线程进入阻塞状态 (同步锁章节细说)

sleep()

​ 使线程休眠,会将运行中的线程进入阻塞状态。当休眠时间结束后,重新争抢cpu的时间片继续运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码// 方法的定义 native方法
public static native void sleep(long millis) throws InterruptedException;

try {
// 休眠2秒
// 该方法会抛出 InterruptedException异常 即休眠过程中可被中断,被中断后抛出异常
Thread.sleep(2000);
} catch (InterruptedException异常 e) {
}
try {
// 使用TimeUnit的api可替代 Thread.sleep
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}

join()

​ join是指调用该方法的线程进入阻塞状态,等待某线程执行完成后恢复运行

1
2
3
4
5
6
7
java复制代码// 方法的定义 有重载
// 等待线程执行完才恢复运行
public final void join() throws InterruptedException {
}
// 指定join的时间。指定时间内 线程还未执行完 调用方线程不继续等待就恢复运行
public final synchronized void join(long millis)
throws InterruptedException{}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码Thread t = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r = 10;
});

t.start();
// 让主线程阻塞 等待t线程执行完才继续执行
// 去除该行,执行结果为0,加上该行 执行结果为10
t.join();
log.info("r:{}", r);

// 运行结果
13:09:13.892 [main] INFO thread.TestJoin - r:10

线程的打断-interrupt()

1
2
3
4
5
6
7
java复制代码// 相关方法的定义
public void interrupt() {
}
public boolean isInterrupted() {
}
public static boolean interrupted() {
}

打断标记:线程是否被打断,true表示被打断了,false表示没有

isInterrupted() 获取线程的打断标记 ,调用后不会修改线程的打断标记

interrupt()方法用于中断线程

  1. 可以打断sleep,wait,join等显式的抛出InterruptedException方法的线程,但是打断后,线程的打断标记还是false
  2. 打断正常线程 ,线程不会真正被中断,但是线程的打断标记为true

interrupted() 获取线程的打断标记,调用后清空打断标记 即如果获取为true 调用后打断标记为false (不常用)

interrupt实例: 有个后台监控线程不停的监控,当外界打断它时,就结束运行。代码如下

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
java复制代码@Slf4j
class TwoPhaseTerminal{
// 监控线程
private Thread monitor;

public void start(){
monitor = new Thread(() ->{
// 不停的监控
while (true){
Thread thread = Thread.currentThread();
// 判断当前线程是否被打断
if (thread.isInterrupted()){
log.info("当前线程被打断,结束运行");
break;
}
try {
Thread.sleep(1000);
// 监控逻辑中被打断后,打断标记为true
log.info("监控");
} catch (InterruptedException e) {
// 睡眠时被打断时抛出异常 在该处捕获到 此时打断标记还是false
// 在调用一次中断 使得中断标记为true
thread.interrupt();
}
}
});
monitor.start();
}

public void stop(){
monitor.interrupt();
}
}

线程的状态

上面说了一些基本的api的使用,调用上面的方法后都会使得线程有对应的状态。

线程的状态可从 操作系统层面分为五种状态 从java api层面分为六种状态。

五种状态

  1. 初始状态:创建线程对象时的状态
  2. 可运行状态(就绪状态):调用start()方法后进入就绪状态,也就是准备好被cpu调度执行
  3. 运行状态:线程获取到cpu的时间片,执行run()方法的逻辑
  4. 阻塞状态: 线程被阻塞,放弃cpu的时间片,等待解除阻塞重新回到就绪状态争抢时间片
  5. 终止状态: 线程执行完成或抛出异常后的状态

六种状态

Thread类中的内部枚举State

1
2
3
4
5
6
7
8
java复制代码public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
  1. NEW 线程对象被创建
  2. Runnable 线程调用了start()方法后进入该状态,该状态包含了三种情况
    1. 就绪状态 :等待cpu分配时间片
    2. 运行状态:进入Runnable方法执行任务
    3. 阻塞状态:BIO 执行阻塞式io流时的状态
  3. Blocked 没获取到锁时的阻塞状态(同步锁章节会细说)
  4. WAITING 调用wait()、join()等方法后的状态
  5. TIMED_WAITING 调用 sleep(time)、wait(time)、join(time)等方法后的状态
  6. TERMINATED 线程执行完成或抛出异常后的状态

六种线程状态和方法的对应关系

线程的相关方法总结

主要总结Thread类中的核心方法

方法名称 是否static 方法说明
start() 否 让线程启动,进入就绪状态,等待cpu分配时间片
run() 否 重写Runnable接口的方法,线程获取到cpu时间片时执行的具体逻辑
yield() 是 线程的礼让,使得获取到cpu时间片的线程进入就绪状态,重新争抢时间片
sleep(time) 是 线程休眠固定时间,进入阻塞状态,休眠时间完成后重新争抢时间片,休眠可被打断
join()/join(time) 否 调用线程对象的join方法,调用者线程进入阻塞,等待线程对象执行完或者到达指定时间才恢复,重新争抢时间片
isInterrupted() 否 获取线程的打断标记,true:被打断,false:没有被打断。调用后不会修改打断标记
interrupt() 否 打断线程,抛出InterruptedException异常的方法均可被打断,但是打断后不会修改打断标记,正常执行的线程被打断后会修改打断标记
interrupted() 否 获取线程的打断标记。调用后会清空打断标记
stop() 否 停止线程运行 不推荐
suspend() 否 挂起线程 不推荐
resume() 否 恢复线程运行 不推荐
currentThread() 是 获取当前线程

Object中与线程相关方法

方法名称 方法说明
wait()/wait(long timeout) 获取到锁的线程进入阻塞状态
notify() 随机唤醒被wait()的一个线程
notifyAll(); 唤醒被wait()的所有线程,重新争抢时间片

同步锁

线程安全

  • 一个程序运行多个线程本身是没有问题的
  • 问题有可能出现在多个线程访问共享资源
    • 多个线程都是读共享资源也是没有问题的
    • 当多个线程读写共享资源时,如果发生指令交错,就会出现问题

临界区: 一段代码如果对共享资源的多线程读写操作,这段代码就被称为临界区。

注意的是 指令交错指的是 java代码在解析成字节码文件时,java代码的一行代码在字节码中可能有多行,在线程上下文切换时就有可能交错。

线程安全指的是多线程调用同一个对象的临界区的方法时,对象的属性值一定不会发生错误,这就是保证了线程安全。

如下面不安全的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码// 对象的成员变量
private static int count = 0;

public static void main(String[] args) throws InterruptedException {
// t1线程对变量+5000次
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count++;
}
});
// t2线程对变量-5000次
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count--;
}
});

t1.start();
t2.start();

// 让t1 t2都执行完
t1.join();
t2.join();
System.out.println(count);
}

// 运行结果
-1399

上面的代码 两个线程,一个+5000次,一个-5000次,如果线程安全,count的值应该还是0。

但是运行很多次,每次的结果不同,且都不是0,所以是线程不安全的。

线程安全的类一定所有的操作都线程安全吗?

开发中经常会说到一些线程安全的类,如ConcurrentHashMap,线程安全指的是类里每一个独立的方法是线程安全的,但是方法的组合就不一定是线程安全的。

成员变量和静态变量是否线程安全?

  • 如果没有多线程共享,则线程安全
  • 如果存在多线程共享
    • 多线程只有读操作,则线程安全
    • 多线程存在写操作,写操作的代码又是临界区,则线程不安全

局部变量是否线程安全?

  • 局部变量是线程安全的
  • 局部变量引用的对象未必是线程安全的
    • 如果该对象没有逃离该方法的作用范围,则线程安全
    • 如果该对象逃离了该方法的作用范围,比如:方法的返回值,需要考虑线程安全

synchronized

同步锁也叫对象锁,是锁在对象上的,不同的对象就是不同的锁。

该关键字是用于保证线程安全的,是阻塞式的解决方案。

让同一个时刻最多只有一个线程能持有对象锁,其他线程在想获取这个对象锁就会被阻塞,不用担心上下文切换的问题。

注意: 不要理解为一个线程加了锁 ,进入 synchronized代码块中就会一直执行下去。如果时间片切换了,也会执行其他线程,再切换回来会紧接着执行,只是不会执行到有竞争锁的资源,因为当前线程还未释放锁。

当一个线程执行完synchronized的代码块后 会唤醒正在等待的线程

synchronized实际上使用对象锁保证临界区的原子性 临界区的代码是不可分割的 不会因为线程切换所打断

基本使用

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复制代码// 加在方法上 实际是对this对象加锁
private synchronized void a() {
}

// 同步代码块,锁对象可以是任意的,加在this上 和a()方法作用相同
private void b(){
synchronized (this){

}
}

// 加在静态方法上 实际是对类对象加锁
private synchronized static void c() {

}

// 同步代码块 实际是对类对象加锁 和c()方法作用相同
private void d(){
synchronized (TestSynchronized.class){

}
}

// 上述b方法对应的字节码源码 其中monitorenter就是加锁的地方
0 aload_0
1 dup
2 astore_1
3 monitorenter
4 aload_1
5 monitorexit
6 goto 14 (+8)
9 astore_2
10 aload_1
11 monitorexit
12 aload_2
13 athrow
14 return

线程安全的代码

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
java复制代码private static int count = 0;

private static Object lock = new Object();

private static Object lock2 = new Object();

// t1线程和t2对象都是对同一对象加锁。保证了线程安全。此段代码无论执行多少次,结果都是0
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
count++;
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
count--;
}
}
});

t1.start();
t2.start();

// 让t1 t2都执行完
t1.join();
t2.join();
System.out.println(count);
}

重点:加锁是加在对象上,一定要保证是同一对象,加锁才能生效

线程通信

wait+notify

线程间通信可以通过共享变量+wait()&notify()来实现

wait()将线程进入阻塞状态,notify()将线程唤醒

当多线程竞争访问对象的同步方法时,锁对象会关联一个底层的Monitor对象(重量级锁的实现)

如下图所示 Thread0,1先竞争到锁执行了代码后,2,3,4,5线程同时来执行临界区的代码,开始竞争锁

  1. Thread-0先获取到对象的锁,关联到monitor的owner,同步代码块内调用了锁对象的wait()方法,调用后会进入waitSet等待,Thread-1同样如此,此时Thread-0的状态为Waitting
  2. Thread2、3、4、5同时竞争,2获取到锁后,关联了monitor的owner,3、4、5只能进入EntryList中等待,此时2线程状态为 Runnable,3、4、5状态为Blocked
  3. 2执行后,唤醒entryList中的线程,3、4、5进行竞争锁,获取到的线程即会关联monitor的owner
  4. 3、4、5线程在执行过程中,调用了锁对象的notify()或notifyAll()时,会唤醒waitSet的线程,唤醒的线程进入entryList等待重新竞争锁

注意:

  1. Blocked状态和Waitting状态都是阻塞状态
  2. Blocked线程会在owner线程释放锁时唤醒
  3. wait和notify使用场景是必须要有同步,且必须获得对象的锁才能调用,使用锁对象去调用,否则会抛异常
  • wait() 释放锁 进入 waitSet 可传入时间,如果指定时间内未被唤醒 则自动唤醒
  • notify()随机唤醒一个waitSet里的线程
  • notifyAll()唤醒waitSet中所有的线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
java复制代码static final Object lock = new Object();
new Thread(() -> {
synchronized (lock) {
log.info("开始执行");
try {
// 同步代码内部才能调用
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("继续执行核心逻辑");
}
}, "t1").start();

new Thread(() -> {
synchronized (lock) {
log.info("开始执行");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("继续执行核心逻辑");
}
}, "t2").start();

try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("开始唤醒");

synchronized (lock) {
// 同步代码内部才能调用
lock.notifyAll();
}
// 执行结果
14:29:47.138 [t1] INFO TestWaitNotify - 开始执行
14:29:47.141 [t2] INFO TestWaitNotify - 开始执行
14:29:49.136 [main] INFO TestWaitNotify - 开始唤醒
14:29:49.136 [t2] INFO TestWaitNotify - 继续执行核心逻辑
14:29:49.136 [t1] INFO TestWaitNotify - 继续执行核心逻辑

wait 和 sleep的区别?

二者都会让线程进入阻塞状态,有以下区别

  1. wait是Object的方法 sleep是Thread的方法
  2. wait会立即释放锁 sleep不会释放锁
  3. wait后线程的状态是Watting sleep后线程的状态为 Time_Waiting

park&unpark

LockSupport是juc下的工具类,提供了park和unpark方法,可以实现线程通信

与wait和notity相比的不同点

  1. wait 和notify需要获取对象锁 park unpark不要
  2. unpark 可以指定唤醒线程 notify随机唤醒
  3. park和unpark的顺序可以先unpark wait和notify的顺序不能颠倒

生产者消费者模型

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
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
java复制代码  public static void main(String[] args) throws InterruptedException {
MessageQueue queue = new MessageQueue(2);

// 三个生产者向队列里存值
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
queue.put(new Message(id, "值" + id));
}, "生产者" + i).start();
}

Thread.sleep(1000);

// 一个消费者不停的从队列里取值
new Thread(() -> {
while (true) {
queue.take();
}
}, "消费者").start();

}
}


// 消息队列被生产者和消费者持有
class MessageQueue {
private LinkedList<Message> list = new LinkedList<>();

// 容量
private int capacity;

public MessageQueue(int capacity) {
this.capacity = capacity;
}

/**
* 生产
*/
public void put(Message message) {
synchronized (list) {
while (list.size() == capacity) {
log.info("队列已满,生产者等待");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.addLast(message);
log.info("生产消息:{}", message);
// 生产后通知消费者
list.notifyAll();
}
}

public Message take() {
synchronized (list) {
while (list.isEmpty()) {
log.info("队列已空,消费者等待");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Message message = list.removeFirst();
log.info("消费消息:{}", message);
// 消费后通知生产者
list.notifyAll();
return message;
}
}


}
// 消息
class Message {

private int id;

private Object value;
}

同步锁案例

为了更形象的表达加同步锁的概念,这里举一个生活中的例子,尽量把以上的概念具体化出来。

这里举一个每个人非常感兴趣的一件东西。 钱!!!(马老师除外)。

现实中,我们去银行门口的自动取款机取钱,取款机的钱就是共享变量,为了保障安全,不可能两个陌生人同时进入同一个取款机内取钱,所以只能一个人进入取钱,然后锁上取款机的门,其他人只能在取款机门口等待。

取款机有多个,里面的钱互不影响,锁也有多个(多个对象锁),取钱人在多个取款机里同时取钱也没有安全问题。

假如每个取钱的陌生人都是线程,当取钱人进入取款机锁了门后(线程获得锁),取到钱后出门(线程释放锁),下一个人竞争到锁来取钱。

假设工作人员也是一个线程,如果取钱人进入后发现取款机钱不足了,这时通知工作人员来向取款机里加钱(调用notifyAll方法),取钱人暂停取钱,进入银行大堂阻塞等待(调用wait方法)。

银行大堂里的工作人员和取钱人都被唤醒,重新竞争锁,进入后如果是取钱人,由于取款机没钱,还得进入银行大堂等待。

当工作人员获得取款机的锁进入后,加了钱后会通知大厅里的人来取钱(调用notifyAll方法)。自己暂停加钱,进入银行大堂等待唤醒加钱(调用wait方法)。

这时大堂里等待的人都来竞争锁,谁获取到谁进入继续取钱。

和现实中不同的就是这里没有排队的概念,谁抢到锁谁进去取。

ReentrantLock

可重入锁 : 一个线程获取到对象的锁后,执行方法内部在需要获取锁的时候是可以获取到的。如以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码private static final ReentrantLock LOCK = new ReentrantLock();

private static void m() {
LOCK.lock();
try {
log.info("begin");
// 调用m1()
m1();
} finally {
// 注意锁的释放
LOCK.unlock();
}
}
public static void m1() {
LOCK.lock();
try {
log.info("m1");
m2();
} finally {
// 注意锁的释放
LOCK.unlock();
}
}

synchronized 也是可重入锁,ReentrantLock有以下优点

  1. 支持获取锁的超时时间
  2. 获取锁时可被打断
  3. 可设为公平锁
  4. 可以有不同的条件变量,即有多个waitSet,可以指定唤醒

api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码// 默认非公平锁,参数传true 表示未公平锁
ReentrantLock lock = new ReentrantLock(false);
// 尝试获取锁
lock()
// 释放锁 应放在finally块中 必须执行到
unlock()
try {
// 获取锁时可被打断,阻塞中的线程可被打断
LOCK.lockInterruptibly();
} catch (InterruptedException e) {
return;
}
// 尝试获取锁 获取不到就返回false
LOCK.tryLock()
// 支持超时时间 一段时间没获取到就返回false
tryLock(long timeout, TimeUnit unit)
// 指定条件变量 休息室 一个锁可以创建多个休息室
Condition waitSet = ROOM.newCondition();
// 释放锁 进入waitSet等待 释放后其他线程可以抢锁
yanWaitSet.await()
// 唤醒具体休息室的线程 唤醒后 重写竞争锁
yanWaitSet.signal()

实例:一个线程输出a,一个线程输出b,一个线程输出c,abc按照顺序输出,连续输出5次

这个考的就是线程的通信,利用 wait()/notify()和控制变量可以实现,此处使用ReentrantLock即可实现该功能。

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
java复制代码  public static void main(String[] args) {
AwaitSignal awaitSignal = new AwaitSignal(5);
// 构建三个条件变量
Condition a = awaitSignal.newCondition();
Condition b = awaitSignal.newCondition();
Condition c = awaitSignal.newCondition();
// 开启三个线程
new Thread(() -> {
awaitSignal.print("a", a, b);
}).start();

new Thread(() -> {
awaitSignal.print("b", b, c);
}).start();

new Thread(() -> {
awaitSignal.print("c", c, a);
}).start();

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
awaitSignal.lock();
try {
// 先唤醒a
a.signal();
} finally {
awaitSignal.unlock();
}
}


}

class AwaitSignal extends ReentrantLock {

// 循环次数
private int loopNumber;

public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}

/**
* @param print 输出的字符
* @param current 当前条件变量
* @param next 下一个条件变量
*/
public void print(String print, Condition current, Condition next) {

for (int i = 0; i < loopNumber; i++) {
lock();
try {
try {
// 获取锁之后等待
current.await();
System.out.print(print);
} catch (InterruptedException e) {
}
next.signal();
} finally {
unlock();
}
}
}

死锁

说到死锁,先举个例子,

下面是代码实现

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复制代码static Beer beer = new Beer();
static Story story = new Story();

public static void main(String[] args) {
new Thread(() ->{
synchronized (beer){
log.info("我有酒,给我故事");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (story){
log.info("小王开始喝酒讲故事");
}
}
},"小王").start();

new Thread(() ->{
synchronized (story){
log.info("我有故事,给我酒");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (beer){
log.info("老王开始喝酒讲故事");
}
}
},"老王").start();
}
class Beer {
}

class Story{
}

死锁导致程序无法正常运行下去

检测工具可以检查到死锁信息

java内存模型(JMM)

jmm 体现在以下三个方面

  1. 原子性 保证指令不会受到上下文切换的影响
  2. 可见性 保证指令不会受到cpu缓存的影响
  3. 有序性 保证指令不会受并行优化的影响

可见性

停不下来的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码static boolean run = true;

public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (run) {
// ....
}
});
t.start();
Thread.sleep(1000);
// 线程t不会如预想的停下来
run = false;
}

如上图所示,线程有自己的工作缓存,当主线程修改了变量并同步到主内存时,t线程没有读取到,所以程序停不下来

有序性

JVM在不影响程序正确性的情况下可能会调整语句的执行顺序,该情况也称为 指令重排序

1
2
3
4
5
6
java复制代码  static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...;
j = ...;
有可能将j先赋值

原子性

原子性大家应该比较熟悉,上述同步锁的synchronized代码块就是保证了原子性,就是一段代码是一个整体,原子性保证了线程安全,不会受到上下文切换的影响。

volatile

该关键字解决了可见性和有序性,volatile通过内存屏障来实现的

  • 写屏障

会在对象写操作之后加写屏障,会对写屏障的之前的数据都同步到主存,并且保证写屏障的执行顺序在写屏障之前

  • 读屏障

会在对象读操作之前加读屏障,会在读屏障之后的语句都从主存读,并保证读屏障之后的代码执行在读屏障之后

注意: volatile不能解决原子性,即不能通过该关键字实现线程安全。

volatile应用场景:一个线程读取变量,另外的线程操作变量,加了该关键字后保证写变量后,读变量的线程可以及时感知。

无锁-cas

cas (compare and swap) 比较并交换

为变量赋值时,从内存中读取到的值v,获取到要交换的新值n,执行 compareAndSwap()方法时,比较v和当前内存中的值是否一致,如果一致则将n和v交换,如果不一致,则自旋重试。

cas底层是cpu层面的,即不使用同步锁也可以保证操作的原子性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码private AtomicInteger balance;

// 模拟cas的具体操作
@Override
public void withdraw(Integer amount) {
while (true) {
// 获取当前值
int pre = balance.get();
// 进行操作后得到新值
int next = pre - amount;
// 比较并设置成功 则中断 否则自旋重试
if (balance.compareAndSet(pre, next)) {
break;
}
}
}

无锁的效率是要高于之前的锁的,由于无锁不会涉及线程的上下文切换

cas是乐观锁的思想,sychronized是悲观锁的思想

cas适合很少有线程竞争的场景,如果竞争很强,重试经常发生,反而降低效率

juc并发包下包含了实现了cas的原子类

  1. AtomicInteger/AtomicBoolean/AtomicLong
  2. AtomicIntegerArray/AtomicLongArray/AtomicReferenceArray
  3. AtomicReference/AtomicStampedReference/AtomicMarkableReference

AtomicInteger

常用api

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码new AtomicInteger(balance)
get()
compareAndSet(pre, next)
// i.incrementAndGet() ++i
// i.decrementAndGet() --i
// i.getAndIncrement() i++
// i.getAndDecrement() ++i
i.addAndGet()
// 传入函数式接口 修改i
int getAndUpdate(IntUnaryOperator updateFunction)
// cas 的核心方法
compareAndSet(int expect, int update)

ABA问题

cas存在ABA问题,即比较并交换时,如果原值为A,有其他线程将其修改为B,在有其他线程将其修改为A。

此时实际发生过交换,但是比较和交换由于值没改变可以交换成功

解决方式

AtomicStampedReference/AtomicMarkableReference

上面两个类解决ABA问题,原理就是为对象增加版本号,每次修改时增加版本号,就可以避免ABA问题

或者增加个布尔变量标识,修改后调整布尔变量值,也可以避免ABA问题

线程池

线程池的介绍

线程池是java并发最重要的一个知识点,也是难点,是实际应用最广泛的。

线程的资源很宝贵,不可能无限的创建,必须要有管理线程的工具,线程池就是一种管理线程的工具,java开发中经常有池化的思想,如 数据库连接池、Redis连接池等。

预先创建好一些线程,任务提交时直接执行,既可以节约创建线程的时间,又可以控制线程的数量。

线程池的好处

  1. 降低资源消耗,通过池化思想,减少创建线程和销毁线程的消耗,控制资源
  2. 提高响应速度,任务到达时,无需创建线程即可运行
  3. 提供更多更强大的功能,可扩展性高

线程池的构造方法

1
2
3
4
5
6
7
8
9
java复制代码public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {

}

构造器参数的意义

参数名 参数意义
corePoolSize 核心线程数
maximumPoolSize 最大线程数
keepAliveTime 救急线程的空闲时间
unit 救急线程的空闲时间单位
workQueue 阻塞队列
threadFactory 创建线程的工厂,主要定义线程名
handler 拒绝策略

线程池案例

下面 我们通过一个实例来理解线程池的参数以及线程池的接收任务的过程

如上图 银行办理业务。

  1. 客户到银行时,开启柜台进行办理,柜台相当于线程,客户相当于任务,有两个是常开的柜台,三个是临时柜台。2就是核心线程数,5是最大线程数。即有两个核心线程
  2. 当柜台开到第二个后,都还在处理业务。客户再来就到排队大厅排队。排队大厅只有三个座位。
  3. 排队大厅坐满时,再来客户就继续开柜台处理,目前最大有三个临时柜台,也就是三个救急线程
  4. 此时再来客户,就无法正常为其 提供业务,采用拒绝策略来处理它们
  5. 当柜台处理完业务,就会从排队大厅取任务,当柜台隔一段空闲时间都取不到任务时,如果当前线程数大于核心线程数时,就会回收线程。即撤销该柜台。

线程池的状态

线程池通过一个int变量的高3位来表示线程池的状态,低29位来存储线程池的数量

状态名称 高三位 接收新任务 处理阻塞队列任务 说明
Running 111 Y Y 正常接收任务,正常处理任务
Shutdown 000 N Y 不会接收任务,会执行完正在执行的任务,也会处理阻塞队列里的任务
stop 001 N N 不会接收任务,会中断正在执行的任务,会放弃处理阻塞队列里的任务
Tidying 010 N N 任务全部执行完毕,当前活动线程是0,即将进入终结
Termitted 011 N N 终结状态
1
2
3
4
5
6
java复制代码// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

线程池的主要流程

线程池创建、接收任务、执行任务、回收线程的步骤

  1. 创建线程池后,线程池的状态是Running,该状态下才能有下面的步骤
  2. 提交任务时,线程池会创建线程去处理任务
  3. 当线程池的工作线程数达到corePoolSize时,继续提交任务会进入阻塞队列
  4. 当阻塞队列装满时,继续提交任务,会创建救急线程来处理
  5. 当线程池中的工作线程数达到maximumPoolSize时,会执行拒绝策略
  6. 当线程取任务的时间达到keepAliveTime还没有取到任务,工作线程数大于corePoolSize时,会回收该线程

注意: 不是刚创建的线程是核心线程,后面创建的线程是非核心线程,线程是没有核心非核心的概念的,这是我长期以来的误解。

拒绝策略

  1. 调用者抛出RejectedExecutionException (默认策略)
  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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
java复制代码// 执行Runnable
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
// 提交Callable
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
// 内部构建FutureTask
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
// 提交Runnable,指定返回值
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
// 内部构建FutureTask
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
// 提交Runnable,指定返回值
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
// 内部构建FutureTask
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}

Execetors创建线程池

注意: 下面几种方式都不推荐使用

1.newFixedThreadPool

1
2
3
4
5
java复制代码public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
  • 核心线程数 = 最大线程数 没有救急线程
  • 阻塞队列无界 可能导致oom

2.newCachedThreadPool

1
2
3
4
5
java复制代码public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
  • 核心线程数是0,最大线程数无限制 ,救急线程60秒回收
  • 队列采用 SynchronousQueue 实现 没有容量,即放入队列后没有线程来取就放不进去
  • 可能导致线程数过多,cpu负担太大

3.newSingleThreadExecutor

1
2
3
4
5
6
java复制代码public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
  • 核心线程数和最大线程数都是1,没有救急线程,无界队列 可以不停的接收任务
  • 将任务串行化 一个个执行, 使用包装类是为了屏蔽修改线程池的一些参数 比如 corePoolSize
  • 如果某线程抛出异常了,会重新创建一个线程继续执行
  • 可能造成oom

4.newScheduledThreadPool

1
2
3
java复制代码public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
  • 任务调度的线程池 可以指定延迟时间调用,可以指定隔一段时间调用

线程池的关闭

shutdown()

会让线程池状态为shutdown,不能接收任务,但是会将工作线程和阻塞队列里的任务执行完 相当于优雅关闭

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}

shutdownNow()

会让线程池状态为stop, 不能接收任务,会立即中断执行中的工作线程,并且不会执行阻塞队列里的任务, 会返回阻塞队列的任务列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}

线程池的正确使用姿势

线程池难就难在参数的配置,有一套理论配置参数

cpu密集型 : 指的是程序主要发生cpu的运算

​ 核心线程数: CPU核心数+1

IO密集型: 远程调用RPC,操作数据库等,不需要使用cpu进行大量的运算。 大多数应用的场景

​ 核心线程数=核数*cpu期望利用率 *总时间/cpu运算时间

但是基于以上理论还是很难去配置,因为cpu运算时间不好估算

实际配置大小可参考下表

cpu密集型 io密集型
线程数数量 核数<=x<=核数*2 核心数*50<=x<=核心数 *100
队列长度 y>=100 1<=y<=10

1.线程池参数通过分布式配置,修改配置无需重启应用

线程池参数是根据线上的请求数变化而变化的,最好的方式是 核心线程数、最大线程数 队列大小都是可配置的

主要配置 corePoolSize maxPoolSize queueSize

java提供了可方法覆盖参数,线程池内部会处理好参数 进行平滑的修改

1
2
java复制代码public void setCorePoolSize(int corePoolSize) {
}

2.增加线程池的监控

3.io密集型可调整为先新增任务到最大线程数后再将任务放到阻塞队列

代码 主要可重写阻塞队列 加入任务的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码public boolean offer(Runnable runnable) {
if (executor == null) {
throw new RejectedExecutionException("The task queue does not have executor!");
}

final ReentrantLock lock = this.lock;
lock.lock();
try {
int currentPoolThreadSize = executor.getPoolSize();

// 如果提交任务数小于当前创建的线程数, 说明还有空闲线程,
if (executor.getTaskCount() < currentPoolThreadSize) {
// 将任务放入队列中,让线程去处理任务
return super.offer(runnable);
}
// 核心改动
// 如果当前线程数小于最大线程数,则返回 false ,让线程池去创建新的线程
if (currentPoolThreadSize < executor.getMaximumPoolSize()) {
return false;
}

// 否则,就将任务放入队列中
return super.offer(runnable);
} finally {
lock.unlock();
}
}

3.拒绝策略 建议使用tomcat的拒绝策略(给一次机会)

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码// tomcat的源码
@Override
public void execute(Runnable command) {
if ( executor != null ) {
try {
executor.execute(command);
} catch (RejectedExecutionException rx) {
// 捕获到异常后 在从队列获取,相当于重试1取不到任务 在执行拒绝任务
if ( !( (TaskQueue) executor.getQueue()).force(command) ) throw new RejectedExecutionException("Work queue full.");
}
} else throw new IllegalStateException("StandardThreadPool not started.");
}

建议修改从队列取任务的方式: 增加超时时间,超时1分钟取不到在进行返回

1
java复制代码public boolean offer(E e, long timeout, TimeUnit unit){}

结语

工作三四年了,还没有正式的写过博客,自学一直都是通过笔记的方式积累,最近重新学了一下java多线程,想着周末把这部分内容认真的写篇博客分享出去。

文章篇幅较长,给看到这里的小伙伴点个大大的赞!由于作者水平有限,加之第一次写博客,文章中难免会有错误之处,欢迎小伙伴们反馈指正。

如果觉得文章对你有帮助,麻烦 点赞、评论、转发、在看 走起

你的支持是我最大的动力!!!

本文转载自: 掘金

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

学习 Java 最强书单推荐,附学习方法

发表于 2020-09-06

请肆无忌惮地点赞吧,微信搜索【沉默王二】关注这个在九朝古都洛阳苟且偷生的程序员。

本文 GitHub github.com/itwanger 已收录,里面还有我精心为你准备的一线大厂面试题。

回想起 10 年前我学习 Java 那会,资源极度匮乏,老师给我们推荐了两本书,一本《Java 编程思想》,一本《Java 开发宝典》。老师以为第一本书讲理论,第二本书讲实战,完美的组合。

但实际上呢,毕业的时候,班里面只有 13 名同学从事了软件开发的工作,其余 30 多名学生要么大一的时候转专业,要么很早就放弃编程了。

《Java 编程思想》是本经典的好书,没错,但作为入门书籍的话,只会劝退。《Java 开发宝典》更是劝退书,里面用 Java 做成的项目都很经典,比如说浏览器,五子棋,但小白根本驾驭不了啊!

10 年过去了,我已经拥有 10 多年的编程经验了。别问,问就是加班加出来的。那我在 Java 编程方面已经有了自己的一些心得和体会,所以我在知乎上推荐了一个书单,阅读人数超过了 66 万,点赞数 1200+,说明大家都很受用,都很喜欢啊。

地址贴一下,需要的同学可以去围观一下。

www.zhihu.com/question/26…

上面这个回答里面包含了 13 本经典的书籍,并且是按照循序渐进的方式,相信同学们看完后会来私信感谢我的。不过,这个回答已经过去了一年多时间了,我想再添加一些书单进去,仍然按照入门→进阶→深入的顺序。

01、入门

  • 《Java 核心技术卷 1》
  • 《Head First Java》

注意跳过 Swing、AWT、Applet 这些章节。当然了,你也可以看一下我写的这些 Java 教程,比较通俗易通一些,数了数,差不多 200 篇,还是非常全面给力的。

www.itwanger.com/java.html

  • 《鸟哥的 Linux 私房菜》

为什么要学 Linux 呢?因为在实际的开发工作中,项目基本上都要部署到 Linux 环境下。Windows 作为服务器的很少,除了慢没别的原因。

假如能够提前掌握一些 Linux 基本操作的话,不仅简历上是加分项,工作中更能快人一步。

  • 《Maven 实战》
  • 《Git 权威指南》

在我刚学 Java 那会,代码只能在本地,要想进行版本控制的话,还得装一个 SVN 服务器和客户端。另外,还不能云同步,换台电脑的话,基本上要把代码和项目依赖的 jar 包重新拷贝一遍。

要知道,代码都是很琐碎的文件,复制粘贴起来非常慢;如果项目比较大的话,依赖的 jar 包也比较多,复制粘贴起来就更慢了。

现在好了,有码云、GitHub 这些云仓库可以用,多台电脑进行同步非常方便。再配合 Maven(项目构建工具) 和 Git(版本控制工具),玩起来 666 啊!

02、进阶

  • 《Java 编程思想》

《Java 编程思想》这本书确实没得说,质量很高,但需要放在 Java 入门后再去读,这样才能真正地去理解思想。

很多没有足够编程基础的同学,盲目自信,直接开啃这本书,然后他们发现,啃得很痛苦,这就是因为没有根基的原因造成的,就像没有学会走就开始跑一样,摔得很痛。

作者在前言里已经表明了,读者需要知道基本的程序语句(比如 C里面的 if 和 for),也就是说,作者假设读者已经熟悉 C 的一些语法。可想而知,没有编程基础的同学在读这本书的时候是多么痛苦啊。

我就是在大学老师的误导下开啃这本书的,第一遍,只读了前言,就读不下去了,晦涩,难懂。实习了一年结束后,我第二次读这本书,仍然觉得头大,强忍着,读了差不多 200 页,耐力就消耗殆尽了。

直到两年前,我再重拾这本书,发现读起来津津有味,每读一个章节就产出一篇文章,这本书的价值真正地体现了。为什么呢?因为我已经有多年的编程经验,结合书中的理论知识,读起来就有一种“恍然大悟”的感觉。

《Java 编程思想》不仅教我们怎么做,还告诉我们为什么要这样做,这才是 Java 这门编程语言的精髓。

不过,《Java 编程思想》出版的时间是 2007 年,已经十多年过去了,虽然经典,但避免不了过时。但幸好,有《On Java 8》,这本书的作者就是 Bruce Eckel,即《Java 编程思想》的作者。事实上,《On Java 8》就是《Java 编程思想》的第五版,第四版用的 JDK 还是 1.5,《On Java 8》用的 JDK 已经升级到了 Java 8。

那同学们可能就要问了,《On Java 8》上哪买啊?

github.com/LingCoder/O…

不用买了,雷锋已经将这本书翻译成中文并且开源了,访问上面的链接就可以在 GitHub 上阅读了。

  • 《Java 网络编程》

《Java 网络编程》这本书的整体评价并不算高,但是,对于学习 Java 网络编程的基础知识非常的有用。

  • 《Netty 实战》

无论是构建高性能的 Web、游戏服务器、推送系统、RPC 框架、消息中间件还是分布式大数据处理引擎,都离不开 Netty,在整个行业中,Netty 广泛而成功的应用,使其成为了 Java 高性能网络编程的卓绝框架。

  • 《Effective Java》

《Effective Java》第三版一共包含了 90 条极具实用价值的经验规则,每条规则都值得 Java 程序员在实战中去参照。这本书不需要按部就班地从头到尾读,可以随意挑选任意小节进行阅读,因为每条规则相对都是独立的,尽管它们之间会交叉引用,但并不妨碍我们随心所欲地阅读。

作者 Josh Bloch 非常的牛逼,曾是 Google 的首席 Java 架构师,《Java开发者杂志》将他列为世界上最顶尖的四十名软件人物之一。Java 之父詹姆斯·高斯林对《Effective Java》的评价也非常的高。

  • 《阿里巴巴 Java 手册》

《阿里巴巴 Java 开发手册》这本小册子虽然只有几十页,但讲的主要是一些典型的开发规约、编程规范、以及最佳实践,已经成为业界普遍遵循的开发规范。

最新版是嵩山版,封面就有一个扫地僧,唉,这就厉害了呀!

  • 《代码整洁之道》

软件的质量,不仅依赖于架构,更与代码质量息息相关。而代码的质量与其整洁度成正比关系,越整洁的代码,其质量毫无疑问的就会越高。

《代码整洁之道》的第一章,对整洁代码下了一个定义,每个程序员都应该铭记在心。

1、整洁的代码力求专注,每个方法、每个类都应该全神贯注于一件事;命名更要给人一种“顾名思义”的感觉。
2、整洁的代码简单直接,从不隐藏设计者的意图。
3、整洁的代码应当有单元测试。
4、整洁的代码拒绝重复,其表达力直击人的心灵。

  • 《Java 并发编程实战》
  • 《Java 并发编程之美》
  • 《实战 Java 高并发程序设计》

对于程序来说,如果具有并发的能力,效率就能够大幅度地提升。对于程序员来说,如果精通 Java 并发编程的话,挣钱能力就会大幅提升,这话真的不是调侃,而是良心话啊。

这三本 Java 并发方面的书可以结合起来看,互相补充,帮助同学们在这方面快速地提高。

  • 《Java 性能权威指南》

通过阅读《Java 性能权威指南》这本书,我们可以运用 4 个基本原则最大程度地提升性能测试的效果、使用 JDK 自带的工具收集程序的性能数据、理解 JIT(即时编译器)编译器的优缺点、调优 JVM 垃圾收集器、最大程度优化多线程等等。

  • 《Spring 实战》
  • 《Spring 揭秘》

Spring 是 Java 平台的一个开源框架,为基于 Java 构建的 Web 应用程序提供了大量的拓展支持。

1、Spring 能帮我们根据配置文件创建以及组装对象之间的依赖关系。

2、Spring 面向切面编程能帮助我们无耦合的实现日志记录、性能统计、安全控制。

3、Spring 能非常简单地帮我们管理数据库事务。

4、Spring 能与第三方数据访问框架(如 MyBatis、JPA)无缝集成。

5、Spring 能方便的与 Java EE(如 Java Mail、任务调度)整合。

《Spring 实战》和《Spring 揭秘》能够帮助同学们对 Spring 有一个更加全面的了解和掌握。

  • 《Spring Boot+Vue全栈开发实战》

Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。简单来说,就是 Spring Boot 其实不是什么新的框架,它默认配置了很多框架的使用方式,就像 Maven 整合了所有的 Jar 包,Spring Boot 整合了所有的框架。

Spring Boot 方面的好书不多,索性就推荐一下我的好朋友江南一点雨的书,他本人还录制了很多免费的视频,这些视频配套着他的书看,绝对可以对 Spring Boot 有着充分的掌握。

springboot.javaboy.org/

这个链接是江南一点雨的教程合集,我觉得质量还是很高的,我自己在学习 Spring Boot 的时候就参照了不少。

  • 《图解 HTTP》
  • 《HTTP 权威指南》

程序员,毕竟是搞 IT(Information Technology)的,网络这一块还是很重要的。HTTP 全称是 HyperText Transfer Protocal ,即:超文本传输协议,从 1990 年开始就在 WWW 上广泛应用,是现在 WWW 上应用最多的协议,HTTP 是应用层协议,当你上网浏览网页的时候,浏览器和 Web 服务器之间就会通过 HTTP 在 Internet 上进行数据的发送和接收。

《图解 HTTP》和《HTTP 权威指南》很值得去读一读。

  • 《高性能 MySQL》
  • 《MySQL 必知必会》
  • 《MySQL 技术内幕-InnoDB 存储引擎》

MySQL 由于性能高、成本低、可靠性好,已经成为最流行的开源数据库,随着 MySQL 不断的成熟,越来越多大规模的网站开始使用 MySQL,比如维基百科、Google 等。

作为一名 Java 程序员,MySQL 必知必会啊。

  • 《MyBatis 从入门到精通》

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

除了《MyBatis 从入门到精通》这本书,同学们还可以直接阅读 MyBatis 的中文网。

mybatis.org/mybatis-3/z…

  • 《Redis 实战》
  • 《Redis 深度历险:核心原理与应用实战》

Redis 是互联网技术领域中使用最广泛的存储中间件,它是 Remote Dictionary Service 三个单词中加粗字母的组合。

Redis 以超高的性能、完美的文档、简洁的源码著称,国内外很多大型互联网公司都在用,比如说阿里、腾讯、GitHub、Stack Overflow 等等。它的版本更新非常的快,功能也越来越强大,最初只是用来作为缓存数据库,现在已经可以用它来实现消息队列了。

可以这么说吧,掌握 Redis 已经变成了一项后端工程师必须具备的基础技能。

  • 《RabbtiMQ 实战指南》

RabbitMQ 是一款开源的消息队列系统。主要特点在于健壮性好、易于使用、高性能、高并发、集群易扩展,以及强大的开源社区支持。《RabbitMQ 实战指南》从消息中间件的概念和 RabbitMQ 的历史切入,主要阐述 RabbitMQ 的安装、使用、配置、管理、运维、原理、扩展等方面的细节。

  • 《Kafka 权威指南》

每个应用程序都会产生数据,包括日志消息、度量指标、用户活动记录、响应消息等。如何移动数据,几乎变得与数据本身一样重要。如果你是架构师、开发者或者产品工程师,同时也是 Apache Kafka 新手,那么这本实践指南将会帮助你成为流式平台上处理实时数据的专家。

  • 《第一本 Docker 书》

Docker 是一个开源的应用容器引擎,开发者可以利用 Docker 打包自己的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。

《第一本 Docker 书》可以帮助我们学习到 Docker 的安装、部署、管理和扩展。

03、深入

  • 《深入理解 Java 虚拟机》

《深入理解 Java 虚拟机》这本书牛逼到什么程度,几乎所有写 Java 虚拟机方面的文章,没有一个不在文章最后注明:本文参考字周志明老师的《深入理解 Java 虚拟机》。假如这篇文章最后没有注明的话,那保不准是不尊重版权。

  • 《重构,改善既有代码的设计》

《重构,改善既有代码的设计》这本书在业界的名声也非常的响,只不过看起来就需要一些精力和耐力,上面这些书看完后,可以看这一本,对于重新审视现有代码有极大的帮助。

  • 《深入理解 Nginx》

Nginx 是异步框架的网页服务器,也可以用作反向代理、负载平衡器和 HTTP 缓存。《深入理解 Nginx》这本书通过还原 Nginx 设计思想,剖析 Nginx 架构来帮助我们快速高效地开发 HTTP 模块。

  • 《深入剖析 Tomcat》

Tomcat 是一个 Servlet 容器,并提供了作为 Web 服务器的一些特有功能。基本上写过 Web 程序的 Java 程序员都用过 Tomcat 作为服务器,所以有时间的话,对 Tomcat 深入剖析一下还是非常值得去做的。

  • 《JDK 里的设计模式》

这并不是一本书,而是左耳朵耗子的一个帖子,文中列出了 JDK 中 23 个经典的设计模式,非常值得一看。

coolshell.cn/articles/33…

  • 《深入浅出设计模式》
  • 《设计模式之禅》
  • 《Head First 设计模式》

大家都听说过,学习设计模式非常的重要,那么为什么这么重要呢,设计模式到底是什么?打个比喻学编程就像学武功一样。

武功要练得很牛逼,有两样东西不能丢。第一,是内功;第二,是武功秘籍。内功对应到编程就是我们编程基础能力,那编程的设计模式就可以想象成武术中的武功秘籍。

设计模式就是根据不同类型场景,设计优雅的(编码)解决方案。学好设计模式有很多好处,比如,容易看懂经典代码中的逻辑(很多优秀的开源框架大量使用了设计模式);应对面试时对答如流(设计模是面试重点);可以编写出优雅的解决方案(或者代码)。

  • 《算法》
  • 《大话数据结构》

现在的大厂面试,都特别喜欢考算法和数据结构方面的知识,把这个作为程序员水平高低的一个考核标准。对于大部分业务开发来说,平常更多的是利用已经封装好的现成接口、类库,很少需要自己实现数据结构和算法。

但是,如果知道这些类库背后的原理,懂得时间、空间复杂度分析,绝对可以在工作中得心应手。掌握数据结构和算法,不管对于阅读框架源码,还是理解背后的设计思想,都是非常有用的。写出高质量,达到开源级别的代码,算法和数据结构,值得我们去掌握。

  • 《大型网站系统与 Java 中间件实践》
  • 《大型网站技术架构:核心原理与案例分析》
  • 《亿级流量网站架构核心技术》

这三本书,如果能够驾驭得了,那基本上可以这么说,你已经是一名不可多得的 Java 方面的高级人才了。

没有什么难题是百度云盘不能解决的,别忘了点赞!

链接:cowtransfer.com/s/f38807155… 提取码:9js99v
备用链接:shimo.im/docs/pJQv6q…

04、学习方法

我们都经历过学生时代(有些同学还正在经历),同一个班级,同一个老师,为什么成绩截然不同呢,有些同学天资聪颖,学什么都快,有些同学学习能力差,但方法得体,也能取得好成绩。如果既没有天赋,又没有学习方法,那就只配俩字了——对,学渣(大学的我就是一个学渣,不堪回首)。

那正确的学习方法是什么呢?

第一,善用搜索引擎。平常需要找资料,需要解决问题,如果自己一时半会没有方法的话,就去搜。

Google 是第一选择,Bing 是第二选择,微信搜索是第三选择。PC 端的话,Google 和 Bing 就很靠谱,基本上要找的东西,都有好的结果。手机端的话,就用微信搜,答案也很靠谱。

第二,学会提问。如果搜索引擎找不到答案的话,不要直接把问题抛到群里,抛给同事、领导,或者大牛,要先对问题梳理一下。

我想问什么,我的环境是什么,问题的描述是否足够清楚,态度是否端正。

不要唐突,不要冒昧,还要脸皮厚,很难,对吧?这就是提问的艺术啊。

第三,善于总结和归纳。很多同学给我反馈,“二哥,怎么总是感觉记不住啊,学完就忘啊,有什么好的办法吗?”

有啊,当然有。学完一个知识点,如果需要动手去实践,那就去敲代码,敲一遍,还是理解,再敲一遍。如果真的感觉自己理解,尝试写篇文章,把自己的学习心得分享出来,看看是否能够教会别人。

如果自己是半瓶水,倒出去只能是半瓶水;只有自己是一瓶水,倒出去才可能多余半瓶水,对吧?

归纳总结的好处就是把零散的知识变成体系,在脑海中构建一副蓝图,日积月累,你就会发现自己也从一条小溪变成了海洋。

顺带推荐一下费曼学习法,灵感源于诺贝尔物理奖获得者理查德·费曼,运用费曼技巧,只需要花 20 分钟就可以深入理解知识点。听起来是不是很神奇?

费曼学习法的四个步骤:

第一步,能把一个孩子教懂。这有点类似我们古代的一名诗人,白居易,他希望自己的诗连老太婆都能看得懂,那就是好诗。

具体怎么做呢?拿出一张白纸,写下要学习的主题,想一下,怎么把它教给一个孩子,你会讲哪些,然后记下来。

同学们可以把白纸换成 iPad 了,有道云笔记了,等等。

第二步,回顾。当我们尝试把知识点讲给小孩子的时候,可能会卡壳,那就需要把这些卡壳的点重新学习,这些点可能就是我们认知的边界——重新学习的地方。

第三步,简化语言。如果发现表述的文字比较复杂,不够清晰,那就尝试把这些内容读出来,直到通顺,直到自然,直到足够简练。

第四,传授。这点虽然是可选项,但真的非常重要。如果确保自己理解了,那就把它教给别人,看你能否把别人教懂,也就是我说的善于总结和归纳。

整个思维导图( 推荐的在线网址有百度脑图、ProcessOn)了,或者博客(建议使用 markdown 格式)了。

第四,多去实践。实践出真知,到底马谡行不行,给他一个街亭守一守,对吧,收不住就是纸上谈兵,没啥鸟用。

GitHub 或者码云上有很多优秀的开源项目,挑一些优质的,down 下来,去研究研究,在原有的代码基础上,尝试做一些优化,或者增加一些功能。

自己动手去做的过程中,你会发现,哇,真的有新大陆呀!

第五,熟练使用开发工具。Intellij IDEA、VSCode 都是非常流行的开发工具,能够帮助我们在学习和工作中变得更加高效,里面有没有快捷键,有没有什么骚操作,对吧?

在使用 Intellij IDEA 编写代码的过程中,一定要注重代码规范。提前就把 SonarLint、阿里巴巴开发规约这些插件安装上,写完代码就 check 一下,按照提示对一些不良的习惯做出修正。

第六,学好英语。这一点真的真的真的非常非常非常重要重要重要,即便是英语功底本身很差,一定不要自暴自弃,对吧?雷军敢一句“are you ok”走天下,你怕什么?

况且,Chrome 浏览器可以安装 Google 翻译插件,Intellij IDEA 可以安装 Translation 翻译插件,只要稍微有点英语的底子,完全不用怵的。

英语水平提高了,可以看官方的文档,可以在 Stack Overflow 上找答案,可以查看 JDK 源码,等等,编程水平就会与日俱增啊。

第七,注意休息。身体健康非常重要,千万不要沦为工作的机器,认为年轻的时候就应该拼命的加班,通过加班提升技术,多挣点钱。这种想法可以有,但要节制,懂吗?

任务紧,马上要 deadline 了,可以适当的加班,但不要一年四季 365 天都在加班。花点时间去旅游了,读书了,学习了,谈恋爱了,享受生活了。

年轻人,不要做个“奋斗逼”,奋斗可以,但不要盲目。想想脑袋上的那点头发,就别熬夜了。一个人加班,就会有第二个人加班,第三个人加班,第四个人无休止的加班。只有大家都不去加班了,工作的良性氛围才会有啊!

以上,希望能够对同学们有所帮助,peace。


我是沉默王二,一枚在九朝古都洛阳苟且偷生的程序员。关注即可提升学习效率,感谢你的三连支持,奥利给🌹。

如果你觉得文章对你有些帮助,欢迎微信搜索「沉默王二」第一时间阅读,回复关键字【小白】可以获取我手写的 10W+ 字的【Java 小白进阶之路】PDF;本文 GitHub github.com/itwanger 已收录,欢迎 star。

本文转载自: 掘金

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

1…782783784…956

开发者博客

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