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

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


  • 首页

  • 归档

  • 搜索

详细完整的对象实例化过程

发表于 2021-01-20

​ 对象的实例化过程需要做哪些工作呢?首先Java是一门面向对象的语言,类是对所属于一类的所有对象的抽象,对象的所有结构化信息都定义在了类中,因此对象的创建需要根据类中定义的类型信息,也就是类所对应的class二进制字节流,所以这就涉及到了类的加载与初始化。其次,对象大多存储在堆内存中,这就涉及到内存的分配。除此之外,还有变量的初始化零值,对象头的设置,在栈中创建对象的引用等等,本文我们来一起详细的分析一下对象的完整实例化过程。

1 整体流程

从整天上来看对象的整个实例化过程如下图所示:

为了故事的顺利发展,这里我们定义一个Demo,并据此详细讨论一下dc对象是如何创建并实例化出来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码public class Demo
{

public static void main(String[] args)
{
DemoClass dc=new DemoClass();
}
}
class DemoClass
{
private static final int a=1;
private static int b=2;
private static int c;
private int d=4;
private int e;
static
{
c=3;
}
public DemoClass()
{
e=5;
}

}

2 类初始化检查

​ 这里我们使用new 关键字创建对象,Java中创建对象的方式还有好多种,比如反射,克隆,序列化与反序列化等等。这些方式不一而同,但是经过编译器编译之后,对应到Java虚拟机中其实就是一条new(这里的new指令与前面提到的new关键字不同,这是虚拟机级别的指令)指令。当Java虚拟机碰到一条new指令时,会首先根据这条指令所对应的参数去常量池中查找是否有该类所对应的符号引用,并判断该类是否已经被加载、解析、初始化过,也就是到方法区中检查是否有该类的类型信息,如果没有,首先要进行类加载与初始化。如果类已经加载和初始化,那么继续后续的操作。

​ 这里假设DemoClass类还没有被加载与初始化,也就是方法区中还没有DemoClass的类型信息,这时需要进行DemoClass类的加载与初始化。

3 类加载过程

类加载过程总的可分为7个步骤:加载、验证、准备、解析、初始化、使用、卸载。这里我们看一下前六个阶段。

加载

加载阶段主要干了三件事:

  1. 根据类的全限定名获取类的二进制字节流。
  2. 将二进制字节流所代表的静态存储结构转化为方法区中运行时数据结构。
  3. 在内存中创建一个代表该类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

​ 具体到这里就是首先根据package.DemoClass全限定名定位DemoClass.class二进制文件,然后将该.class文件加载到内存进行解析,将解析之后的结果存储在方法区中,最后在堆内存中创建一个Java.lang.Class的对象,用来访问方法区中加载的这些类信息。

验证

​ 验证阶段完成的任务主要是确保class文件中字节流中包含的信息符合Java虚拟机的规范,虽然说得很简单,但是Java虚拟机进行了很多复杂的验证工作,总的来说可分为四个方面:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

具体到这里就是对于加载进内存的DemoClass.class中存储的信息进行虚拟机级别的校验,以确保DemoClass.class中存储的信息不会危害到Java虚拟机的运行。

准备

​ 准备阶段完成的工作就是为类变量(也就是静态变量)分配内存并赋予初始值,通常情况下是变量所对应的数据类型的零值。但是在这个阶段,被final修饰的变量也就是常量会在这个阶段准确的被赋值。

​ 具体到这里,在这个阶段DemoClass中的a会被赋值为1,b与c均被赋值为0。

解析

这个阶段主要的任务是将常量池中的符号引用替换为直接引用。

初始化

​ 在之前的阶段中,除了加载阶段通过自定义的类加载器可以干预虚拟机的加载过程外,其他的阶段都是虚拟机完全主导,而在初始化阶段才开始根据程序员的意愿执行类的初始化,这个阶段主要完成的工作是执行类构造器方法(),同时虚拟机会保证执行该类的类构造器方法时,其父类的类构造器方法已经被正确的执行,同时,由于类的初始化只进行一次,当多个线程并发的进行初始化时,虚拟机可以确保多个线程只有一个可以完成类的初始化工作, 保证线程安全工作。

具体到DemoClass类,在这个阶段会将b赋值为2,c赋值为3。

4 分配内存

当类加载过程完成后,或者类本身之前已经被加载过,下一步就是虚拟机要为新生对象分配内存。对象所需要的内存空间在类加载过程完成后就可以完全确定下来,为对象分配内存空间就相当于从堆内存中划分出一块合适的内存来,分配内存的主要方式有两种:指针碰撞和空闲列表。

  • 指针碰撞:这种方式将堆内存分为空闲空间与已分配空间,使用一个指针来作为二者之间的分界线,当要为新生对象分配内存空间的时候,相当于将指针向着空闲空间的方向移动一段与对象大小相等的距离,可见这种分配方式Java堆内存必须是规整的,所有空闲空间在一边,已分配空间在另外一边。

  • 空闲列表:在虚拟机中维护一个列表,用来记录堆中哪一块内存是空闲可用的,在为新生对象分配内存时,从列表中寻找一块合适大小的可用内存块,分配完成后更新空闲列表,这种方式下堆内存的空闲空间与分配空间可以交错存在。

  <img src="images/空闲列表.png" style="zoom:80%;" />

从上面来看,选择采用指针碰撞还是空闲列表法分配内存,主要由Java堆内存是否规整决定的,而Java堆内存是否规整又取决于所采用的垃圾收集算法,这就涉及到垃圾回收机制(可见知识都是相通的,程序员就是活到老学到死啊!),GC之后是否具有压缩或者整理的动作等等。

同时,由于创建对象的动作是十分频繁的,多线程可能存在多个线程同时申请为对象分配内存空间,这个时候如果不采取一定的同步机制,就有可能导致一个线程还未来得及修改指针,另一个线程就使用了原来的指针分配内存空间,因此衍生出来了两种解决方案:CAS配上失败重试、TLAB方式。

第一种方式很好理解,多个线程使用CAS的方式更新指针,多线程下只有一个线程可以更新完成,其他线程通过不断重试完成内存指针的重新移动。

第二种方式是每个线程提前分配一块内存空间,这个内存空间就是线程本地缓冲TLAB,这样线程每次要分配内存时,先去TLAB中获取,当TLAB中内存空间不足的时候才采用同步机制继续申请一块TLAB空间,这样就降低了同步锁的申请次数。

具体到这个阶段,是在堆内存中为DemoClass对象,也就是dc对象实例开辟了一块内存空间。

5 初始化零值

在为对象分配内存完成之后,虚拟机会将分配到的这块内存初始化为零值,这样也就使得Java中的对象的实例变量可以在不赋初值的情况下使用,因为代码所访问当的就是虚拟机为这块内存分配的零值。

具体到这里,就是Java虚拟机将上面分配的内存空间初始化为零值,这一步使得现在DemoClass中的d与e均被赋值为0。

6 设置对象头

对象头就像我们人的身份证一样,存放了一些标识对象的数据,也就是对象的一些元数据,我们首先看一下对象的构成。

在初始化了零值之后,怎么知道对象是哪个类的实例,就需要设置指向方法区中类型信息的指针,对象Mark Word中相关信息的设置,就在这个阶段完成。

7 实例对象初始化

这一步虚拟机将调用实例构造器方法(), 根据我们程序员的意愿初始化对象,在这一步会调用构造函数,完成实例对象的初始化。

具体到这里就是DemoClass的d被赋值为4,e被赋值为5。

8 创建引用,入栈

执行到这一步,堆内存中已经存在被完成创建完成的对象,但是我们知道,在Java中使用对象是通过虚拟机栈中的引用来获取对象属性,调用对象的方法,因此这一步将创建对象的引用,并压如虚拟机栈中,最终返回引用供我们使用。

在这里就是讲对象的引入入栈,并返回赋值给dc,至此,一个对象被创建完成。

对象实例化的完整流程

根据上面的讨论,我们再来回顾一下对象实例化的整个流程:

本文转载自: 掘金

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

K8S太火了!花10分钟玩转它不香么?

发表于 2021-01-20

SpringBoot实战电商项目mall(40k+star)地址:github.com/macrozheng/…

摘要

我的Mall电商实战项目一直使用的是Docker容器化部署,有很多朋友建议搞个Kubernetes部署。最近正好在学习Kubernetes,准备更新一波!今天我们先来学习下Kubernetes的核心概念和基本使用,希望对大家有所帮助!

Kubernetes简介

Kubernetes(简称K8S,K和S之间有8个字母)是用于自动部署,扩展和管理容器化应用程序的开源系统。它将组成应用程序的容器组合成逻辑单元,以便于管理和服务发现。Kubernetes 源自Google 15 年生产环境的运维经验,同时凝聚了社区的最佳创意和实践。

Kubernetes具有如下特性:

  • 服务发现与负载均衡:无需修改你的应用程序即可使用陌生的服务发现机制。
  • 存储编排:自动挂载所选存储系统,包括本地存储。
  • Secret和配置管理:部署更新Secrets和应用程序的配置时不必重新构建容器镜像,且不必将软件堆栈配置中的秘密信息暴露出来。
  • 批量执行:除了服务之外,Kubernetes还可以管理你的批处理和CI工作负载,在期望时替换掉失效的容器。
  • 水平扩缩:使用一个简单的命令、一个UI或基于CPU使用情况自动对应用程序进行扩缩。
  • 自动化上线和回滚:Kubernetes会分步骤地将针对应用或其配置的更改上线,同时监视应用程序运行状况以确保你不会同时终止所有实例。
  • 自动装箱:根据资源需求和其他约束自动放置容器,同时避免影响可用性。
  • 自我修复:重新启动失败的容器,在节点死亡时替换并重新调度容器,杀死不响应用户定义的健康检查的容器。

Minikube简介

Minikube是一种轻量级的Kubernetes实现,可在本地计算机上创建VM并部署仅包含一个节点的简单集群,Minikube可用于Linux、MacOS和Windows系统。Minikube CLI提供了用于引导集群工作的多种操作,包括启动、停止、查看状态和删除。

Kubernetes核心概念

由于Kubernetes有很多核心概念,学习它们对理解Kubernetes的使用很有帮助,所以我们先来学习下这些核心概念。

Node

Kubernetes集群是指Kubernetes协调一个高可用计算机集群,每个计算机作为独立单元互相连接工作。

一个Kubernetes集群包含两种类型的资源:

  • Master:负责管理整个集群。协调集群中的所有活动,例如调度应用、维护应用的所需状态、应用扩容以及推出新的更新。
  • Node:用于托管正在运行的应用。可以是一个虚拟机或者物理机,它在Kubernetes集群中充当工作机器的角色,每个Node都有Kubelet,它管理Node而且是Node与Master通信的代理,Node还具有用于处理容器操作的工具,例如Docker或rkt。

Deployment

Deployment负责创建和更新应用程序的实例。创建Deployment后,Kubernetes Master 将应用程序实例调度到集群中的各个节点上。如果托管实例的节点关闭或被删除,Deployment控制器会将该实例替换为群集中另一个节点上的实例。这提供了一种自我修复机制来解决机器故障维护问题。

可以使用Kubernetes命令行界面Kubectl创建和管理Deployment。Kubectl使用Kubernetes API与集群进行交互。

Pod

Pod相当于逻辑主机的概念,负责托管应用实例。包括一个或多个应用程序容器(如 Docker),以及这些容器的一些共享资源(共享存储、网络、运行信息等)。

Service

Service是一个抽象层,它定义了一组Pod的逻辑集,并为这些Pod支持外部流量暴露、负载平衡和服务发现。

尽管每个Pod 都有一个唯一的IP地址,但是如果没有Service,这些IP不会暴露在群集外部。Service允许您的应用程序接收流量。Service也可以用在ServiceSpec标记type的方式暴露,type类型如下:

  • ClusterIP(默认):在集群的内部IP上公开Service。这种类型使得Service只能从集群内访问。
  • NodePort:使用NAT在集群中每个选定Node的相同端口上公开Service。使用<NodeIP>:<NodePort>从集群外部访问Service。是ClusterIP的超集。
  • LoadBalancer:在当前云中创建一个外部负载均衡器(如果支持的话),并为Service分配一个固定的外部IP。是NodePort的超集。
  • ExternalName:通过返回带有该名称的CNAME记录,使用任意名称(由spec中的externalName指定)公开Service。不使用代理。

Docker安装

由于Kubernetes运行需要依赖容器运行时(负责运行容器的软件),现比较通用的容器运行时有Docker、containerd和CRI-O。这里选择Docker,先在Linux服务器上安装好Docker环境。

  • 安装yum-utils:
1
bash复制代码yum install -y yum-utils device-mapper-persistent-data lvm2
  • 为yum源添加docker仓库位置:
1
bash复制代码yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
  • 安装Docker:
1
bash复制代码yum install docker-ce
  • 启动Docker:
1
bash复制代码systemctl start docker

Minikube安装

  • 首先我们需要下载Minikube的二进制安装包并安装:
1
2
bash复制代码curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
  • 然后使用如下命令启动Minikube:
1
bash复制代码minikube start
  • 如果你使用的是root用户的话会无法启动并提示如下信息,那是因为Minikube不允许使用root权限启动,需要创建一个非root账号再启动;
1
2
3
4
5
6
7
bash复制代码* minikube v1.16.0 on Centos 7.6.1810
* Automatically selected the docker driver
* The "docker" driver should not be used with root privileges.
* If you are running minikube within a VM, consider using --driver=none:
* https://minikube.sigs.k8s.io/docs/reference/drivers/none/

X Exiting due to DRV_AS_ROOT: The "docker" driver should not be used with root privileges.
  • 这里创建了一个属于docker用户组的macro用户,并切换到该用户;
1
2
3
4
5
6
bash复制代码# 创建用户
useradd -u 1024 -g docker macro
# 设置用户密码
passwd macro
# 切换用户
su macro
  • 再次使用minikube start命令启动Minikube,启动成功后会显示如下信息:
1
2
3
4
5
6
7
8
9
bash复制代码* To pull new external images, you may need to configure a proxy: https://minikube.sigs.k8s.io/docs/reference/networking/proxy/
* Preparing Kubernetes v1.20.0 on Docker 20.10.0 ...
- Generating certificates and keys ...
- Booting up control plane ...
- Configuring RBAC rules ...
* Verifying Kubernetes components...
* Enabled addons: default-storageclass, storage-provisioner
* kubectl not found. If you need it, try: 'minikube kubectl -- get pods -A'
* Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

Kubernetes的使用

创建集群

通过Minikube我们可以创建一个单节点的K8S集群,集群管理Master和负责运行应用的Node都部署在此节点上。

  • 查看Minikube的版本号:
1
bash复制代码minikube version
1
2
bash复制代码minikube version: v1.16.0
commit: 9f1e482427589ff8451c4723b6ba53bb9742fbb1
  • 查看kubectl的版本号,第一次使用会直接安装kubectl:
1
bash复制代码minikube kubectl version
1
2
bash复制代码Client Version: version.Info{Major:"1", Minor:"20", GitVersion:"v1.20.0", GitCommit:"af46c47ce925f4c4ad5cc8d1fca46c7b77d13b38", GitTreeState:"clean", BuildDate:"2020-12-08T17:59:43Z", GoVersion:"go1.15.5", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"20", GitVersion:"v1.20.0", GitCommit:"af46c47ce925f4c4ad5cc8d1fca46c7b77d13b38", GitTreeState:"clean", BuildDate:"2020-12-08T17:51:19Z", GoVersion:"go1.15.5", Compiler:"gc", Platform:"linux/amd64"}
  • 如果你想直接使用kubectl命令的话,可以将其复制到/bin目录下去:
1
2
3
4
5
6
bash复制代码# 查找kubectl命令的位置
find / -name kubectl
# 找到之后复制到/bin目录下
cp /mydata/docker/volumes/minikube/_data/lib/minikube/binaries/v1.20.0/kubectl /bin/
# 直接使用kubectl命令
kubectl version
  • 查看集群详细信息:
1
bash复制代码kubectl cluster-info
1
2
3
4
bash复制代码Kubernetes control plane is running at https://192.168.49.2:8443
KubeDNS is running at https://192.168.49.2:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
  • 查看集群中的所有Node,可以发现Minikube创建了一个单节点的简单集群:
1
bash复制代码kubectl get nodes
1
2
bash复制代码NAME       STATUS   ROLES                  AGE   VERSION
minikube Ready control-plane,master 46m v1.20.0

部署应用

一旦运行了K8S集群,就可以在其上部署容器化应用程序。通过创建Deployment对象,可以指挥K8S如何创建和更新应用程序的实例。

  • 指定好应用镜像并创建一个Deployment,这里创建一个Nginx应用:
1
bash复制代码kubectl create deployment kubernetes-nginx --image=nginx:1.10
  • 创建一个Deployment时K8S会产生如下操作:
+ 选择一个合适的Node来部署这个应用;
+ 将该应用部署到Node上;
+ 当应用异常关闭或删除时重新部署应用。
  • 查看所有Deployment:
1
bash复制代码kubectl get deployments
1
2
bash复制代码NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
kubernetes-nginx 1/1 1 1 21h
  • 我们可以通过kubectl proxy命令创建一个代理,这样就可以通过暴露出来的接口直接访问K8S的API了,这里调用了查询K8S版本的接口;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码[macro@linux-local root]$ kubectl proxy
Starting to serve on 127.0.0.1:8001
[root@linux-local ~]# curl http://localhost:8001/version
{
"major": "1",
"minor": "20",
"gitVersion": "v1.20.0",
"gitCommit": "af46c47ce925f4c4ad5cc8d1fca46c7b77d13b38",
"gitTreeState": "clean",
"buildDate": "2020-12-08T17:51:19Z",
"goVersion": "go1.15.5",
"compiler": "gc",
"platform": "linux/amd64"
}

查看应用

通过对运行应用的Pod进行操作,可以查看容器日志,也可以执行容器内部命令。

  • 查看K8s中所有Pod的状态:
1
bash复制代码kubectl get pods
1
2
bash复制代码NAME                                   READY   STATUS             RESTARTS   AGE
kubernetes-nginx-78bcc44665-8fnnn 1/1 Running 1 21h
  • 查看Pod的详细状态,包括IP地址、占用端口、使用镜像等信息;
1
bash复制代码kubectl describe pods
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
bash复制代码Name:         kubernetes-nginx-78bcc44665-8fnnn
Namespace: default
Priority: 0
Node: minikube/192.168.49.2
Start Time: Tue, 05 Jan 2021 13:57:46 +0800
Labels: app=kubernetes-nginx
pod-template-hash=78bcc44665
version=v1
Annotations: <none>
Status: Running
IP: 172.17.0.7
IPs:
IP: 172.17.0.7
Controlled By: ReplicaSet/kubernetes-nginx-78bcc44665
Containers:
nginx:
Container ID: docker://31eb1277e507ec4cf8a27b66a9f4f30fb919d17f4cd914c09eb4cfe8322504b2
Image: nginx:1.10
Image ID: docker-pullable://nginx@sha256:6202beb06ea61f44179e02ca965e8e13b961d12640101fca213efbfd145d7575
Port: <none>
Host Port: <none>
State: Running
Started: Wed, 06 Jan 2021 09:22:40 +0800
Last State: Terminated
Reason: Completed
Exit Code: 0
Started: Tue, 05 Jan 2021 14:24:55 +0800
Finished: Tue, 05 Jan 2021 17:32:48 +0800
Ready: True
Restart Count: 1
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-dhr4b (ro)
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
default-token-dhr4b:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-dhr4b
Optional: false
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events: <none>
  • 将Pod的名称设置为环境变量,方便之后使用$POD_NAME来应用Pod的名称:
1
bash复制代码export POD_NAME=kubernetes-nginx-78bcc44665-8fnnn
  • 查看Pod打印的日志:
1
bash复制代码kubectl logs $POD_NAME
  • 使用exec可以在Pod的容器中执行命令,这里使用env命令查看环境变量:
1
bash复制代码kubectl exec $POD_NAME -- env
1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=kubernetes-nginx-78bcc44665-8fnnn
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_PORT=443
NGINX_VERSION=1.10.3-1~jessie
HOME=/root
  • 进入容器内部并执行bash命令,如果想退出容器可以使用exit命令:
1
bash复制代码kubectl exec -ti $POD_NAME -- bash

公开暴露应用

默认Pod无法被集群外部访问,需要创建Service并暴露端口才能被外部访问。

  • 创建一个Service来暴露kubernetes-nginx这个Deployment:
1
bash复制代码kubectl expose deployment/kubernetes-nginx --type="NodePort" --port 80
  • 查看K8S中所有Service的状态:
1
bash复制代码kubectl get services
1
2
3
bash复制代码NAME               TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 5h16m
kubernetes-nginx NodePort 10.105.177.114 <none> 80:31891/TCP 5s
  • 查看Service的详情,通过NodePort属性可以得到暴露到外部的端口;
1
bash复制代码kubectl describe services/kubernetes-nginx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bash复制代码Name:                     kubernetes-nginx
Namespace: default
Labels: app=kubernetes-nginx
Annotations: <none>
Selector: app=kubernetes-nginx
Type: NodePort
IP Families: <none>
IP: 10.106.227.54
IPs: 10.106.227.54
Port: <unset> 80/TCP
TargetPort: 80/TCP
NodePort: <unset> 30158/TCP
Endpoints: 172.17.0.7:80
Session Affinity: None
External Traffic Policy: Cluster
Events: <none>
  • 通过CURL命令通过Minikube IP:NodePort IP可以访问Nginx服务,此时将打印Nginx主页信息;
1
bash复制代码curl $(minikube ip):30158
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
html复制代码<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

标签的使用

通过给资源添加Label,可以方便地管理资源(如Deployment、Pod、Service等)。

  • 查看Deployment中所包含的Label;
1
bash复制代码kubectl describe deployment
1
2
3
4
5
6
7
8
9
10
bash复制代码Name:                   kubernetes-nginx
Namespace: default
CreationTimestamp: Tue, 05 Jan 2021 13:57:46 +0800
Labels: app=kubernetes-nginx
Annotations: deployment.kubernetes.io/revision: 1
Selector: app=kubernetes-nginx
Replicas: 1 desired | 1 updated | 1 total | 1 available | 0 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
  • 通过Label查询Pod:
1
bash复制代码kubectl get pods -l app=kubernetes-nginx
1
2
bash复制代码NAME                                READY   STATUS    RESTARTS   AGE
kubernetes-nginx-78bcc44665-8fnnn 1/1 Running 1 21h
  • 通过Label查询Service:
1
bash复制代码kubectl get services -l app=kubernetes-nginx
1
2
bash复制代码NAME               TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes-nginx NodePort 10.106.227.54 <none> 80:30158/TCP 4m44s
  • 给Pod添加Label:
1
bash复制代码kubectl label pod $POD_NAME version=v1
  • 查看Pod的详细信息,可以查看Label信息:
1
bash复制代码kubectl describe pods $POD_NAME
1
2
3
4
5
6
7
8
bash复制代码Name:         kubernetes-nginx-78bcc44665-8fnnn
Namespace: default
Priority: 0
Node: minikube/192.168.49.2
Start Time: Tue, 05 Jan 2021 13:57:46 +0800
Labels: app=kubernetes-nginx
pod-template-hash=78bcc44665
version=v1
  • 通过Label查询Pod:
1
bash复制代码kubectl get pods -l version=v1
  • 通过Label删除服务:
1
bash复制代码kubectl delete service -l app=kubernetes-nginx
1
2
bash复制代码NAME               TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 30h

可视化管理

Dashboard是基于网页的K8S用户界面。你可以使用Dashboard将容器应用部署到K8S集群中,也可以对容器应用排错,还能管理集群资源。

  • 查看Minikube内置插件,默认情况下Dashboard插件未启用:
1
bash复制代码minikube addons list
1
2
3
4
5
6
sql复制代码|-----------------------------|----------|--------------|
| ADDON NAME | PROFILE | STATUS |
|-----------------------------|----------|--------------|
| dashboard | minikube | disabled |
| default-storageclass | minikube | enabled ✅ |
|-----------------------------|----------|--------------|
  • 启用Dashboard插件:
1
bash复制代码minikube addons enable dashboard
  • 开启Dashboard,通过--url参数不会打开管理页面,并可以在控制台获得访问路径:
1
bash复制代码minikube dashboard --url
1
2
3
4
bash复制代码* Verifying dashboard health ...
* Launching proxy ...
* Verifying proxy health ...
http://127.0.0.1:44469/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/
  • 要想从外部访问Dashboard,需要从使用kubectl设置代理才行,--address设置为你的服务器地址;
1
bash复制代码kubectl proxy --port=44469 --address='192.168.5.94' --accept-hosts='^.*' &
  • 从外部访问服务器需要开启防火墙端口;
1
2
3
4
5
6
bash复制代码# 切换到root用户
su -
# 开启端口
firewall-cmd --zone=public --add-port=44469/tcp --permanent
# 重启防火墙
firewall-cmd --reload
  • 通过如下地址即可访问Dashboard:
1
ruby复制代码http://192.168.5.94:44469/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/
  • 查看K8S集群中的资源状态信息:

  • 通过yaml脚本创建K8S资源:

  • 查看K8S中所有Pod的状态信息,通过更多按钮可以查看容器日志和执行内部命令。

总结

当我们的应用需要部署在多个物理机上时,传统的做法是一个个物理机器去部署。如果我们使用了K8S的话,就可以把这些物理机认为是一个集群,只需通过K8S把应用部署到集群即可,无需关心物理机的部署细节。同时K8S提供了水平扩容、自动装箱、自动修复等功能,大大减少了应用集群化部署的工作量。

参考资料

官方文档:kubernetes.io/zh/docs/hom…

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

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

3万字加50张图,带你深度解析 Netty 架构与原理(下)

发表于 2021-01-19

篇幅限制,上文请见:3万字加50张图,带你深度解析 Netty 架构与原理(上)

  1. Netty 的架构与原理

2.1. 为什么要制造 Netty

既然 Java 提供了 NIO,为什么还要制造一个 Netty,主要原因是 Java NIO 有以下几个缺点:

1)Java NIO 的类库和 API 庞大繁杂,使用起来很麻烦,开发工作量大。

2)使用 Java NIO,程序员需要具备高超的 Java 多线程编码技能,以及非常熟悉网络编程,比如要处理断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流处理等一系列棘手的工作。

3)Java NIO 存在 Bug,例如 Epoll Bug 会导致 Selector 空轮训,极大耗费 CPU 资源。

Netty 对于 JDK 自带的 NIO 的 API 进行了封装,解决了上述问题,提高了 IO 程序的开发效率和可靠性,同时 Netty:

1)设计优雅,提供阻塞和非阻塞的 Socket;提供灵活可拓展的事件模型;提供高度可定制的线程模型。

2)具备更高的性能和更大的吞吐量,使用零拷贝技术最小化不必要的内存复制,减少资源的消耗。

3)提供安全传输特性。

4)支持多种主流协议;预置多种编解码功能,支持用户开发私有协议。

**注:所谓支持 TCP、UDP、HTTP、WebSocket 等协议,就是说 Netty 提供了相关的编程类和接口,因此本文后面主要对基于 Netty 的 TCP Server/Client 开发案例进行讲解,以展示 Netty 的核心原理,对于其他协议 Server/Client 开发不再给出示例,帮助读者提升内力而非教授花招是我写作的出发点 :-) **

下图为 Netty 官网给出的 Netty 架构图。

3万字加50张图,带你深度解析 Netty 架构与原理(下)

我们从其中的几个关键词就能看出 Netty 的强大之处:零拷贝、可拓展事件模型;支持 TCP、UDP、HTTP、WebSocket 等协议;提供安全传输、压缩、大文件传输、编解码支持等等。

2.2. 几种 Reactor 线程模式

传统的 BIO 服务端编程采用“每线程每连接”的处理模型,弊端很明显,就是面对大量的客户端并发连接时,服务端的资源压力很大;并且线程的利用率很低,如果当前线程没有数据可读,它会阻塞在 read 操作上。这个模型的基本形态如下图所示(图片来源于网络)。

3万字加50张图,带你深度解析 Netty 架构与原理(下)

BIO 服务端编程采用的是 Reactor 模式(也叫做 Dispatcher 模式,分派模式),Reactor 模式有两个要义:

1)基于 IO 多路复用技术,多个连接共用一个多路复用器,应用程序的线程无需阻塞等待所有连接,只需阻塞等待多路复用器即可。当某个连接上有新数据可以处理时,应用程序的线程从阻塞状态返回,开始处理这个连接上的业务。

2)基于线程池技术复用线程资源,不必为每个连接创建专用的线程,应用程序将连接上的业务处理任务分配给线程池中的线程进行处理,一个线程可以处理多个连接的业务。

下图反应了 Reactor 模式的基本形态(图片来源于网络):

3万字加50张图,带你深度解析 Netty 架构与原理(下)

Reactor 模式有两个核心组成部分:

1)Reactor(图中的 ServiceHandler):Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理线程来对 IO 事件做出反应。

2)Handlers(图中的 EventHandler):处理线程执行处理方法来响应 I/O 事件,处理线程执行的是非阻塞操作。

Reactor 模式就是实现网络 IO 程序高并发特性的关键。它又可以分为单 Reactor 单线程模式、单 Reactor 多线程模式、主从 Reactor 多线程模式。

2.2.1. 单 Reactor 单线程模式

单 Reactor 单线程模式的基本形态如下(图片来源于网络):

3万字加50张图,带你深度解析 Netty 架构与原理(下)

这种模式的基本工作流程为:

1)Reactor 通过 select 监听客户端请求事件,收到事件之后通过 dispatch 进行分发

2)如果事件是建立连接的请求事件,则由 Acceptor 通过 accept 处理连接请求,然后创建一个 Handler 对象处理连接建立后的后续业务处理。

3)如果事件不是建立连接的请求事件,则由 Reactor 对象分发给连接对应的 Handler 处理。

4)Handler 会完成 read–>业务处理–>send 的完整处理流程。

这种模式的优点是:模型简单,没有多线程、进程通信、竞争的问题,一个线程完成所有的事件响应和业务处理。当然缺点也很明显:

1)存在性能问题,只有一个线程,无法完全发挥多核 CPU 的性能。Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈。

2)存在可靠性问题,若线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。

单 Reactor 单线程模式使用场景为:客户端的数量有限,业务处理非常快速,比如 Redis 在业务处理的时间复杂度为 O(1)的情况。

2.2.2. 单 Reactor 多线程模式

单 Reactor 单线程模式的基本形态如下(图片来源于网络):

3万字加50张图,带你深度解析 Netty 架构与原理(下)

这种模式的基本工作流程为:

1)Reactor 对象通过 select 监听客户端请求事件,收到事件后通过 dispatch 进行分发。

2)如果事件是建立连接的请求事件,则由 Acceptor 通过 accept 处理连接请求,然后创建一个 Handler 对象处理连接建立后的后续业务处理。

3)如果事件不是建立连接的请求事件,则由 Reactor 对象分发给连接对应的 Handler 处理。Handler 只负责响应事件,不做具体的业务处理,Handler 通过 read 读取到请求数据后,会分发给后面的 Worker 线程池来处理业务请求。

4)Worker 线程池会分配独立线程来完成真正的业务处理,并将处理结果返回给 Handler。Handler 通过 send 向客户端发送响应数据。

这种模式的优点是可以充分的利用多核 cpu 的处理能力,缺点是多线程数据共享和控制比较复杂,Reactor 处理所有的事件的监听和响应,在单线程中运行,面对高并发场景还是容易出现性能瓶颈。

2.2.3. 主从 Reactor 多线程模式

主从 Reactor 多线程模式的基本形态如下(第一章图片来源于网络,第二章图片是 JUC 作者 Doug Lea 老师在《Scalable IO in Java》中给出的示意图,两张图表达的含义一样):

3万字加50张图,带你深度解析 Netty 架构与原理(下)

3万字加50张图,带你深度解析 Netty 架构与原理(下)

针对单 Reactor 多线程模型中,Reactor 在单个线程中运行,面对高并发的场景易成为性能瓶颈的缺陷,主从 Reactor 多线程模式让 Reactor 在多个线程中运行(分成 MainReactor 线程与 SubReactor 线程)。这种模式的基本工作流程为:

1)Reactor 主线程 MainReactor 对象通过 select 监听客户端连接事件,收到事件后,通过 Acceptor 处理客户端连接事件。

2)当 Acceptor 处理完客户端连接事件之后(与客户端建立好 Socket 连接),MainReactor 将连接分配给 SubReactor。(即:MainReactor 只负责监听客户端连接请求,和客户端建立连接之后将连接交由 SubReactor 监听后面的 IO 事件。)

3)SubReactor 将连接加入到自己的连接队列进行监听,并创建 Handler 对各种事件进行处理。

4)当连接上有新事件发生的时候,SubReactor 就会调用对应的 Handler 处理。

5)Handler 通过 read 从连接上读取请求数据,将请求数据分发给 Worker 线程池进行业务处理。

6)Worker 线程池会分配独立线程来完成真正的业务处理,并将处理结果返回给 Handler。Handler 通过 send 向客户端发送响应数据。

7)一个 MainReactor 可以对应多个 SubReactor,即一个 MainReactor 线程可以对应多个 SubReactor 线程。

这种模式的优点是:

1)MainReactor 线程与 SubReactor 线程的数据交互简单职责明确,MainReactor 线程只需要接收新连接,SubReactor 线程完成后续的业务处理。

2)MainReactor 线程与 SubReactor 线程的数据交互简单, MainReactor 线程只需要把新连接传给 SubReactor 线程,SubReactor 线程无需返回数据。

3)多个 SubReactor 线程能够应对更高的并发请求。

这种模式的缺点是编程复杂度较高。但是由于其优点明显,在许多项目中被广泛使用,包括 Nginx、Memcached、Netty 等。

这种模式也被叫做服务器的 1+M+N 线程模式,即使用该模式开发的服务器包含一个(或多个,1 只是表示相对较少)连接建立线程+M 个 IO 线程+N 个业务处理线程。这是业界成熟的服务器程序设计模式。

2.3. Netty 的模样

Netty 的设计主要基于主从 Reactor 多线程模式,并做了一定的改进。本节将使用一种渐进式的描述方式展示 Netty 的模样,即先给出 Netty 的简单版本,然后逐渐丰富其细节,直至展示出 Netty 的全貌。

简单版本的 Netty 的模样如下:

3万字加50张图,带你深度解析 Netty 架构与原理(下)

关于这张图,作以下几点说明:

1)BossGroup 线程维护 Selector,ServerSocketChannel 注册到这个 Selector 上,只关注连接建立请求事件(相当于主 Reactor)。

2)当接收到来自客户端的连接建立请求事件的时候,通过 ServerSocketChannel.accept 方法获得对应的 SocketChannel,并封装成 NioSocketChannel 注册到 WorkerGroup 线程中的 Selector,每个 Selector 运行在一个线程中(相当于从 Reactor)。

3)当 WorkerGroup 线程中的 Selector 监听到自己感兴趣的 IO 事件后,就调用 Handler 进行处理。

我们给这简单版的 Netty 添加一些细节:

3万字加50张图,带你深度解析 Netty 架构与原理(下)

关于这张图,作以下几点说明:

1)有两组线程池:BossGroup 和 WorkerGroup,BossGroup 中的线程(可以有多个,图中只画了一个)专门负责和客户端建立连接,WorkerGroup 中的线程专门负责处理连接上的读写。

2)BossGroup 和 WorkerGroup 含有多个不断循环的执行事件处理的线程,每个线程都包含一个 Selector,用于监听注册在其上的 Channel。

3)每个 BossGroup 中的线程循环执行以下三个步骤:

3.1)轮训注册在其上的 ServerSocketChannel 的 accept 事件(OP_ACCEPT 事件)

3.2)处理 accept 事件,与客户端建立连接,生成一个 NioSocketChannel,并将其注册到 WorkerGroup 中某个线程上的 Selector 上

3.3)再去以此循环处理任务队列中的下一个事件

4)每个 WorkerGroup 中的线程循环执行以下三个步骤:

4.1)轮训注册在其上的 NioSocketChannel 的 read/write 事件(OP_READ/OP_WRITE 事件)

4.2)在对应的 NioSocketChannel 上处理 read/write 事件

4.3)再去以此循环处理任务队列中的下一个事件

我们再来看下终极版的 Netty 的模样,如下图所示(图片来源于网络):

3万字加50张图,带你深度解析 Netty 架构与原理(下)

关于这张图,作以下几点说明:

1)Netty 抽象出两组线程池:BossGroup 和 WorkerGroup,也可以叫做 BossNioEventLoopGroup 和 WorkerNioEventLoopGroup。每个线程池中都有 NioEventLoop 线程。BossGroup 中的线程专门负责和客户端建立连接,WorkerGroup 中的线程专门负责处理连接上的读写。BossGroup 和 WorkerGroup 的类型都是 NioEventLoopGroup。

2)NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环,每个事件循环就是一个 NioEventLoop。

3)NioEventLoop 表示一个不断循环的执行事件处理的线程,每个 NioEventLoop 都包含一个 Selector,用于监听注册在其上的 Socket 网络连接(Channel)。

4)NioEventLoopGroup 可以含有多个线程,即可以含有多个 NioEventLoop。

5)每个 BossNioEventLoop 中循环执行以下三个步骤:

5.1)select:轮训注册在其上的 ServerSocketChannel 的 accept 事件(OP_ACCEPT 事件)

5.2)processSelectedKeys:处理 accept 事件,与客户端建立连接,生成一个 NioSocketChannel,并将其注册到某个 WorkerNioEventLoop 上的 Selector 上

5.3)runAllTasks:再去以此循环处理任务队列中的其他任务

6)每个 WorkerNioEventLoop 中循环执行以下三个步骤:

6.1)select:轮训注册在其上的 NioSocketChannel 的 read/write 事件(OP_READ/OP_WRITE 事件)

6.2)processSelectedKeys:在对应的 NioSocketChannel 上处理 read/write 事件

6.3)runAllTasks:再去以此循环处理任务队列中的其他任务

7)在以上两个processSelectedKeys步骤中,会使用 Pipeline(管道),Pipeline 中引用了 Channel,即通过 Pipeline 可以获取到对应的 Channel,Pipeline 中维护了很多的处理器(拦截处理器、过滤处理器、自定义处理器等)。这里暂时不详细展开讲解 Pipeline。

2.4. 基于 Netty 的 TCP Server/Client 案例

下面我们写点代码来加深理解 Netty 的模样。下面两段代码分别是基于 Netty 的 TCP Server 和 TCP Client。

服务端代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
java复制代码/**
* 需要的依赖:
* <dependency>
* <groupId>io.netty</groupId>
* <artifactId>netty-all</artifactId>
* <version>4.1.52.Final</version>
* </dependency>
*/
public static void main(String[] args) throws InterruptedException {

// 创建 BossGroup 和 WorkerGroup
// 1. bossGroup 只处理连接请求
// 2. 业务处理由 workerGroup 来完成
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();

try {
// 创建服务器端的启动对象
ServerBootstrap bootstrap = new ServerBootstrap();
// 配置参数
bootstrap
// 设置线程组
.group(bossGroup, workerGroup)
// 说明服务器端通道的实现类(便于 Netty 做反射处理)
.channel(NioServerSocketChannel.class)
// 设置等待连接的队列的容量(当客户端连接请求速率大
// 于 NioServerSocketChannel 接收速率的时候,会使用
// 该队列做缓冲)
// option()方法用于给服务端的 ServerSocketChannel
// 添加配置
.option(ChannelOption.SO_BACKLOG, 128)
// 设置连接保活
// childOption()方法用于给服务端 ServerSocketChannel
// 接收到的 SocketChannel 添加配置
.childOption(ChannelOption.SO_KEEPALIVE, true)
// handler()方法用于给 BossGroup 设置业务处理器
// childHandler()方法用于给 WorkerGroup 设置业务处理器
.childHandler(
// 创建一个通道初始化对象
new ChannelInitializer<SocketChannel>() {
// 向 Pipeline 添加业务处理器
@Override
protected void initChannel(
SocketChannel socketChannel
) throws Exception {
socketChannel.pipeline().addLast(
new NettyServerHandler()
);

// 可以继续调用 socketChannel.pipeline().addLast()
// 添加更多 Handler
}
}
);

System.out.println("server is ready...");

// 绑定端口,启动服务器,生成一个 channelFuture 对象,
// ChannelFuture 涉及到 Netty 的异步模型,后面展开讲
ChannelFuture channelFuture = bootstrap.bind(8080).sync();
// 对通道关闭进行监听
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}

/**
* 自定义一个 Handler,需要继承 Netty 规定好的某个 HandlerAdapter(规范)
* InboundHandler 用于处理数据流入本端(服务端)的 IO 事件
* InboundHandler 用于处理数据流出本端(服务端)的 IO 事件
*/
static class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 当通道有数据可读时执行
*
* @param ctx 上下文对象,可以从中取得相关联的 Pipeline、Channel、客户端地址等
* @param msg 客户端发送的数据
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
// 接收客户端发来的数据

System.out.println("client address: "
+ ctx.channel().remoteAddress());

// ByteBuf 是 Netty 提供的类,比 NIO 的 ByteBuffer 性能更高
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("data from client: "
+ byteBuf.toString(CharsetUtil.UTF_8));
}

/**
* 数据读取完毕后执行
*
* @param ctx 上下文对象
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx)
throws Exception {
// 发送响应给客户端
ctx.writeAndFlush(
// Unpooled 类是 Netty 提供的专门操作缓冲区的工具
// 类,copiedBuffer 方法返回的 ByteBuf 对象类似于
// NIO 中的 ByteBuffer,但性能更高
Unpooled.copiedBuffer(
"hello client! i have got your data.",
CharsetUtil.UTF_8
)
);
}

/**
* 发生异常时执行
*
* @param ctx 上下文对象
* @param cause 异常对象
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
// 关闭与客户端的 Socket 连接
ctx.channel().close();
}
}

客户端端代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
java复制代码/**
* 需要的依赖:
* <dependency>
* <groupId>io.netty</groupId>
* <artifactId>netty-all</artifactId>
* <version>4.1.52.Final</version>
* </dependency>
*/
public static void main(String[] args) throws InterruptedException {

// 客户端只需要一个事件循环组,可以看做 BossGroup
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();

try {
// 创建客户端的启动对象
Bootstrap bootstrap = new Bootstrap();
// 配置参数
bootstrap
// 设置线程组
.group(eventLoopGroup)
// 说明客户端通道的实现类(便于 Netty 做反射处理)
.channel(NioSocketChannel.class)
// handler()方法用于给 BossGroup 设置业务处理器
.handler(
// 创建一个通道初始化对象
new ChannelInitializer<SocketChannel>() {
// 向 Pipeline 添加业务处理器
@Override
protected void initChannel(
SocketChannel socketChannel
) throws Exception {
socketChannel.pipeline().addLast(
new NettyClientHandler()
);

// 可以继续调用 socketChannel.pipeline().addLast()
// 添加更多 Handler
}
}
);

System.out.println("client is ready...");

// 启动客户端去连接服务器端,ChannelFuture 涉及到 Netty 的异步模型,后面展开讲
ChannelFuture channelFuture = bootstrap.connect(
"127.0.0.1",
8080).sync();
// 对通道关闭进行监听
channelFuture.channel().closeFuture().sync();
} finally {
eventLoopGroup.shutdownGracefully();
}
}

/**
* 自定义一个 Handler,需要继承 Netty 规定好的某个 HandlerAdapter(规范)
* InboundHandler 用于处理数据流入本端(客户端)的 IO 事件
* InboundHandler 用于处理数据流出本端(客户端)的 IO 事件
*/
static class NettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 通道就绪时执行
*
* @param ctx 上下文对象
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx)
throws Exception {
// 向服务器发送数据
ctx.writeAndFlush(
// Unpooled 类是 Netty 提供的专门操作缓冲区的工具
// 类,copiedBuffer 方法返回的 ByteBuf 对象类似于
// NIO 中的 ByteBuffer,但性能更高
Unpooled.copiedBuffer(
"hello server!",
CharsetUtil.UTF_8
)
);
}

/**
* 当通道有数据可读时执行
*
* @param ctx 上下文对象
* @param msg 服务器端发送的数据
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
// 接收服务器端发来的数据

System.out.println("server address: "
+ ctx.channel().remoteAddress());

// ByteBuf 是 Netty 提供的类,比 NIO 的 ByteBuffer 性能更高
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("data from server: "
+ byteBuf.toString(CharsetUtil.UTF_8));
}

/**
* 发生异常时执行
*
* @param ctx 上下文对象
* @param cause 异常对象
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
// 关闭与服务器端的 Socket 连接
ctx.channel().close();
}
}

什么?你觉得使用 Netty 编程难度和工作量更大了?不会吧不会吧,你要知道,你通过这么两段简短的代码得到了一个基于主从 Reactor 多线程模式的服务器,一个高吞吐量和并发量的服务器,一个异步处理服务器……你还要怎样?

对上面的两段代码,作以下简单说明:

1)Bootstrap 和 ServerBootstrap 分别是客户端和服务器端的引导类,一个 Netty 应用程序通常由一个引导类开始,主要是用来配置整个 Netty 程序、设置业务处理类(Handler)、绑定端口、发起连接等。

2)客户端创建一个 NioSocketChannel 作为客户端通道,去连接服务器。

3)服务端首先创建一个 NioServerSocketChannel 作为服务器端通道,每当接收一个客户端连接就产生一个 NioSocketChannel 应对该客户端。

4)使用 Channel 构建网络 IO 程序的时候,不同的协议、不同的阻塞类型和 Netty 中不同的 Channel 对应,常用的 Channel 有:

  • NioSocketChannel:非阻塞的 TCP 客户端 Channel(本案例的客户端使用的 Channel)
  • NioServerSocketChannel:非阻塞的 TCP 服务器端 Channel(本案例的服务器端使用的 Channel)
  • NioDatagramChannel:非阻塞的 UDP Channel
  • NioSctpChannel:非阻塞的 SCTP 客户端 Channel
  • NioSctpServerChannel:非阻塞的 SCTP 服务器端 Channel……

启动服务端和客户端代码,调试以上的服务端代码,发现:

1)默认情况下 BossGroup 和 WorkerGroup 都包含 16 个线程(NioEventLoop),这是因为我的 PC 是 8 核的 NioEventLoop 的数量=coreNum*2。这 16 个线程相当于主 Reactor。

3万字加50张图,带你深度解析 Netty 架构与原理(下)

3万字加50张图,带你深度解析 Netty 架构与原理(下)

3万字加50张图,带你深度解析 Netty 架构与原理(下)

其实创建 BossGroup 和 WorkerGroup 的时候可以指定 NioEventLoop 数量,如下:

1
2
ini复制代码EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(16);

这样就能更好地分配线程资源。

2)每一个 NioEventLoop 包含如下的属性(比如自己的 Selector、任务队列、执行器等):

3万字加50张图,带你深度解析 Netty 架构与原理(下)

3)将代码断在服务端的 NettyServerHandler.channelRead 上:

3万字加50张图,带你深度解析 Netty 架构与原理(下)

可以看到 ctx 中包含的属性如下:

3万字加50张图,带你深度解析 Netty 架构与原理(下)

可以看到:

  • 当前 ChannelHandlerContext ctx 是位于 ChannelHandlerContext 责任链中的一环,可以看到其 next、prev 属性
  • 当前 ChannelHandlerContext ctx 包含一个 Handler
  • 当前 ChannelHandlerContext ctx 包含一个 Pipeline
  • Pipeline 本质上是一个双向循环列表,可以看到其 tail、head 属性
  • Pipeline 中包含一个 Channel,Channel 中又包含了该 Pipeline,两者互相引用……

从下一节开始,我将深入剖析以上两段代码,向读者展示 Netty 的更多细节。

2.5. Netty 的 Handler 组件

无论是服务端代码中自定义的 NettyServerHandler 还是客户端代码中自定义的 NettyClientHandler,都继承于 ChannelInboundHandlerAdapter,ChannelInboundHandlerAdapter 又继承于 ChannelHandlerAdapter,ChannelHandlerAdapter 又实现了 ChannelHandler:

1
2
3
4
5
6
7
8
9
java复制代码public class ChannelInboundHandlerAdapter 
extends ChannelHandlerAdapter
implements ChannelInboundHandler {
......


public abstract class ChannelHandlerAdapter
implements ChannelHandler {
......

因此无论是服务端代码中自定义的 NettyServerHandler 还是客户端代码中自定义的 NettyClientHandler,都可以统称为 ChannelHandler。

Netty 中的 ChannelHandler 的作用是,在当前 ChannelHandler 中处理 IO 事件,并将其传递给 ChannelPipeline 中下一个 ChannelHandler 处理,因此多个 ChannelHandler 形成一个责任链,责任链位于 ChannelPipeline 中。

数据在基于 Netty 的服务器或客户端中的处理流程是:读取数据–>解码数据–>处理数据–>编码数据–>发送数据。其中的每个过程都用得到 ChannelHandler 责任链。

3万字加50张图,带你深度解析 Netty 架构与原理(下)

Netty 中的 ChannelHandler 体系如下(第一张图来源于网络):

3万字加50张图,带你深度解析 Netty 架构与原理(下)

3万字加50张图,带你深度解析 Netty 架构与原理(下)

其中:

  • ChannelInboundHandler 用于处理入站 IO 事件
  • ChannelOutboundHandler 用于处理出站 IO 事件
  • ChannelInboundHandlerAdapter 用于处理入站 IO 事件
  • ChannelOutboundHandlerAdapter 用于处理出站 IO 事件

ChannelPipeline 提供了 ChannelHandler 链的容器。以客户端应用程序为例,如果事件的方向是从客户端到服务器的,我们称事件是出站的,那么客户端发送给服务器的数据会通过 Pipeline 中的一系列 ChannelOutboundHandler 进行处理;如果事件的方向是从服务器到客户端的,我们称事件是入站的,那么服务器发送给客户端的数据会通过 Pipeline 中的一系列 ChannelInboundHandler 进行处理。

3万字加50张图,带你深度解析 Netty 架构与原理(下)

无论是服务端代码中自定义的 NettyServerHandler 还是客户端代码中自定义的 NettyClientHandler,都继承于 ChannelInboundHandlerAdapter,ChannelInboundHandlerAdapter 提供的方法如下:

3万字加50张图,带你深度解析 Netty 架构与原理(下)

从方法名字可以看出,它们在不同的事件发生后被触发,例如注册 Channel 时执行 channelRegistred()、添加 ChannelHandler 时执行 handlerAdded()、收到入站数据时执行 channelRead()、入站数据读取完毕后执行 channelReadComplete()等等。

2.6. Netty 的 Pipeline 组件

上一节说到,Netty 的 ChannelPipeline,它维护了一个 ChannelHandler 责任链,负责拦截或者处理 inbound(入站)和 outbound(出站)的事件和操作。这一节给出更深层次的描述。

ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个 ChannelHandler 如何相互交互。

每个 Netty Channel 包含了一个 ChannelPipeline(其实 Channel 和 ChannelPipeline 互相引用),而 ChannelPipeline 又维护了一个由 ChannelHandlerContext 构成的双向循环列表,其中的每一个 ChannelHandlerContext 都包含一个 ChannelHandler。(前文描述的时候为了简便,直接说 ChannelPipeline 包含了一个 ChannelHandler 责任链,这里给出完整的细节。)

如下图所示(图片来源于网络):

3万字加50张图,带你深度解析 Netty 架构与原理(下)

还记得下面这张图吗?这是上文中基于 Netty 的 Server 程序的调试截图,可以从中看到 ChannelHandlerContext 中包含了哪些成分:

3万字加50张图,带你深度解析 Netty 架构与原理(下)

ChannelHandlerContext 除了包含 ChannelHandler 之外,还关联了对应的 Channel 和 Pipeline。可以这么来讲:ChannelHandlerContext、ChannelHandler、Channel、ChannelPipeline 这几个组件之间互相引用,互为各自的属性,你中有我、我中有你。

在处理入站事件的时候,入站事件及数据会从 Pipeline 中的双向链表的头 ChannelHandlerContext 流向尾 ChannelHandlerContext,并依次在其中每个 ChannelInboundHandler(例如解码 Handler)中得到处理;出站事件及数据会从 Pipeline 中的双向链表的尾 ChannelHandlerContext 流向头 ChannelHandlerContext,并依次在其中每个 ChannelOutboundHandler(例如编码 Handler)中得到处理。

3万字加50张图,带你深度解析 Netty 架构与原理(下)

2.7. Netty 的 EventLoopGroup 组件

在基于 Netty 的 TCP Server 代码中,包含了两个 EventLoopGroup——bossGroup 和 workerGroup,EventLoopGroup 是一组 EventLoop 的抽象。

追踪 Netty 的 EventLoop 的继承链,可以发现 EventLoop 最终继承于 JUC Executor,因此 EventLoop 本质就是一个 JUC Executor,即线程,JUC Executor 的源码为:

1
2
3
4
5
6
csharp复制代码public interface Executor {
/**
* Executes the given command at some time in the future.
*/
void execute(Runnable command);
}

Netty 为了更好地利用多核 CPU 的性能,一般会有多个 EventLoop 同时工作,每个 EventLoop 维护着一个 Selector 实例,Selector 实例监听注册其上的 Channel 的 IO 事件。

EventLoopGroup 含有一个 next 方法,它的作用是按照一定规则从 Group 中选取一个 EventLoop 处理 IO 事件。

在服务端,通常 Boss EventLoopGroup 只包含一个 Boss EventLoop(单线程),该 EventLoop 维护者一个注册了 ServerSocketChannel 的 Selector 实例。该 EventLoop 不断轮询 Selector 得到 OP_ACCEPT 事件(客户端连接事件),然后将接收到的 SocketChannel 交给 Worker EventLoopGroup,Worker EventLoopGroup 会通过 next()方法选取一个 Worker EventLoop 并将这个 SocketChannel 注册到其中的 Selector 上,由这个 Worker EventLoop 负责该 SocketChannel 上后续的 IO 事件处理。整个过程如下图所示:

3万字加50张图,带你深度解析 Netty 架构与原理(下)

2.8. Netty 的 TaskQueue

在 Netty 的每一个 NioEventLoop 中都有一个 TaskQueue,设计它的目的是在任务提交的速度大于线程的处理速度的时候起到缓冲作用。或者用于异步地处理 Selector 监听到的 IO 事件。

3万字加50张图,带你深度解析 Netty 架构与原理(下)

Netty 中的任务队列有三种使用场景:

1)处理用户程序的自定义普通任务的时候

2)处理用户程序的自定义定时任务的时候

3)非当前 Reactor 线程调用当前 Channel 的各种方法的时候。

对于第一种场景,举个例子,2.4 节的基于 Netty 编写的服务端的 Handler 中,假如 channelRead 方法中执行的过程很耗时,那么以下的阻塞式处理方式无疑会降低当前 NioEventLoop 的并发度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码/**
* 当通道有数据可读时执行
*
* @param ctx 上下文对象
* @param msg 客户端发送的数据
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
// 借助休眠模拟耗时操作
Thread.sleep(LONG_TIME);

ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("data from client: "
+ byteBuf.toString(CharsetUtil.UTF_8));
}

改进方法就是借助任务队列,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码/**
* 当通道有数据可读时执行
*
* @param ctx 上下文对象
* @param msg 客户端发送的数据
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
// 假如这里的处理非常耗时,那么就需要借助任务队列异步执行

final Object finalMsg = msg;

// 通过 ctx.channel().eventLoop().execute()将耗时
// 操作放入任务队列异步执行
ctx.channel().eventLoop().execute(new Runnable() {
public void run() {
// 借助休眠模拟耗时操作
try {
Thread.sleep(LONG_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}

ByteBuf byteBuf = (ByteBuf) finalMsg;
System.out.println("data from client: "
+ byteBuf.toString(CharsetUtil.UTF_8));
}
});

// 可以继续调用 ctx.channel().eventLoop().execute()
// 将更多操作放入队列

System.out.println("return right now.");
}

断点跟踪这个函数的执行,可以发现该耗时任务确实被放入的当前 NioEventLoop 的 taskQueue 中了。

3万字加50张图,带你深度解析 Netty 架构与原理(下)

对于第二种场景,举个例子,2.4 节的基于 Netty 编写的服务端的 Handler 中,假如 channelRead 方法中执行的过程并不需要立即执行,而是要定时执行,那么代码可以这样写:

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复制代码/**
* 当通道有数据可读时执行
*
* @param ctx 上下文对象
* @param msg 客户端发送的数据
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {

final Object finalMsg = msg;

// 通过 ctx.channel().eventLoop().schedule()将操作
// 放入任务队列定时执行(5min 之后才进行处理)
ctx.channel().eventLoop().schedule(new Runnable() {
public void run() {

ByteBuf byteBuf = (ByteBuf) finalMsg;
System.out.println("data from client: "
+ byteBuf.toString(CharsetUtil.UTF_8));
}
}, 5, TimeUnit.MINUTES);

// 可以继续调用 ctx.channel().eventLoop().schedule()
// 将更多操作放入队列

System.out.println("return right now.");
}

断点跟踪这个函数的执行,可以发现该定时任务确实被放入的当前 NioEventLoop 的 scheduleTasjQueue 中了。

3万字加50张图,带你深度解析 Netty 架构与原理(下)

对于第三种场景,举个例子,比如在基于 Netty 构建的推送系统的业务线程中,要根据用户标识,找到对应的 SocketChannel 引用,然后调用 write 方法向该用户推送消息,这时候就会将这一 write 任务放在任务队列中,write 任务最终被异步消费。这种情形是对前两种情形的应用,且涉及的业务内容太多,不再给出示例代码,读者有兴趣可以自行完成,这里给出以下提示:

3万字加50张图,带你深度解析 Netty 架构与原理(下)

2.9. Netty 的 Future 和 Promise

Netty**对使用者提供的多数 IO 接口(即 Netty Channel 中的 IO 方法)**是异步的(即都立即返回一个 Netty Future,而 IO 过程异步进行),因此,调用者调用 IO 操作后是不能直接拿到调用结果的。要想得到 IO 操作结果,可以借助 Netty 的 Future(上面代码中的 ChannelFuture 就继承了 Netty Future,Netty Future 又继承了 JUC Future)查询执行状态、等待执行结果、获取执行结果等,使用过 JUC Future 接口的同学会非常熟悉这个机制,这里不再展开描述了。也可以通过 Netty Future 的 addListener()添加一个回调方法来异步处理 IO 结果,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
csharp复制代码// 启动客户端去连接服务器端
// 由于 bootstrap.connect()是一个异步操作,因此用.sync()等待
// 这个异步操作完成
final ChannelFuture channelFuture = bootstrap.connect(
"127.0.0.1",
8080).sync();

channelFuture.addListener(new ChannelFutureListener() {
/**
* 回调方法,上面的 bootstrap.connect()操作执行完之后触发
*/
public void operationComplete(ChannelFuture future)
throws Exception {
if (channelFuture.isSuccess()) {
System.out.println("client has connected to server!");
// TODO 其他处理
} else {
System.out.println("connect to serverfail!");
// TODO 其他处理
}
}
});

Netty Future 提供的接口有:

3万字加50张图,带你深度解析 Netty 架构与原理(下)

注:会有一些资料给出这样的描述:“Netty 中所有的 IO 操作都是异步的”,这显然是错误的。Netty 基于 Java NIO,Java NIO 是同步非阻塞 IO。Netty 基于 Java NIO 做了封装,向使用者提供了异步特性的接口,因此本文说 Netty**对使用者提供的多数 IO 接口(即 Netty Channel 中的 IO 方法)**是异步的。例如在 io.netty.channel.ChannelOutboundInvoker(Netty Channel 的 IO 方法多继承于此)提供的多数 IO 接口都返回 Netty Future:

Promise 是可写的 Future,Future 自身并没有写操作相关的接口,Netty 通过 Promise 对 Future 进行扩展,用于设置 IO 操作的结果。Future 继承了 Future,相关的接口定义如下图所示,相比于上图 Future 的接口,它多出了一些 setXXX 方法:

3万字加50张图,带你深度解析 Netty 架构与原理(下)

Netty 发起 IO 写操作的时候,会创建一个新的 Promise 对象,例如调用 ChannelHandlerContext 的 write(Object object)方法时,会创建一个新的 ChannelPromise,相关代码如下:

1
2
3
4
5
6
7
8
9
10
typescript复制代码@Override
public ChannelFuture write(Object msg) {
return write(msg, newPromise());
}
......
@Override
public ChannelPromise newPromise() {
return new DefaultChannelPromise(channel(), executor());
}
......

当 IO 操作发生异常或者完成时,通过 Promise.setSuccess()或者 Promise.setFailure()设置结果,并通知所有 Listener。关于 Netty 的 Future/Promise 的工作原理,我将在下一篇文章中进行源码级的解析。

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 java烂猪皮 』,不定期分享原创知识。
  3. 同时可以期待后续文章ing🚀
  4. .关注后回复【666】扫码即可获取学习资料包

本文转载自: 掘金

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

Python学习路线图(2021最新版)

发表于 2021-01-19

这是我最开始学Python时的一套学习路线,从入门到上手。(不敢说精通,哈哈~)

一、Python基础知识、变量、数据类型


二、Python条件结构、循环结构

图片

三、Python函数

四、字符串

图片

五、列表与元组

六、字典与集合

更多高清大图,请关注攻粽浩【Python小白集训营】查看历史文章。
PS:结尾有彩蛋,别忘了领取视频课程!

最后再送给大家一套免费的视频教程:

为期137天的Python全套视频教程,总计62G!
里面还有笔记和源码,希望对大家有所帮助哈!
排序有点乱,展示一部分给大家看看~

1-30天

…
…

116-137天

如何领取???

关注“Python小白集训营”微信公众号
回复“001”
即可免费领取!

本文转载自: 掘金

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

Java集合中,为什么会需要迭代器

发表于 2021-01-19

关于collection的那些事

在这里插入图片描述

问题一:1.为什么要使用iterator的迭代器?

首先:我们做一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
java复制代码public class Iterator1 {

public static void main(String[] args) {
//list集合:

ArrayList arrayList=new ArrayList();
arrayList.add("1");
arrayList.add("c");
arrayList.add("33");
arrayList.add("cc");

//arrayList的forEach的方法;
arrayList.forEach(System.out::println);


//方法二: foreach-增强for循环 (数据类型 遍历的名字: 遍历集合)
for (Object oo:arrayList)
{
System.out.println(oo);
}



//方法三:获取迭代器的打印
//获取迭代器的打印;listIterator()---先是一个取到迭代器的对象

ListIterator listIterator = arrayList.listIterator();
while (listIterator.hasNext()){

//表示的迭代器直接可以将数据的集合,通过方法-区创建一个迭代器对象,只能是单独的一个;

Object object = listIterator.next();
System.out.println(object);


}


}




}

这就是简单的迭代器的使用方式:
迭代器:->将集合看做成一个公共汽车时,其中的乘客就是一个个数据,如果我想遍历出每个数据,或者说是查找某一个乘客的话,让售票员去找(迭代器iterator),这就会不会暴露==内部数据和结构==,提高==安全性==

迭代器的优点(为什么要使用迭代器)

1
2
3
4
ini复制代码      优点: 
1.可以不了解集合内部的数据结构,就可以直接遍历
2.不暴露内部的数据,可以直接外部遍历;
3.适用性强,基本上的集合都能使用迭代器;

本文转载自: 掘金

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

这样提问,大牛才会为你解答(提问的智慧)

发表于 2021-01-19

在职场上、在生活中提问无处不在,有时我们扮演着提问者,有时我们扮演着回答者。

有些人可能觉得不就是提个问嘛?有啥难的?我很认真的告诉你,提问是一门技术活。

如何正确地提出问题,是一个至关重要的技能。

在写公众号之后,有很多同学会私信我,问我各种问题,作为过来人,我当然懂得那些初入行的程序员们的困惑,我也经历过那个阶段。

在入行初期,我也会问出一些让人难以回答的问题,后来经过社会的毒打,我才渐渐地懂得提问的艺术。

今天我就想谈谈程序员们应该如何提问,有些观点可能听着有点刺耳,但这就是现实。

错误的提问

提的问题其实只要简单的用下搜索引擎就能得到答案。

比如有人问:什么是区块链?

很难吗?身为程序员搜索引擎都不会用?

还是你已经懒到需要别人把:

  1. 打开搜索引擎
  2. 输入关键字
  3. 点击搜索
  4. 点击答案

这几步骤帮你做了,然后再发你链接,你直接点就行了?

再比如一些代码错误,搜索引擎随便一搜即可得知答案,比如:

前人栽树后人乘凉,你偏不在那棵树下待着,拉着别人让他当场种树。

我从来没劝退过学编程的,看到这种的我要劝退你了。

搜索引擎都不会用写什么代码?

你不适合写代码,你没有编程细胞,你适合做少爷。

所以提问之前至少要先自己搜搜吧?别遇到个问题就抛出来,自己都不努力,不去尝试,就想着饭来张口吗?

还有工作上的一些事情,也需要提一提。

比如领导上周发了个代码规范手册,内容很多,上百条。

然后你今天写代码的时候忘了里面一项具体的约束,但是你有点印象,这时候我建议你自己去拉下那个文档找一找,而不是去问同事。

你可能会觉得问下同事更快,但是你自己记不得不要期望别人能记得,公司文件盘就在那里,你自己下载,翻一下很快,能不打扰别人尽量不要打扰别人。

职场上养成这样的习惯,同事间的关系会更加融洽。

就像小时候妈妈教的,自己的事情自己做。

提一些范围过于大的问题。

比如有人问:如何才能学好编程?

我很能理解你想要努力成长奋斗的热血之心。

人人都想上进,人人都想成为别人家的孩子。

但是问题范围过于大,怎样才算是你心目中的学好编程?

是做出个网站就算了吗?还是做出个有口皆碑的轮子?

是月薪达到 1W、2W、3W?

这类问题很模糊,而且只言片语间是说不清的,这肯定是需要经过系统的研究,这估计得一个专栏才能讲得清,这样你让人如何回答?

如果非得让我给出一个答案,那我只能说努力学习?

我估计得到这答案你又不乐意了,说我在敷衍你。

再来个问题:mysql 如何调优?

我感觉你在面试我,我只能回答你:***********。

懂的掌声!

问题冗长,错别字连篇,一堆代码。

这类问题在我们程序员中还是比较常见。

因为我们碰到的问题经常是需要结合一大段代码,需要涉及具体业务逻辑。

然后一堆代码直接微信发过来,没有任何排版,问题有好几段话,直接一扔,坐着解答。

还有一些打了错别字,语句不通,你聊天的时候打错字可以理解,但是你现在是在提问,是请求别人帮你解答。

你需要为自己的提问负责,你的问题都说不清晰,回答者为什么要耗费精力去解读你的提问?

是有几十套房整天想着如何收租,真的闲的蛋疼了吗?

将心比心,别人向你提问的时候这样一堆甩过来,代码一堆,问题都读不通顺,你有何感想?

所以上点心,这种问题,我贴个 RocketMQ issue template,参照这样的格式,写出来,然后发过来提问。

还有和业务强相关的问题,在你充分研究都束手无策之下,真心建议问下你 leader,放着 leader 不用干嘛?外人理解你业务都得花很大精力。

leader 不是拿来供着的,是拿来用的。

正确的提问

自己要先想清楚细节,精简问题。

问题不要张口就来,你遇到问题你真的努力思考过了吗?

你真的理清数据的来龙去脉的吗?

你真的自己打断点一步一步调试过了吗?

还是你只看到,呀,这个服务报错了,怎么回事?

于是问旁边的老哥,我这调用怎么报错了,你帮我看看?

我以前就是这样的,因为我是真的不懂,束手无策,那时断点都打不利索。

随便出点问题就睁眼瞎。

这时候就需要反思下自己,去学习,让知识武装自己。

再说回来,如果你仔细思考过你可能会发现问题被你解决了。

有时候就是缺少前后的梳理,你就集中在某个点,一直想一直想,抓破脑袋也想不出来。

这时候就可以通过为了要正确的提问反推一下自己,因为正确的提问需要提炼问题,需要清楚里面的细节,所以迫使你自己从头开始理,往往到最后问题就被你自己解决了。

就算还有疑问,那问题也相当精简和准确,面对回答者的反问,你能很清晰地把流程说清楚,提高解决问题的效率。

所以自己先摸清细节,精简问题,既能锻炼自己的问题排查能力,提炼能力,还能更好地提问,百利而无一害。

不要觉得替你解决问题理所当然,要珍惜每一次提问。

我其实是一个不喜欢提问的人,我觉得会麻烦别人,除非自己真的解决不了我才会发问。

奈何能力有限,所以我还是会经常问别人问题,我也有提问得不到回复的时候,我会气愤,凭什么不回答我的问题。

人之常情,我很能理解编辑了一段话之后得不到任何回应的感受。

但是反过来想想人家可能在赶项目?

人家可能每天凌晨两点到家?我又没给人钱,人家有什么义务和责任来回答我的问题?

所以要接收这个现实,也没什么好抱怨的,他不是你爸妈,他也很忙,理解一下。

所以有人回答你的提问时你要珍惜,在提问之前你要思考这个问题这样问好不好,是不是模棱两可?

别人抽出时间回答你的问题,你能做的就是缩短别人回答的时间,最高境界就是能提出一些有价值的问题,让双方都得到收益。

还有,保持尊重,不要用向你提问是给你面子一样的语气说话,蜜汁自信?

尽量自己先思考得出几个可能性,或者说几个方案。

提问之后说出自己的理解,至少证明你努力思考过。

毕竟你自己是当事人,你熟悉自己的问题场景,所以可以说说几个大方向,或者备选方案。

我随便举个例子,比如:我现在要做些日志的处理,消息队列用哪个好?

把这种问题换成:我现在要做些日志的处理,消息队列是用 Kafka 、RocketMQ 还是 RabbitMQ?

这样问题就更加具体,这里就有个选择题好过简答题的说法。

比如你向领导提问,让他做决定。

提问 A:领导,最近业绩有点下滑,我这里有两个方案,A方案需要XXXX,B方案需要XXXX,您觉得哪个比较好?

提问 B:领导,最近业绩有点下滑,我该怎么办?

你是领导,你喜欢哪个?

毋庸置疑,在考试的时候我就喜欢做选择题,而不是简答题。

领导招你是要你想给方案,而不是给你想方案。

最后

这篇文章的目的不是出于讽刺,而是希望大家都能正确的提问。

正确的提问不仅能提高个人对问题的分析能力、提炼能力,还能提高解决问题的效率。

这样,你好我好大家好,才是真的好。

别做伸手党,有些问题真的是需要自己钻研才能成长,别人告诉你的和你自己研究出来的不一样。

还有强调一下,这篇文章不是让我们不要提问,而是正确的提问。

然后再推荐看一篇文章,《How-To-Ask-Questions-The-Smart-Way》,专门写给我们编程人士看的。

英文版地址:www.catb.org/~esr/faqs/s…

中文版地址:github.com/ryanhanwu/H…

微信搜索【yes的练级攻略】,关注 yes,回复【123】一份20W字的算法刷题笔记等你来领。 个人文章汇总:github.com/yessimida/y… 欢迎 star !


我是 yes,从一点点到亿点点,欢迎在看、转发、留言,我们下篇见。

本文转载自: 掘金

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

初识MyBatis Plus 增删改操作

发表于 2021-01-19

前言

C: 在上一篇,查老师带大家快速入门了 MP,不知道你是否已经掌握了 MP 的使用步骤,折服于 MP 的强大呢?本篇,查老师将继续在上一篇的 Demo 基础上带你学习 MP,掌握 MP 中常见的 CUD 操作 API。

查老师有话说: CUD 不知道是什么意思?在咱们后端的圈子里,有一个常常挂在嘴边的名词 “CRUD”。甚至有些同学在去面试时,直言自己在某个项目中就是在做 xxx 的 “CRUD” 而已。

至于它的含义,你使用有道词典都能搜到它的含义。CRUD 代表的是 Create, Read (Retrieve), Update, Delete 这四个单词的简写,俗称 “增删改查”/“增删查改”。

因为我们做后端,避免不掉的就是操作数据库,而数据库的基本操作就是这四类。另外在行业内,它也一度成为了咱们圈子里较为 “自嘲” 类的名词,有些小伙伴儿常常自称自己是 “CRUD 工程师” ,旨在表达自己平时做的业务都很简单的意思。

CUD 自然就是查老师从 CRUD 中拆出来的,意味着数据的增删改操作。

image-20210118222222874

插入操作

你是不是以为,查老师要做一大堆的前情准备工作?你错了,在 MP 中,这一切都不需要。直接 “莽”。

一直给你鼓掌就对了

在上一篇 Demo 项目的 UserMapper 中,因为它已经继承了 BaseMapper 接口,所以甚至无需编写 UserMapper.xml 文件,就已获得了通用的 CRUD API。

在 BaseMapper 接口中,插入操作的 API 只有一个。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码// 其他 API 略
public interface BaseMapper<T> extends Mapper<T> {

/**
* 插入一条记录
*
* @param entity 实体对象
* @return 影响行数
*/
int insert(T entity);

}

插入1条记录

接下来,我们准备测试一下插入操作 API,我们先在上一篇 Demo 项目的基础上,复制粘贴一个专门用于测试 CUD 操作的单元测试类。

MPCUD操作单元测试类

测试代码:

查老师提前准备好了一条测试数据。

姓名 年龄 邮箱
Charles 18 charles7c@126.com
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码import org.junit.Assert;

@SpringBootTest
class MybatisPlusCUDTests {

@Autowired
private UserMapper userMapper;

@Test
public void testInsert(){
// 创建用户对象
User user = new User();
user.setName("Charles");
user.setAge(18);
user.setEmail("charles7c@126.com");

// 调用插入操作API
int rows = userMapper.insert(user);
Assert.assertEquals(1, rows);

// MP默认就会自动回填生成的主键
System.out.println(user.getId());
}

}

控制台输出:

1
2
3
sql复制代码==>  Preparing: INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? )
==> Parameters: 1352882704631181313(Long), Charles(String), 18(Integer), charles7c@126.com(String)
<== Updates: 1
1
复制代码1352882704631181313

ID生成策略

我们在插入成功后,看到用户的 id 值是一个很长的数值,不仔细看还以为是身份证号呢?其实,这是 MP 默认的主键生成策略:生成分布式唯一ID 的锅。

查老师有话说: 所谓的分布式唯一ID,简称分布式ID。我们都知道数据库中的每条数据都要定义好一个唯一 ID,像 MySQL 等数据库,提供了主键自增功能,以帮助我们自动生成 1、2、3…这种简单的唯一ID。但随着系统业务越来越复杂,数据库开始分库分表,这种传统的 ID 生成策略在分布式情况下有极大可能出现重复 ID,所以分布式 ID 的概念就诞生了。常见的分布式 ID 解决方案有:Redis生成ID、UUID、Snowflake(雪花算法)等。

自 MP 3.3.0 开始,主键生成策略默认为:使用 雪花算法 + UUID(不含中划线) 组合而成。

UUID: UUID是国际标准化组织(ISO)提出的一个概念,是通用唯一识别码(Universally Unique Identifier)的缩写。在所有空间和时间上被视为唯一的标识,UUID 通常由32个十六进制数字表示,以5个字符组显示,每个字符组以“-”隔开。例如:6db55ec5-ff6f-478a-911d-313de67ed563。

Snowflake: Snowflake 是 Twitter 开源的分布式ID生成算法,结果是一个 long 型的 ID。

如果我们的需求不需要分布式ID,可以替换成 MP 中其他的主键生成策略。我们可以通过查看 MP 的 ID 类型枚举类源码,来查看它有哪些主键生成策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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
java复制代码/**
* 生成ID类型枚举类
*
* @author hubin
* @since 2015-11-10
*/
@Getter
public enum IdType {
/**
* 数据库ID自增
* 该类型请确保数据库支持并设置了主键自增 否则无效
*/
AUTO(0),

/**
* 该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
*/
NONE(1),

/**
* 用户输入ID
* 该类型可以通过自己注册自动填充插件进行填充
*/
INPUT(2),

/* 注意:以下类型、只有当插入对象ID 为空,才自动填充。 */
/**
* 【MP 默认采用此种主键生成策略】
* 通过雪花算法分配ID (主键类型为数值或字符串),
* @since 3.3.0 (3.3.0版本新增该策略)
*/
ASSIGN_ID(3),

/**
* 通过UUID分配ID (主键类型为字符串)
*/
ASSIGN_UUID(4),

/* ---------已过时,不建议采用--------- */
/**
* 从3.3.0版本开始过时,可使用ASSIGN_ID替代
*/
@Deprecated
ID_WORKER(3),
/**
* 从3.3.0版本开始过时,可使用ASSIGN_ID替代
*/
@Deprecated
ID_WORKER_STR(3),
/**
* 从3.3.0版本开始过时,可使用ASSIGN_UUID替代
*/
@Deprecated
UUID(4);

private final int key;

IdType(int key) {
this.key = key;
}
}

当你看中了某一个生成策略,想来改变默认的主键生成策略时,直接在对应实体类的主键属性上,添加 @TableId 注解即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Data
public class User {

// @TableId是用于标注主键属性的注解
// value:数据表对应的列名,如果实体类属性和数据表列名不一致时使用
// type:主键生成策略类型
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
private String email;

}

当然,如果你每个实体都要更改主键生成策略,最好的方式还是直接在 application.yml 中进行全局配置。

1
2
3
4
5
6
7
yaml复制代码# MyBatis Plus配置
mybatis-plus:
# 全局配置
global-config:
db-config:
# 主键生成策略
id-type: auto

查老师有话说: 上方的两种配置,如果你选择了全局配置, 就不需要再配置第一种了 。

更改了主键生成策略后,别忘了先 Truncate 再 Insert 来重置下当前的用户表数据,否则再测试插入时,数据库主键自增序列是从当前的 ID 最大值开始的。

MPCUD操作-重置数据表1

重置完成后,我们再去测试一下刚才的插入操作。

控制台输出:

1
2
3
sql复制代码==>  Preparing: INSERT INTO user ( name, age, email ) VALUES ( ?, ?, ? )
==> Parameters: Charles(String), 18(Integer), charles7c@126.com(String)
<== Updates: 1
1
复制代码6

很显然,这一次,MP 不再为我们分配 ID 了,而是由数据库进行主键自增生成的 ID。

修改操作

在 BaseMapper 接口中,修改操作的 API 有两个,但本篇我们先只介绍一个,另一个需要等我们学完下一篇的条件构造器再介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码// 其他 API 略
public interface BaseMapper<T> extends Mapper<T> {

/**
* 根据 ID 修改
*
* @param entity 实体对象
* @return 影响行数
*/
int updateById(@Param(Constants.ENTITY) T entity);

/**
* 根据 whereEntity 条件,更新记录
*
* @param entity 实体对象 (set 条件值,可以为 null)
* @param updateWrapper 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)
* @return 影响行数
*/
int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> updateWrapper);

}

根据ID修改

为了测试修改操作 API,查老师也提前准备好了一条测试数据。

主键 姓名 年龄 邮箱
5 Billie 18 Billie@126.com

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@SpringBootTest
class MybatisPlusCUDTests {

@Autowired
private UserMapper userMapper;

@Test
void testUpdateById(){
// 创建用户对象
User user = new User();
user.setId(5L);
user.setAge(18);

// 执行修改操作 API
int rows = userMapper.updateById(user);
Assert.assertEquals(1, rows);
}

}

控制台输出:

1
2
3
sql复制代码==>  Preparing: UPDATE user SET age=? WHERE id=?
==> Parameters: 18(Integer), 5(Long)
<== Updates: 1

自动填充

我们在项目开发过程中,经常要在最终存储数据前,进行一些数据填充工作,例如审计信息:创建人、创建时间,更新人、更新时间等。

这些数据的填充工作,重复且枯燥,有时候还容易忘记,MP 中提供了自动填充功能,可以结束这类问题。

接下来,我们就以自动填充实体类的 创建时间、更新时间 这两个属性为例来演示一下 MP 的自动填充功能。

第1步:我们需要给实体类、数据库表先做一些结构调整工作。

1
2
3
4
sql复制代码-- 给用户表添加 create_time 和 update_time 两个列
ALTER TABLE `mybatisplus_demodb`.`user`
ADD COLUMN `create_time` datetime(0) NULL COMMENT '创建时间' AFTER `email`,
ADD COLUMN `update_time` datetime(0) NULL COMMENT '更新时间' AFTER `create_time`;
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Data
public class User {

private Long id;
private String name;
private Integer age;
private String email;
// 添加对应的实体属性:createTime、updateTime
private LocalDateTime createTime;
private LocalDateTime updateTime;

}

第2步:在 User 类中添加 @TableField 注解来给属性指定自动填充类型。

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

private Long id;
private String name;
private Integer age;
private String email;

// @TableField是用于标注普通属性的注解
// value:数据表对应的列名,如果实体类属性和数据表列名不一致时使用
// fill:自动填充类型
// INSERT:插入时自动填充
// UPDATE:更新时自动填充
// INSERT_UPDATE:插入或更新时自动填充
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;

}

第3步:创建自动审计处理器,实现元对象处理器接口。

com.baomidou.mybatisplus.core.handlers.MetaObjectHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码/**
* 通过 MP 的自动填充功能实现自动审计
*/
@Component
public class AutoAuditHandler implements MetaObjectHandler {

@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
}

@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
}

}

这么做完之后,我们再来测试下刚才的修改操作。

控制台输出:

1
2
3
sql复制代码==>  Preparing: UPDATE user SET age=?, update_time=? WHERE id=?
==> Parameters: 18(Integer), 2021-01-23T16:51:18.413(LocalDateTime), 5(Long)
<== Updates: 1

显而易见,执行的 SQL 中多了一个更新时间的修改,而且传的值是当前时间。

查老师有话说: 你也可以测试一下刚才的插入操作 API,看看新增数据时是否会自动填充 创建时间 和 更新时间 数据。

删除操作

在 BaseMapper 接口中,删除操作的 API 一共有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
java复制代码// 其他 API 略
public interface BaseMapper<T> extends Mapper<T> {
/**
* 根据 ID 删除
*
* @param id 主键ID
* @return 影响行数
*/
int deleteById(Serializable id);

/**
* 删除(根据ID 批量删除)
*
* @param idList 主键ID列表(不能为 null 以及 empty)
* @return 影响行数
*/
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);

/**
* 根据 columnMap 条件,删除记录
*
* @param columnMap 表字段 map 对象
* @return 影响行数
*/
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);

/**
* 根据 entity 条件,删除记录
*
* @param queryWrapper 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)
* @return 影响行数
*/
int delete(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

}

根据ID删除

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@SpringBootTest
class MybatisPlusCUDTests {

@Autowired
private UserMapper userMapper;

@Test
void testDeleteById() {
// 执行删除操作API,删除ID为1的用户数据
int rows = userMapper.deleteById(1L);
Assert.assertEquals(1, rows);
}
}

控制台输出:

1
2
3
sql复制代码==>  Preparing: DELETE FROM user WHERE id=?
==> Parameters: 1(Long)
<== Updates: 1

批量ID删除

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@SpringBootTest
class MybatisPlusCUDTests {

@Autowired
private UserMapper userMapper;

@Test
void testDeleteBatchIds() {
// 删除ID为2、3的用户数据
List<Integer> ids = Arrays.asList(2, 3);
int rows = userMapper.deleteBatchIds(ids);
Assert.assertEquals(2, rows);
}

}

控制台输出:

1
2
3
sql复制代码==>  Preparing: DELETE FROM user WHERE id IN ( ? , ? )
==> Parameters: 2(Integer), 3(Integer)
<== Updates: 2

简单的带条件删除

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@SpringBootTest
class MybatisPlusDemoApplication {

@Autowired
private UserMapper userMapper;

@Test
void testDeleteByMap() {
// 删除姓名为Sandy的用户数据
// Map集合的键:表示的是数据库列名不是实体类属性名
Map<String, Object> columnMap = new HashMap<>();
columnMap.put("name", "Sandy");
int rows = userMapper.deleteByMap(columnMap);
Assert.assertEquals(1, rows);
}

}

控制台输出:

1
2
3
sql复制代码==>  Preparing: DELETE FROM user WHERE name = ?
==> Parameters: Sandy(String)
<== Updates: 1

逻辑删除

在项目开发过程中,为了保留用户数据,在删除用户数据时,我们会选择逻辑删除,而非物理删除。

  • 物理删除: 真实删除,将对应数据从数据库中删除,即采用 delete SQL。
  • 逻辑删除: 假删除,将对应数据中代表是否被删除的列,修改为 “被删除状态值”,即采用 update SQL。

接下来,我们也实现一下逻辑删除功能。

第1步:我们需要给实体类、数据库表先做一些结构调整工作。

1
2
3
sql复制代码-- 给用户表添加 is_delete 列
ALTER TABLE `mybatisplus_demodb`.`user`
ADD COLUMN `is_delete` int(2) NULL COMMENT '是否被删除' AFTER `update_time`;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@Data
public class User {

private Long id;
private String name;
private Integer age;
private String email;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;

// 添加对应的实体属性:isDelete
// 为 逻辑删除 属性指定插入数据时自动填充,并调整好数据填充处理器
@TableField(fill = FieldFill.INSERT)
private Integer isDelete;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码/**
* 通过 MP 的自动填充功能实现自动审计
*/
@Component
public class AutoAuditHandler implements MetaObjectHandler {

@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
// 插入数据时,逻辑删除属性自动填充值
this.strictInsertFill(metaObject, "isDelete", Integer.class, 0);
}

@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
}

}

第2步:在 application.yml 中,全局配置逻辑删除的默认值和删除值。

查老师有话说: 这一步也可以通过在 逻辑删除 属性上方添加 @TableLogic 注解实现,但还是建议采用全局配置。

1
2
3
4
5
6
7
8
9
yaml复制代码mybatis-plus:
global-config:
db-config:
# 逻辑删除属性
logic-delete-field: isDelete
# 未删除状态值
logic-not-delete-value: 0
# 删除状态值
logic-delete-value: 1

配置完后,我们再去测试一下刚才的 根据ID删除 操作。

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@SpringBootTest
class MybatisPlusCUDTests {

@Autowired
private UserMapper userMapper;

@Test
void testDeleteById() {
// 执行删除操作API,删除ID为5的用户数据
int rows = userMapper.deleteById(5L);
// int rows = userMapper.deleteById(1L);
Assert.assertEquals(1, rows);
}

}

控制台输出:

1
2
3
sql复制代码==>  Preparing: UPDATE user SET is_delete=1 WHERE id=? AND is_delete=0
==> Parameters: 5(Long)
<== Updates: 0
1
2
3
makefile复制代码java.lang.AssertionError: expected:<1> but was:<0>
Expected :1
Actual :0

这次执行单元测试,竟然报错了!仔细看一下,原来是 断言 提示我们实际结果和预期结果不一致。

什么原因导致失败呢?其实是因为我们加入逻辑删除列之后,数据库表中虽然多了这列,但是这一列都还没有设置过值呢。

image-20210123173103499

而执行的 SQL 中却需要查找逻辑删除列值为0的数据,这肯定找不到啊,影响行数自然为 0 了,和我们预期的影响行数 1 不符,于是报错了。

知道原因后,那就先手动,给数据表的逻辑删除列都设置为0。

image-20210123173244665

再去执行一次刚才的删除测试。

控制台输出:

1
2
3
sql复制代码==>  Preparing: UPDATE user SET is_delete=1 WHERE id=? AND is_delete=0
==> Parameters: 5(Long)
<== Updates: 1

这回就不再报错了,而且显而易见,执行的 SQL 由没有做逻辑删除配置前的 DELETE 操作现在变为了 UPDATE 操作。

image-20210123174034516

查老师有话说: 在逻辑删除配置好之后,原来的部分操作,像查询操作,在执行 SQL 时将自动带上 where is_delete = 0 这个条件。

参考文献

[1]MyBatis Plus 官网. 指南[EB/OL]. baomidou.com/guide/. 2021-01-18

后记

C: 学习 MyBatis 的时候,我们就没担心过 CUD,现在 MP 中自然更不存在这事儿了。而且 MP 还给我们提供了这么多实用功能。

下一篇我们将会学到较为复杂的查询操作,但 MP 还是相对简单的,敬请期待吧。

查老师有话说: 对于技术的学习,查老师一贯遵循的步骤是:先用最最简单的 demo 让它跑起来,然后学学它的最最常用 API 和 配置让自己能用起来,最后熟练使用的基础上,在空闲时尝试阅读它的源码让自己能够洞彻它的运行机制,部分问题出现的原因,同时借鉴这些技术实现来提升自己的代码高度。

所以在查老师的文章中,前期基本都是小白文,仅仅穿插很少量的源码研究。当然等小白文更新多了,你们还依然喜欢,后期会不定时专门对部分技术的源码进行解析。

本文转载自: 掘金

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

JVM性能调优(4) —— 内存分配和垃圾回收调优 内存调优

发表于 2021-01-18

系列文章专栏:JVM系列专栏

系列文章:

  • JVM性能调优(1) —— JVM内存模型和类加载运行机制
  • JVM性能调优(2) —— 垃圾回收算法和垃圾回收器
  • JVM性能调优(3) —— 通过GC日志分析垃圾回收策略

内存调优的目标

新生代的垃圾回收是比较简单的,Eden区满了无法分配新对象时就触发 YoungGC。而且新生代采用的复制算法效率极高,加上新生代存活的对象很少,只要迅速标记出这少量存活对象,移动到Survivor区,然后快速回收掉Eden区,速度很快。一般一次YoungGC就耗费几毫秒或几十毫秒,所以新生代GC对系统的影响基本不是很大。

但老年代的GC就不一样了,老年代GC通常都很耗费时间,尤其是频繁触发老年代GC(FullGC/OldGC)。因为无论是CMS垃圾回收器还是G1垃圾回收器,比如说CMS就要经历初始标记、并发标记、重新标记、并发清理、碎片整理几个环节,过程非常的复杂,STW的时间也会更长,G1同样也是如此。通常来说,FullGC至少比YoungGC慢10倍以上。

新生代对象进入老年代有四个时机:对象年龄超过阀值、大对象直接进入老年代,动态年龄判断规则、新生代GC后存活对象太多无法放入Survivor区。对象年龄太大进入老年代无可避免,因为这部分对象一般来说都是长期存活的对象,是需要进入老年代的。而后三个一般都是因为内存分配不合理或一些参数设置不合理导致对象进入老年代,而且基本都是生命周期较短的对象,然后占满老年代,触发老年代GC。

因此,基于JVM运行的系统最大的问题,就是因为内存分配、参数设置不合理,导致对象频繁的进入老年代,然后频繁触发FullGC,导致系统每隔一段时间就卡顿几百毫秒甚至几秒钟,这对用户体验来说将是极差的。

所以,JVM调优的目标,最重要的就是对内存分配调优,然后合理优化新生代、老年代、Eden和Survivor各个区域的内存大小。接着再尽量优化参数避免新生代的对象进入老年代,尽量让对象留在新生代里被回收掉,甚至不会出现 FullGC。

估算内存运转模型

在设置JVM内存的时候,是没有一个固定标准、固定参数的,但是有一套比较通用的分析和优化方法,就是根据实际业务预估这个系统未来的业务量、访问量,去推算这个系统每秒种的并发量,然后推算每秒钟的请求对内存空间的占用,进而推算出整个系统运行期间的JVM内存运转模型。然后通过各个参数调优,尽量让垃圾对象在年轻代被回收掉,避免频繁 Full GC。

下面就假定有一个每日百万交易的支付系统,来看看怎么估算一个比较合理的内存运转模型。

第1步:分析系统核心业务与核心压力

首先要分析出一个系统的核心压力集中在哪里,每日百万交易的支付系统,最核心的业务当属支付流程。每次支付请求将创建至少一个订单对象,这个订单对象包含支付的用户、渠道、金额、商品、时间等信息。

支付系统的压力有很多方面,包括高并发请求、高性能处理请求、大量订单数据存储等,但在JVM层面,这个支付系统最大的压力就是每天会在JVM中频繁的创建和销毁100万个支付订单对象。

第2步:预估每秒需处理多少次请求

要设置合理的JVM内存大小,首先要估算出核心业务每秒钟有多少次请求。假设每天100万个支付订单,一般用户交易都集中在每天的高峰期,也就是中午或晚上那3~4个小时,那么平均每秒就将近100次。

假设支付系统部署3台机器,那么平均到每台机器就30个支付请求。

第3步:估算一次请求耗时多久

用户发起一次支付请求,后端将创建一个订单对象、做一些关联校验、写入数据库等,还有一些其它操作,比如调用第三方支付平台等。假设一次支付请求耗时1秒吧,那么每秒钟就会产生30个订单对象,然后1秒后这30个对象就变为垃圾对象了。

第4步:估算每秒请求占多少内存

我们可以根据订单类中的实例变量类型来计算就可以了,比如 Integer 占4个字节,Long 占8个字节,String 类型根据长度来计算。假设一个订单类按20个字段来算,往大一点粗略估算占500字节吧。那么每秒30个支付请求就是 30 * 500B ≈ 15KB。

但实际上,每次请求的过程中,除了订单对象,往往还会创建大量其它类型的对象,比如其它的一些关联查询对象,Spring框架创建的对象等,这时一般需要对单个对象放大10~20倍。

而且支付系统还会包含其它的一些业务,比如交易记录、对账管理、结算管理等,再扩大个5~10倍。这样算下来每秒钟基本会产生1M左右的对象。

但这些也不是绝对的,对于一些特殊的系统,比如报表系统、数据计算系统,每次请求创建的对象可能超过10几M了,那么附属创建的这些对象可能影响就没那么大了,此时可以考虑忽略不计。

第5步:估算元空间大小

元空间主要是存放类型信息,也没什么太多好调优的,一般设置几百M够用就可以了,比如256M。

第6步:估算栈内存大小

线程栈主要就是运行期间存储方法的参数、局部变量等信息,一般设置1M就足够了。比如系统有100个线程,那么虚拟机栈就会至少占用100M内存。

第7步:内存分配

这个每日百万交易的支付系统部署3台机器,每台机器每秒扛30个请求。假设部署的机器是2核4G,但是机器本身运行还需要一些内存,那么JVM就只分2G,考虑到要给元空间、虚拟机栈预留空间,那假设堆内存只分1G,新生代给500M,老年代给500M,那 Eden 区就占400M,两个 Survivor 区各占50M。

这样估算下来,就是如下的内存参数设置:

1
java复制代码-Xms1G -Xmx1G -Xmn500M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8

第8步:系统运转模型

经过上面的分析,再结合机器配置,我们就能大致估算这个系统的内存运转模型了。使用上面的内存设置,那么每秒接收30个请求,在Eden区创建30个订单相关的对象;将产生1M新对象,1秒后请求处理完成,将产生1M的垃圾对象;将在400秒后,也就几分钟的时间,Eden 区就占满了,然后触发 Young GC;YoungGC时会把存活对象复制到FromSurvivor区,然后回收掉新生代的垃圾对象,如此往复。如果Survivor区分配不合理,导致存活对象进入老年代,还可以估算出多久触发一次FullGC/OldGC。主要就是估算出GC的频率,然后就可以对内存进行调优了。

第9步:瞬时压力增加时的模型估算

如果遇到搞大促活动或一些突发的性能抖动,压力可能瞬间增加10倍甚至更多,那每秒可能就是上千笔支付请求,每秒内存占用至少10M以上了。这个时候每次支付请求可能就不是1秒能处理完的了,因为压力骤增,系统内存、线程资源、CPU资源都将打满,导致系统性能下降,这样可能有些支付请求需要耗时好几秒,那可能就有几十M对象会占用堆内存几秒钟。

还是按照2核4G的机器部署,堆内存设置1G,新生代500M,Eden区400M,Survivor50M。这时Eden区只需几十秒就满了,然后触发YoungGC。但是,因为压力增加,有些请求需要好几秒,就会有几十M对象会将无法被回收,就被复制到 Survivor 区。

这时就有多种情况了,首先存活几十M的对象可能大于Survivor区50M的内存,那么就会直接复制到老年代。然后如果小于Survivor区,也大于了Survivor区50%的空间了,下一次通过动态年龄规则判断也可能会将部分对象复制到老年代。

然后经过大概10几次YoungGC,也就几百秒后老年代也快满了,这时可能就会触发FullGC,FullGC时要暂停系统运行,无法处理任何请求,而且这种情况下老年代大部分都是垃圾对象,回收性能是很低的。

YoungGC 调优

合理分配内存降低YoungGC频率

根据前面的估算,在正常的情况下如果给堆分配1G的空间,会频繁触发 YoungGC,新生代回收虽然效率高,但也会 Stop The World,暂停系统运行,如果频繁YoungGC,就会频繁暂停系统。

我们可以考虑增大新生代内存,同时使用内存大一点的机器,比如使用4核8G,那么JVM分4G,给堆空间分配3G,新生代给1.5G,老年代给1.5G,Eden 区差不多1.2G,Survivor区150M,这个时候Eden区差不多要半个小时才会占满,然后触发一次YoungGC,而其中99%都是垃圾对象,采用标记-复制算法基本上很能就能完成YoungGC,这就大大降低了YoungGC的频率。

如果业务量更大,还可以考虑横向多部署几台机器,这样分到每台机器的请求就更少了,压力也更小。

保证Survivor空间足够

如果遇到大促活动,瞬时压力增大,每秒就会有10M以上的对象产生,然后有几十兆甚至上百兆的对象会存活几秒以上。按照前面的内存模型来分析下,那 Eden 区2分钟左右就会占满,然后将存活的几十兆对象复制到 Survivor 区;如果这批存活对象大于150M,将直接进入老年代;如果小于150M但大于 75M,那么由于动态年龄判断也有可能频繁导致部分生命周期短的对象进入老年代。老年代如果快速占满将频繁触发FullGC。

新生代调优最重要的一个就是尽量保证 Surivivor 空间足够,避免因为 YoungGC 时 Survivor 空间不够导致大批对象进入老年代,这样就能极大减少甚至不会FullGC了。

这种业务系统其实绝大多数对象的生命周期都很短,长时间存活的对象占不了多少内存,我们应该尽量让对象都留在新生代里。因此我们可以把新生代的内存占比调高一点,比如新生代给2G,老年代给1G,这样 Eden 区就占了1.6G,Survivor 占200M,这样就基本能保证每次YoungGC时存活的对象都能放进 Survivor 区了。或者再可以用 -XX:SurvivorRatio 参数调整下 Eden 区和 Survivor 区的比例,让 Survivor 区尽可能装下每次 YoungGC 后存活的对象。

优化对象年龄阀值

还有一种情况会导致新生代对象进入老年代,就是有些对象连续躲过15次回收后,就会晋升到老年代。这个我们也可以结合实际的业务模型做调整,比如大促的场景中,新生代分2G,Eden区分1.6G,差不多每隔3分钟就触发一次YoungGC,那么在新生代来回复制15次就是45分钟左右的时间才会进入老年代,对于这个系统来说,绝大多数对象的生命周期都是很短的,能存活几分钟以上的对象应该都是程序中的 Controller、Service、Repository 之类的需要长期存活的业务核心组件。

所以对于这种类型的系统,应尽快让长期存活的对象进入老年代,而不是在新生代来回复制15次后再进入老年代。可以通过 -XX:MaxTenuringThreshold 参数降低年龄阀值,比如设置为 5。

优化大对象阀值

还有一种情况就是大对象将直接进入老年代,大对象阀值一般设置1M就够了,一般来说很少有一个对象超过1M的。如果我们确定系统中会频繁创建生命周期短的大对象,我们可以适当调大这个阀值,避免其进入老年代。

可以通过参数 -XX:PretenureSizeThreshold=1M 来设置大对象阀值。

选择垃圾回收器

新生代垃圾回收器有 Serial、ParNew、ParallelScavenge,一般来说老年代要用性能较好的 CMS 垃圾回收器,那么新生代就只能指定 ParNew 回收器。

使用 ParNew 回收器,调优的思路基本就是前面4点,合理分配新生代内存,保证对象能放入 Survivor 区,避免进入老年代,基本 YoungGC 就没啥问题了。

JVM参数

调优后的JVM参数如下:

1
2
3
4
5
6
7
8
9
10
11
jaav复制代码-Xms3G
-Xmx3G
-Xmn2G
-Xss1M
-XX:SurvivorRatio=8
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:MaxTenuringThreshold=5
-XX:PretenureSizeThreshold=1M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC

FullGC 调优

老年代主要使用CMS垃圾回收器,我们就主要结合上面的业务模型来看看CMS回收器的各个参数优化。

多久触发一次 FullGC

在前面年轻代的优化基础之上,我们还需要估算系统多久会触发一次 Full GC,这将决定我们是否要重点优化下老年代。比如估算下来每隔一两个小时或更久执行一次 Full GC,这时候高峰期那一个小时已经过了,这时候执行 Full GC 对系统的影响来说其实是很小的了。

首先看下触发 Full GC 的条件:

  • JDK6 之前有个 -XX:HandlePromotionFailure 分配担保失败的参数,就是每次 YoungGC 前都会判断老年代的可用空间大小是否大于新生代对象总大小,按前面的配置,新生代最多会有 1.8G 的对象,老年大最大才 1G,那岂不是每次 YoungGC 都会担保失败。不过JDK1.6之后就没有这个参数了,也没有这个判断了。
  • 每次 YoungGC 前检查老年代可用空间是否大于历次 YoungGC 后进入老年代的平均对象大小,按照前面的配置,基本上对象在新生代就被回收了,历次进入老年代的平均对象大小其实是很小的,这个条件基本不会触发。
  • 可能某次 YoungGC 后存活对象大于 Survivor 区大小了,要复制到老年代,但发现老年代空间不足也放不下了,这时就会触发FullGC,但年轻代优化好之后,这种概率是非常小的了。
  • CMS 有个 92% 的阀值,就是老年代超过 92% 的时候,会自动触发老年代垃圾回收,这个参数可以通过 -XX:CMSInitiatingOccupancyFraction 设置。

系统运行时,可能会有部分对象慢慢进入老年代,但是新生代优化好之后,对象晋升到老年代的速度是很慢的,可能需要几个小时才触发一次 FullGC。错过高峰期,FullGC 的影响也不会太大。

CMS并发失败

触发老年代GC后,基本就是老年代快满了,CMS有个92%的阀值,那么1G的老年代,就还剩100M左右空间,如果老年代在并发回收时,新晋升到老年代的对象超过100M了,就会导致并发失败(Concurrent Model Failure)。并发失败后,就会进入 Stop The World 的状态,老年代切换为 Serial Old 回收器,Serial Old 回收器是单线程回收,效率非常低的。

但是经过年轻代的调优后,对象升入老年代的速度是很慢的,而且每次升入老年代的平均对象大小是很小的,所以一般在并发回收时还有超过100M的对象升入老年代的概率也是很小的。这种情况下我们一般也不用去调整 -XX:CMSInitiatingOccupancyFraction 参数的值。

CMS回收后碎片整理频率

CMS完成FullGC后,默认是每次都会进行一次内存碎片整理,这个过程也会 Stop The World。但是按照前面的分析,其实我们也没必须要调整这部分参数。

CMS 通过 -XX:+UseCMSCompactAtFullCollection 参数开启GC后内存碎片整理的过程,通过 -XX:CMSFullGCsBeforeCompaction 设置多少次FullGC后进行内存碎片整理,默认0,就是每次FullGC后都整理。

一般不用调整 CMSFullGCsBeforeCompaction 的值,提高这个值,意味着要多次 FullGC 后才会进行内存碎片整理,那么前几次FullGC会导致很多内存碎片产生,不整理就会导致更频繁的触发FullGC,因为虽然FullGC后可用空间很多,但可用的连续空间并不多。所以一般是设置为0,每次FullGC后整理内存碎片。

CMS提升FullGC的性能

CMS还有两个参数可以进一步优化FullGC的性能,降低FullGC的时间。

  • -XX:+CMSParallelInitialMarkEnabled:开启这个参数会在CMS垃圾回收器的“初始标记”阶段开启多线程并发执行,减少STW的时间,进一步降低FullGC的时间。
  • -XX:+CMSScavengeBeforeRemark:这个参数会在CMS的重新标记阶段之前,先尽量执行一次YoungGC。CMS的重新标记也会STW,所以如果在重新标记之前,先执行一次YoungGC,就会回收掉一些年轻代里没有被引用的对象,那么在CMS的重新标记阶段就可以少扫描一些对象,此时就可以提升CMS的重新标记阶段的性能,减少这个阶段的耗时。(注意:无论是并发标记还是重新标记,都会扫描整个堆的对象,因为就算对象在老年代,也可能被新生代对象引用着)

禁用System.gc

在代码中,我们可以通过 System.gc() 建议JVM执行一次 FullGC,但JVM不一定会执行。但这个方法不能随便调用,基本上来说是禁止手动 GC 的,因为使用不当很有可能会频繁触发 FullGC。

针对这个,我们一般可以通过加入 -XX:+DisableExplicitGC 参数来禁止显示执行GC,就是不允许通过代码 System.gc 来触发GC。

元空间GC优化

FullGC 不只老年代满了会触发,元空间配置不当或动态加载的类过多也有可能频繁触发 FullGC。

一般可能有如下情况会动态生成类放入Metaspace区域:

  • 比如通过 ASM、CGLib、javassist 等字节码框架创建代理类。
  • 还有通过反射调用时,如 Method method = XXX.class.getDeclaredMethod(); method.invoke(target, args);,在反射调用一定次数后就会动态生成一些类。

如果由于元空间导致了 FullGC,我们可以加上 -XX:+TraceClassLoading、-XX:+TraceClassUnloading 来观察有哪些类频繁的被加载和卸载,然后分析出根源问题。

有两个参数可控制元空间的大小:

  • -XX:MaxMetaspaceSize:设置元空间最大值,默认是 -1,即不限制,只受限于本地内存大小
  • -XX:MetaspaceSize:指定元空间的初始空间大小,达到该值就会触发垃圾回收进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过 -XX:MaxMetaspaceSize 的情况下,适当提高该值。

JVM参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码-Xms3G
-Xmx3G
-Xmn2G
-Xss1M
-XX:SurvivorRatio=8
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:MaxTenuringThreshold=5
-XX:PretenureSizeThreshold=1M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=92
-XX:CMSWaitDuration=2000
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
-XX:+CMSParallelInitialMarkEnabled
-XX:+CMSScavengeBeforeRemark
-XX:+DisableExplicitGC

大内存机器GC调优

使用大内存机器的场景

前面通过对支付系统的优化,YoungGC 的频率为几分钟一次,Full GC 基本不会发生。但是像遇到双十一这样的大促场景,可能就凌晨那几分钟就会增加平时数十倍甚至上百倍的压力,这个时候如果还是按照4核8G的内存来部署,那可能需要上百台机器。这个时候就可以考虑提升机器的配置,比如提升到16核32G,每台机器每秒可以扛几千次请求,这样就只需要部署十多台机器可能就够了。

其实还有类系统比如报表系统、BI系统、数据计算系统、大数据系统,这类系统的核心业务如数据报表,一次请求可能会查询几十上百兆数据在内存中做计算,如果还是使用小内存机器,那么Eden区将迅速填满,然后触发 YoungGC,而且随着并发压力增加,需要加更多机器。这种情况下我们一般就可以提高机器配置,使用大内存机器来部署了。

总的来说使用大内存机器的场景一般就是由于并发量高或每次请求内存占用高导致频繁YoungGC,然后需要增加很多台机器的时候,为了减少机器的数量,我们就可以使用大内存机器来部署。

大内存机器的问题

比如使用16核32G的内存,假设新生代给20G,那么Eden区就是16G,Survivor 区各占2G。按每秒产生50M对象来计算,5分钟左右就会触发一次YoungGC。内存比之前扩大了10倍,这时如果还是使用 ParNew+CMS这样的垃圾回收器组合,YoungGC 的停顿时间就需要几百毫秒甚至一两秒,这个时候就是每隔几分钟卡个几百毫秒。而且由于长时间卡顿,还会导致请求积压排队,严重的时候还会导致有些请求超时返回。如果再提高配置,比如使用32核64G,那每次YoungGC就需要停顿几秒钟了,这对系统的影响就非常大了。

这个时候就可以使用G1回收器来解决大内存YoungGC过慢的问题。我们可以给G1设置一个预期的GC停顿时间,比如100毫秒,这样G1会保证每次YoungGC停顿时间不超过100毫秒,避免影响用户的体验。

不过对于一些后台运行不直接面向用户的系统,就算一次GC耗时1秒或几秒其实影响也不大,这个时候就没必要用G1回收器了。

G1回收器调优

G1内存布局

G1 可以使用 -XX:G1NewSizePercent 设置新生代Region初始占比,默认是5%;使用 -XX:G1MaxNewSizePercent 设置新生代Region最大占比,默认是 60%。这两个参数一般不用去设置,使用默认值就可以了。

默认情况下,G1 每个 Region 大小为堆内存大小除以2048,取2的N次冥。也可以通过 -XX:G1HeapRegionSize 参数设置每个 Region 的大小。

GC停顿时间

G1 有一个非常重要的参数会影响到G1回收器的表现:-XX:MaxGCPauseMillis,用来设置一次GC最大的停顿时间。这个参数一般需要结合系统压测工具、GC日志、内存分析工具来综合参考,要尽量让GC的频率别太高,同时每次GC停顿时间也别太长,达到一个理想的合理值。

G1会随着系统的运行,不断给新生代分配Region,但并不是非要到60%时才触发YoungGC。其实G1到底会分配多少个Region给新生代,多久触发一次YoungGC,每次耗费多长时间,这些都是不确定的。它整个都是动态的,它会根据预设的停顿时间,给新生代分配一些内存,然后到一定程度就触发YoungGC,把GC时间控制在预设的时间内,避免一次回收过多的Region导致GC停顿时间超出预期,又避免一次回收过少的Region导致频繁GC。

MixedGC 优化

G1 默认在老年代占比超过45%时,就会触发 MixedGC。其实优化 MixedGC 最重要的还是优化内存分配,尽量避免对象进入老年代,尽量避免频繁触发 MixedGC 就行了。

然后还是最核心的 -XX:MaxGCPauseMillis 参数,如果这个参数设置过高,导致系统运行很久,然后新生代占比达到60%了,这个时候可能存活下来的对象放不进Survivor区或者触发Survivor区动态年龄判断,就会导致有些对象进入老年代,进而触发MixedGC。所以就需要合理设置这个参数,保证YoungGC别太频繁的同时,还得考虑每次GC过后存活的对象大小,避免大量对象进入老年代而触发 MixedGC。

JVM参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码-Xms24G
-Xmx24G
-Xmn20G
-Xss1M
-XX:SurvivorRatio=8
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:MaxTenuringThreshold=5
-XX:PretenureSizeThreshold=1M
-XX:+UseG1GC
-XX:+UnlockExperimentalVMOptions
-XX:G1NewSizePercent=5
-XX:G1MaxNewSizePercent=60
-XX:G1HeapRegionSize=4M
-XX:MaxGCPauseMillis=200
-XX:ParallelGCThreads=4

OOM内存溢出问题

在《Java虚拟机规范》的规定里,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能。通常而言,内存溢出问题对系统是毁灭性的,它代表VM内存不足以支撑程序的运行,所以—旦发生这个情况,就会导致系统直接停止运转,甚至会导致VM进程直接崩溃掉。OOM是非常严重的问题,这节就来看下通常有哪些原因导致OOM。

元空间溢出

元空间溢出原因

Metaspace 这块区域一般很少发生内存溢出,如果发生内存溢出—般都是因为两个原因:

  • Metaspace 参数设置不当,比如 Metaspace 内存给的太小,就很容易导致 Metaspace 不够用
  • 代码中用 CGLib、ASM、javassist 等动态字节码技术动态创建一些类,如果代码写的有问题就可能导致生成过多的类而把 Metaspace 塞满

模拟元空间溢出

下面通过CGLib来不断创建类来模拟塞满 Metaspace。

首先在 pom.xml 添加 cglib 的依赖:

1
2
3
4
5
xml复制代码<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.4</version>
</dependency>

下面这段程序通过CGLib不断地创建代理类:

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

public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(IService.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, objects);
}
});
enhancer.create();
}
}

static class IService { }
}

设置如下的JVM参数:元空间固定10M,还添加了追踪类加载和卸载的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码-Xms200M
-Xmx200M
-Xmn150M
-XX:SurvivorRatio=8
-XX:MetaspaceSize=10M
-XX:MaxMetaspaceSize=10M
-XX:+UseConcMarkSweepGC
-XX:+TraceClassLoading
-XX:+TraceClassUnloading
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:./gc.log

运行程序一会就报OOM错误,然后直接退出运行。

从 Caused by: java.lang.OutOfMemoryError: Metaspace 可以看出是由于 Metaspace 引起的OOM。而且从上面类加载的追踪可以看到,程序一直在加载CGLIB动态创建的代理类。

再看下GC日志:可以看出由于元空间满了触发了一次 FullGC。

栈溢出

栈溢出原因

通过前两篇文章可以知道,每个线程都会有一个线程栈,线程栈的大小是固定的,比如设置的1MB。这个线程每调用一个方法,都会将调用方法的栈桢压入线程栈里,方法调用结束就弹出栈帧。栈桢会存储方法的局部变量、异常表、方法地址等信息,也是会占用一定内存的。

如果这个线程不停的调用方法,不停的压入栈帧,而没有弹出栈帧,比如递归调用没有写好结束条件,那线程栈迟早都会被占满,然后导致栈内存溢出。一般来说,引发栈内存溢出,往往都是代码里写了一些bug导致的,正常情况下很少发生。

关于虚拟机栈和本地方法栈,《Java虚拟机规范》中描述了两种异常:StackOverflowError 和 OutOfMemoryError。

1、StackOverflowError

如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常。栈深度在大多数情况下到达1000~2000是完全没有问题,对于正常的方法调用,这个深度应该完全够用了。

2、OutOfMemoryError

如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出 OutOfMemoryError 异常。而HotSpot虚拟机是不支持扩展的,而且栈深度是动态变化的,在设置线程栈大小时(-Xss),如果设置小一些,相应的栈深度就会缩小。

所以 HotSpot 虚拟机栈溢出只会因为栈容量无法容纳新的栈帧而导致 StackOverflowError 异常,而不会出现 OutOfMemoryError 异常。

模拟栈溢出

运行如下这段代码:递归调用 recursion 方法,没有结束条件,所以必定会导致栈溢出

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class GCMain {

public static void main(String[] args) {
recursion(1);
}

public static void recursion(int count) {
System.out.println("times: " + count++);
recursion(count);
}
}

设置如下JVM参数:线程栈设置为256K

1
2
3
4
5
6
7
java复制代码-Xms200M
-Xmx200M
-Xmn150M
-Xss256K
-XX:SurvivorRatio=8
-XX:MetaspaceSize=10M
-XX:MaxMetaspaceSize=10M

运行一会就出现了 StackOverflowError 异常:

堆溢出

堆溢出原因

堆内存溢出主要就是因为有限的内存中放了过多的对象,而且大多数都是存活的,即使GC过后还是大部分都存活,然后堆内存无法在放入对象就导致堆内存溢出。

—般来说堆内存溢出有两种主要的场景:

  • 系统负载过高,请求量过大,导致大量对象都是存活的,无法继续放入对象后,就会引发OOM系统崩溃
  • 系统有内存泄漏的问题,莫名其妙创建了很多的对象,而且都是存活的,GC时无法回收,最终导致OOM

模拟堆溢出

运行如下代码:不断的创建 String 对象,而且都被 datas 引用着无法被回收掉,最终必然会导致OOM。

1
2
3
4
5
6
java复制代码public static void main(String[] args) {
Set<String> datas = new HashSet<>();
while (true) {
datas.add(UUID.randomUUID().toString());
}
}

设置如下JVM参数:新生代、老年代各100M

1
2
3
4
5
6
7
8
java复制代码-Xms200M
-Xmx200M
-Xmn100M
-Xss1M
-XX:SurvivorRatio=8
-XX:MetaspaceSize=10M
-XX:MaxMetaspaceSize=10M
-XX:+UseParNewGC

OutOfMemoryError:可以看到由于Java heap space 不够了导致OOM。

堆外内存溢出

堆外内存

Java中还有一块区域叫直接内存 Direct Memory,也叫堆外内存,它的的容量大小可通过 -XX:MaxDirectMemorySize 参数来指定,如果不指定,则默认与Java堆最大值(-Xmx)一致。

如果想在Java代码里申请使用一块堆外内存空间,可以使用 DirectByteBuffer 这个类,然后构建一个 DirectByteBuffer 对象,这个对象本身是在JVM堆内存里的。但是在构建这个对象的同时,就会在堆外内存中划出来一块内存空间跟这个对象关联起来。当 DirectByteBuffer 对象没地方引用了,成了垃圾对象之后,就会在某一次YoungGC或FullGC的时候把 DirectByteBuffer 对象回收掉,然后就可以释放掉 DirectByteBuffer 关联的堆外内存了。

模拟堆外内存溢出

如果创建了很多的 DirectByteBuffer 对象,占用了大量的堆外内存,而这些 DirectByteBuffer 对象虽然成为了垃圾对象,如果没有被GC回收掉,那么就不会释放堆外内存,久而久之,就有可能导致堆外内存溢出。

但是NIO实际上有个机制是当堆外内存快满了的时候,就调用一次 System.gc() 来建议JVM去执行一次 GC,把垃圾对象回收掉,进而释放堆外内存。

运行如下代码:通过 ByteBuffer.allocateDirect 循环分配1M的堆外内存,allocateDirect 内部会构建 DirectByteBuffer 对象。

1
2
3
4
5
6
7
8
9
10
java复制代码public class GCMain {
private static final int _1M = 1024 * 1024;

public static void main(String[] args) {
ByteBuffer byteBuffer;
for (int i = 0; i < 40; i++) {
byteBuffer = ByteBuffer.allocateDirect(_1M);
}
}
}

设置如下JVM参数:新生代300M,堆外内存最大20M,这样不会触发YoungGC。

1
2
3
4
5
6
7
8
java复制代码-Xms500M
-Xmx500M
-Xmn300M
-XX:MaxDirectMemorySize=20M
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:./gc.log

运行程序后看GC日志:可以看到由于堆外内存不足,NIO调用了两次 System.gc(),这样就没有导致OOM了。

如果我们再加上 -XX:+DisableExplicitGC 参数,禁止调用 System.gc():

1
2
3
4
5
6
7
8
9
java复制代码-Xms500M
-Xmx500M
-Xmn300M
-XX:MaxDirectMemorySize=20M
-XX:+DisableExplicitGC
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:./gc.log

这时就会发现抛出了堆外内存溢出的异常了:

所以一般来说,如果程序中使用了堆外内存时,为了保险起见,就不要设置 -XX:+DisableExplicitGC 参数了。

OOM问题如何解决

OOM分析思路

一般来说解决OOM问题大致的思路是类似的,出现OOM时,首先从日志中分析是哪块区域内存溢出了,然后分析下OOM的线程栈,如果是自己编写的代码通过线程栈基本就能看出问题所在。

然后先检查下内存是否分配合理,是否存在频繁YoungGC和FullGC,因为如果内存分配不合理就会导致年轻代和老年代迅速占满或长时间有大量对象存活,那必然很快占满内存,也有可能导致OOM。

最后可以结合MAT工具分析下堆转储快照,堆转储包含了堆现场全貌和线程栈信息,可以知道是什么对象太多导致OOM的,然后分析对象引用情况,定位是哪部分代码导致的内存溢出,找出根源问题所在。

但是分析OOM问题一般来说是比较复杂的,一般线上系统OOM都不是由我们编写的代码引发的,可能是由于使用的某个开源框架、容器等导致的,这种就需要了解这个框架,进一步分析其底层源码才能从根本上了解其原因。

堆转储快照

加入如下启动参数就可以在OOM时自动dump内存快照:

  • -XX:+HeapDumpOnOutOfMemoryError:OOM时自动dump内存快照
  • -XX:HeapDumpPath=dump.hprof:快照文件存储位置

有了内存快照后就可以使用 MAT 这类工具来分析大量创建了哪些对象。但是对于堆外内存溢出来说,dump的快照文件不会看见什么明显的异常,这个时候就要注意检查下程序是不是使用了堆外内存,比如使用了NIO,然后从这方面入手去排查。

性能调优总结

调优过程总结

一般来说GC频率是越少越好,YoungGC的效率很快,FullGC则至少慢10倍以上,所以应尽可能让对象在年轻代回收掉,减少FullGC的频率。一般一天只发生几次FullGC或者几天发生一次,甚至不发生FullGC才是一个比较良好的JVM性能。

从前面的调优过程可以总结出来,老年代调优的前提是年轻代调优,年轻代调优的前提是合理分配内存空间,合理分配内存空间的前提就是估算内存使用模型。

因此JVM调优的大致思路就是先估算内存使用模型,合理分配各代的内存空间和比例,尽量让年轻代存活对象进入Survivor区,让垃圾对象在年轻代被回收掉,不要进入老年代,减少 FullGC 的频率。最后就是选择合适的垃圾回收器。

频繁FullGC的几种表现

当出现如下情况时,我们就要考虑是不是出现频繁的FullGC了:

  • 机器 CPU 负载过高
  • 频繁 FullGC 报警
  • 系统无法处理请求或者处理过慢

CPU负载过高一般就两个场景:

  • 在系统里创建了大量的线程,这些线程同时并发运行,而且工作负载都很重,过多的线程同时并发运行就会导致机器CPU负载过高。
  • 机器上运行的VM在执行频繁的FullGC,FullGC是非常耗费CPU资源的。而且频繁的FullGC会导致系统时不时的卡死。

频繁FullGC的几种常见原因

① 系统承载高并发请求,或者处理数据量过大,导致YoungGC很频繁,而且每次YoungGC过后存活对象太多,内存分配不合理,Survivor区域过小,导致对象频繁进入老年代,频繁触发FullGC

② 系统一次性加载过多数据进内存,搞出来很多大对象,导致频繁有大对象进入老年代,然后频繁触发FullGC

③ 系统发生了内存泄漏,创建大量的对象,始终无法回收,一直占用在老年代里,必然频繁触发FullGC

④ Metaspace 因为加载类过多触发FullGC

⑤ 误调用 System.gc() 触发 FullGC

JVM参数模板

通过前面的分析总结,JVM参数虽然没有固定的标准,但对于一般的系统,我们其实可以总结出一套通用的JVM参数模板,基本上保证JVM的性能不会太差,又不用一个个系统去调优,在某个系统遇到性能问题时,再针对性的去调优就可以了。

对于一般的系统,我们可能使用4核8G的机器来部署,那么总结一套模板如下:

  • 堆内存分配4G,新生代3G,老年代1G,Eden区2.4G,Survivor区各300M,一般来说YoungGC后存活的对象小于150M就没太大问题
  • 元空间给个 512M 一般就足够了,如果系统会运行时创建很多类,可以调大这个值
  • -XX:MaxTenuringThreshold 对象GC年龄调整为5岁,让长期存活的对象更快的进入老年代
  • -XX:PretenureSizeThreshold 大对象阀值设置为1M,如果有超过1M的大对象,可以调整下这个值
  • -XX:+UseParNewGC、-XX:+UseConcMarkSweepGC,垃圾回收器使用 ParNew + CMS 的组合
  • -XX:CMSFullGCsBeforeCompaction 设置为0,每次FullGC后都进行一次内存碎片整理
  • -XX:+CMSParallelInitialMarkEnabled,CMS初始标记阶段开启多线程并发执行,降低FullGC的时间
  • -XX:+CMSScavengeBeforeRemark,CMS重新标记阶段之前,先尽量执行一次Young GC
  • -XX:+DisableExplicitGC,禁止显示手动GC
  • -XX:+HeapDumpOnOutOfMemoryError,OOM时导出堆快照便于分析问题
  • -XX:+PrintGC,打印GC日志便于出问题时分析问题
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复制代码-Xms4G
-Xmx4G
-Xmn3G
-Xss1M
-XX:SurvivorRatio=8
-XX:MetaspaceSize=512M
-XX:MaxMetaspaceSize=512M
-XX:MaxTenuringThreshold=5
-XX:PretenureSizeThreshold=1M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=92
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSWaitDuration=2000
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
-XX:+CMSParallelInitialMarkEnabled
-XX:+CMSScavengeBeforeRemark
-XX:+DisableExplicitGC
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=dump.hprof
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:./gc.log

JVM参数

前面已经提到过很多JVM的参数了,这节再简单汇总下,以及部分不常用的参数。

Java启动参数共分为三类:

  • 标准参数(-):所有的JVM实现都必须实现这些参数的功能,而且向后兼容,如 -version、-classpath
  • 非标准参数(-X):默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容,如 -Xms、-Xmx
  • 非Stable参数(-XX):此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用,如 -XX:UseParNewGC、-XX:MetaspaceSize

JVM标准参数(-)

通过 java -help 命令可以看到JVM的标准参数

JVM非标准参数(-X)

通过 java -X 命令可以看到JVM非标准参数

常用参数:

JVM非Stable参数(-XX)

JVM非Stable参数分为三类:

  • 功能开关参数:一些功能的开关,用于改变jvm的一些基础行为
  • 性能调优参数:用于jvm的性能调优
  • 调试参数:一般用于打开跟踪、打印、输出等jvm参数,用于显示jvm更加详细的信息

注意:带有加号“+”、减号“-”的参数一般为开关参数,加号就是启用,减号就是禁用,如 -XX:+/-UseAdaptiveSizePolicy。不带加减号的就需要通过等号“=”带上参数值,如 -XX:SurvivorRatio=8。

可以通过设置 -XX:+PrintFlagsFinal 在启动时打印所有JVM的参数及其值。

功能开关参数

1、垃圾回收器相关参数

2、其它的一些参数

性能调优参数

调试参数

即时编译调优参数

类初始化完成后,类在调用执行过程中,执行引擎会把字节码转为机器码,然后在操作系统中才能执行。在字节码转换为机器码的过程中,虚拟机中还存在着一道编译,那就是即时编译。最初,虚拟机中的字节码是由解释器Interpreter完成编译的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,即时编译器(JIT)会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,然后保存到内存中。如果没有 JIT 即时编译,每次运行相同的代码都会使用解释器编译。

与编译优化有关的主要有即时编译器的选择、热点探测计数阀值的优化、方法内联、逃逸分析、锁消除、标量替换等,一般来说也不用对编译进行调优,这里就不展开说了,下面先列举下编译优化相关的一些JVM参数。

本文转载自: 掘金

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

美团面试官:生成订单后一段时间不支付订单会自动关闭的功能该如

发表于 2021-01-18

业务场景


我们以订单功能为例说明下:生成订单后一段时间不支付订单会自动关闭。最简单的想法是设置定时任务轮询,但是每个订单的创建时间不一样,定时任务的规则无法设定,如果将定时任务执行的间隔设置的过短,太影响效率。还有一种想法,在用户进入订单界面的时候,判断时间执行相关操作。方式可能有很多,在这里介绍一种监听 Redis 键值对过期时间来实现订单自动关闭。整理了一份Java面试宝典完整版PDF

实现思路


在生成订单时,向 Redis 中增加一个 KV 键值对,K 为订单号,保证通过 K 能定位到数据库中的某个订单即可,V 可为任意值。假设,生成订单时向 Redis 中存放 K 为订单号,V 也为订单号的键值对,并设置过期时间为 30 分钟,如果该键值对在 30 分钟过期后能够发送给程序一个通知,或者执行一个方法,那么即可解决订单关闭问题。实现:通过监听 Redis 提供的过期队列来实现,监听过期队列后,如果 Redis 中某一个 KV 键值对过期了,那么将向监听者发送消息,监听者可以获取到该键值对的 K,注意,是获取不到 V 的,因为已经过期了,这就是上面所提到的,为什么要保证能通过 K 来定位到订单,而 V 为任意值即可。拿到 K 后,通过 K 定位订单,并判断其状态,如果是未支付,更新为关闭,或者取消状态即可。

开启 Redis key 过期提醒


修改 redis 相关事件配置。找到 redis 配置文件 redis.conf,查看 notify-keyspace-events 配置项,如果没有,添加 notify-keyspace-events Ex,如果有值,则追加 Ex,相关参数说明如下:

  • K:keyspace 事件,事件以 keyspace@ 为前缀进行发布
  • E:keyevent 事件,事件以 keyevent@ 为前缀进行发布
  • g:一般性的,非特定类型的命令,比如del,expire,rename等
  • $:字符串特定命令
  • l:列表特定命令
  • s:集合特定命令
  • h:哈希特定命令
  • z:有序集合特定命令
  • x:过期事件,当某个键过期并删除时会产生该事件
  • e:驱逐事件,当某个键因 maxmemore 策略而被删除时,产生该事件
  • A:g$lshzxe的别名,因此”AKE”意味着所有事件

引入依赖


在 pom.xml 中添加 org.springframework.boot:spring-boot-starter-data-redis 依赖

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

相关配置


定义配置 RedisListenerConfig 实现监听 Redis key 过期时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;

@Configuration
public class RedisListenerConfig {

@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {

RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}

定义监听器 RedisKeyExpirationListener,实现KeyExpirationEventMessageListener 接口,查看源码发现,该接口监听所有 db 的过期事件 keyevent@*:expired”整理了一份Java面试宝典完整版PDF

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
scala复制代码import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;

/**
* 监听所有db的过期事件__keyevent@*__:expired"
*/
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}

/**
* 针对 redis 数据失效事件,进行数据处理
* @param message
* @param pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {

// 获取到失效的 key,进行取消订单业务处理
String expiredKey = message.toString();
System.out.println(expiredKey);
}
}

本文转载自: 掘金

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

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

发表于 2021-01-18

灵魂一问,我们为什么要学习JDK源码?

当然不是为了装,毕竟谁没事找事虐自己 …

1、面试跑不掉。现在只要面试Java相关的岗位,肯定或多或少会会涉及JDK源码相关的问题。

2、弄懂原理才不慌。我们作为JDK的使用者,虽然说天天用得很开心,但是有时候遇到问题还是得跟到底层源码去看看,才能帮助我们更好的弄懂原理,

3、学习优秀的代码、思想和模式。JDK毕竟是一个优秀的代码库,我们天天用,源码也就在里面,作为一个有志向的程序员,读一读源码也能让我们吸取到更多优秀的思想和模式。

那么源码难吗?

废话,当然有难度啦,不然我也不会到现在都还没看完,而且看了也经常忘,哭唧唧…

毕竟像JDK这种源码,和我们平常练手写小例子、写业务代码不一样,人家毕竟是 类库,为了性能、稳定性、通用性,扩展性等因素考虑,加入了很多辅助代码、泛型、以及一些设计模式上的考量,所以看起来肯定没有那么轻松,没办法一眼看穿它。

所以这玩意儿肯定是一个长期的过程,但是我们一定要有足够的信心,我坚信“JDK源码笔记”人家都写出来了,我就不信我看不懂!接下来我们就一看究竟!

有兴趣想要学习的一键三连,添加小编vx:mxzFAFAFA即可免费获取!!

主要内容

第一章多线程基础

  • 锁的本质是什么

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

第二章Atomic类

  • AtomicBoolean和AtomicReference

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

  • Striped64与LongAdder

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

第三章Lock与Condition

  • 互斥锁

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

  • 读写锁

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

第4章同步工具类

  • CountDownLatch

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

  • Exchanger

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

  • Phaser

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

第5章并发容器

在Lock和Phaser的实现中,已经介绍了基于CAS实现的无锁队列和无锁栈。本章将全面介绍Concurrent包提供的各种并发容器。

  • BlockingQueue

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

  • ConcurrentHashMap

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

  • ConcurrentSkipListMap/Set

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

第6章线程池与Future

  • 线程池与Future

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

  • 线程池的类继承体系

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

  • ScheduledThreadPoolExecutor

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

第7章ForkJoinPool

  • 工作窃取队列

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

  • ForkJoinTask的fork/join

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

  • ForkJoinPool的优雅关闭

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

CompletableFuture

  • CompletableFuture用法

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

  • 任务的网状执行:有向无环图

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

跨年巨作!13万字!腾讯高工手写JDK源码笔记 带你飙向实战

共勉

看源码这东西不能急,慢一点才能更快!也希望这篇“JDK源码剖析”对各位大哥们也有所帮助!共勉。

本文转载自: 掘金

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

1…733734735…956

开发者博客

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