首页 文章 精选 留言 我的

精选列表

搜索[面试],共4913篇文章
优秀的个人博客,低调大师

k8s面试问什么?

文章来源:https://zhuanlan.zhihu.com/p/385389671 注:以下所有问题,均为自己总结,若有错误之处,还请指出。 1、 k8s是什么?请说出你的了解? 答:Kubenetes是一个针对容器应用,进行自动部署,弹性伸缩和管理的开源系统。主要功能是生产环境中的容器编排。 K8S是Google公司推出的,它来源于由Google公司内部使用了15年的Borg系统,集结了Borg的精华。 2、 K8s架构的组成是什么? 答:和大多数分布式系统一样,K8S集群至少需要一个主节点(Master)和多个计算节点(Node)。 主节点主要用于暴露API,调度部署和节点的管理; 计算节点运行一个容器运行环境,一般是docker环境(类似docker环境的还有rkt),同时运行一个K8s的代理(kubelet)用于和master通信。计算节点也会运行一些额外的组件,像记录日志,节点监控,服务发现等等。计算节点是k8s集群中真正工作的节点。 K8S架构细分: 1、Master节点(默认不参加实际工作): Kubectl:客户端命令行工具,作为整个K8s集群的操作入口; Api Server:在K8s架构中承担的是“桥梁”的角色,作为资源操作的唯一入口,它提供了认证、授权、访问控制、API注册和发现等机制。客户端与k8s群集及K8s内部组件的通信,都要通过Api Server这个组件; Controller-manager:负责维护群集的状态,比如故障检测、自动扩展、滚动更新等; Scheduler:负责资源的调度,按照预定的调度策略将pod调度到相应的node节点上; Etcd:担任数据中心的角色,保存了整个群集的状态; 2、Node节点: Kubelet:负责维护容器的生命周期,同时也负责Volume和网络的管理,一般运行在所有的节点,是Node节点的代理,当Scheduler确定某个node上运行pod之后,会将pod的具体信息(image,volume)等发送给该节点的kubelet,kubelet根据这些信息创建和运行容器,并向master返回运行状态。(自动修复功能:如果某个节点中的容器宕机,它会尝试重启该容器,若重启无效,则会将该pod杀死,然后重新创建一个容器); Kube-proxy:Service在逻辑上代表了后端的多个pod。负责为Service提供cluster内部的服务发现和负载均衡(外界通过Service访问pod提供的服务时,Service接收到的请求后就是通过kube-proxy来转发到pod上的); container-runtime:是负责管理运行容器的软件,比如docker Pod:是k8s集群里面最小的单位。每个pod里边可以运行一个或多个container(容器),如果一个pod中有两个container,那么container的USR(用户)、MNT(挂载点)、PID(进程号)是相互隔离的,UTS(主机名和域名)、IPC(消息队列)、NET(网络栈)是相互共享的。我比较喜欢把pod来当做豌豆夹,而豌豆就是pod中的container; 3、 容器和主机部署应用的区别是什么? 答:容器的中心思想就是秒级启动;一次封装、到处运行;这是主机部署应用无法达到的效果,但同时也更应该注重容器的数据持久化问题。 另外,容器部署可以将各个服务进行隔离,互不影响,这也是容器的另一个核心概念。 4、请你说一下kubenetes针对pod资源对象的健康监测机制? 答:K8s中对于pod资源对象的健康状态检测,提供了三类probe(探针)来执行对pod的健康监测: 1) livenessProbe探针 可以根据用户自定义规则来判定pod是否健康,如果livenessProbe探针探测到容器不健康,则kubelet会根据其重启策略来决定是否重启,如果一个容器不包含livenessProbe探针,则kubelet会认为容器的livenessProbe探针的返回值永远成功。 2) ReadinessProbe探针 同样是可以根据用户自定义规则来判断pod是否健康,如果探测失败,控制器会将此pod从对应service的endpoint列表中移除,从此不再将任何请求调度到此Pod上,直到下次探测成功。 3) startupProbe探针 启动检查机制,应用一些启动缓慢的业务,避免业务长时间启动而被上面两类探针kill掉,这个问题也可以换另一种方式解决,就是定义上面两类探针机制时,初始化时间定义的长一些即可。 每种探测方法能支持以下几个相同的检查参数,用于设置控制检查时间: initialDelaySeconds:初始第一次探测间隔,用于应用启动的时间,防止应用还没启动而健康检查失败 periodSeconds:检查间隔,多久执行probe检查,默认为10s; timeoutSeconds:检查超时时长,探测应用timeout后为失败; successThreshold:成功探测阈值,表示探测多少次为健康正常,默认探测1次。 上面两种探针都支持以下三种探测方法: 1)Exec:通过执行命令的方式来检查服务是否正常,比如使用cat命令查看pod中的某个重要配置文件是否存在,若存在,则表示pod健康。反之异常。 Exec探测方式的yaml文件语法如下: spec: containers: - name: liveness image: k8s.gcr.io/busybox args: - /bin/sh - -c - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600 livenessProbe: #选择livenessProbe的探测机制 exec: #执行以下命令 command: - cat - /tmp/healthy initialDelaySeconds: 5 #在容器运行五秒后开始探测 periodSeconds: 5 #每次探测的时间间隔为5秒 在上面的配置文件中,探测机制为在容器运行5秒后,每隔五秒探测一次,如果cat命令返回的值为“0”,则表示健康,如果为非0,则表示异常。 2)Httpget:通过发送http/htps请求检查服务是否正常,返回的状态码为200-399则表示容器健康(注http get类似于命令curl -I)。 Httpget探测方式的yaml文件语法如下: spec: containers: - name: liveness image: k8s.gcr.io/liveness livenessProbe: #采用livenessProbe机制探测 httpGet: #采用httpget的方式 scheme:HTTP #指定协议,也支持https path: /healthz #检测是否可以访问到网页根目录下的healthz网页文件 port: 8080 #监听端口是8080 initialDelaySeconds: 3 #容器运行3秒后开始探测 periodSeconds: 3 #探测频率为3秒 上述配置文件中,探测方式为项容器发送HTTP GET请求,请求的是8080端口下的healthz文件,返回任何大于或等于200且小于400的状态码表示成功。任何其他代码表示异常。 3)tcpSocket:通过容器的IP和Port执行TCP检查,如果能够建立TCP连接,则表明容器健康,这种方式与HTTPget的探测机制有些类似,tcpsocket健康检查适用于TCP业务。 tcpSocket探测方式的yaml文件语法如下: spec: containers: - name: goproxy image: k8s.gcr.io/goproxy:0.1 ports: - containerPort: 8080 #这里两种探测机制都用上了,都是为了和容器的8080端口建立TCP连接 readinessProbe: tcpSocket: port: 8080 initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: tcpSocket: port: 8080 initialDelaySeconds: 15 periodSeconds: 20 在上述的yaml配置文件中,两类探针都使用了,在容器启动5秒后,kubelet将发送第一个readinessProbe探针,这将连接容器的8080端口,如果探测成功,则该pod为健康,十秒后,kubelet将进行第二次连接。 除了readinessProbe探针外,在容器启动15秒后,kubelet将发送第一个livenessProbe探针,仍然尝试连接容器的8080端口,如果连接失败,则重启容器。 探针探测的结果无外乎以下三者之一: Success:Container通过了检查; Failure:Container没有通过检查; Unknown:没有执行检查,因此不采取任何措施(通常是我们没有定义探针检测,默认为成功)。 若觉得上面还不够透彻,可以移步其官网文档: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ 5、 如何控制滚动更新过程? 答:可以通过下面的命令查看到更新时可以控制的参数: [root@master yaml]# kubectl explain deploy.spec.strategy.rollingUpdate maxSurge: 此参数控制滚动更新过程,副本总数超过预期pod数量的上限。可以是百分比,也可以是具体的值。默认为1。 (上述参数的作用就是在更新过程中,值若为3,那么不管三七二一,先运行三个pod,用于替换旧的pod,以此类推) maxUnavailable:此参数控制滚动更新过程中,不可用的Pod的数量。 (这个值和上面的值没有任何关系,举个例子:我有十个pod,但是在更新的过程中,我允许这十个pod中最多有三个不可用,那么就将这个参数的值设置为3,在更新的过程中,只要不可用的pod数量小于或等于3,那么更新过程就不会停止)。 6、K8s中镜像的下载策略是什么? 答:可通过命令“kubectl explain pod.spec.containers”来查看imagePullPolicy这行的解释。 K8s的镜像下载策略有三种:Always、Never、IFNotPresent; Always:镜像标签为latest时,总是从指定的仓库中获取镜像; Never:禁止从仓库中下载镜像,也就是说只能使用本地镜像; IfNotPresent:仅当本地没有对应镜像时,才从目标仓库中下载。 默认的镜像下载策略是:当镜像标签是latest时,默认策略是Always;当镜像标签是自定义时(也就是标签不是latest),那么默认策略是IfNotPresent。 7、 image的状态有哪些? Running:Pod所需的容器已经被成功调度到某个节点,且已经成功运行, Pending:APIserver创建了pod资源对象,并且已经存入etcd中,但它尚未被调度完成或者仍然处于仓库中下载镜像的过程 Unknown:APIserver无法正常获取到pod对象的状态,通常是其无法与所在工作节点的kubelet通信所致。 8、 pod的重启策略是什么? 答:可以通过命令“kubectl explain pod.spec”查看pod的重启策略。(restartPolicy字段) Always:但凡pod对象终止就重启,此为默认策略。 OnFailure:仅在pod对象出现错误时才重启 9、 Service这种资源对象的作用是什么? 答:用来给相同的多个pod对象提供一个固定的统一访问接口,常用于服务发现和服务访问。 10、版本回滚相关的命令? [root@master httpd-web]# kubectl apply -f httpd2-deploy1.yaml --record #运行yaml文件,并记录版本信息; [root@master httpd-web]# kubectl rollout history deployment httpd-devploy1 #查看该deployment的历史版本 [root@master httpd-web]# kubectl rollout undo deployment httpd-devploy1 --to-revision=1 #执行回滚操作,指定回滚到版本1 #在yaml文件的spec字段中,可以写以下选项(用于限制最多记录多少个历史版本): spec: revisionHistoryLimit: 5 #这个字段通过 kubectl explain deploy.spec 命令找到revisionHistoryLimit <integer>行获得 11、 标签与标签选择器的作用是什么? 标签:是当相同类型的资源对象越来越多的时候,为了更好的管理,可以按照标签将其分为一个组,为的是提升资源对象的管理效率。 标签选择器:就是标签的查询过滤条件。目前API支持两种标签选择器: 基于等值关系的,如:“=”、“”“==”、“!=”(注:“==”也是等于的意思,yaml文件中的matchLabels字段); 基于集合的,如:in、notin、exists(yaml文件中的matchExpressions字段); 注:in:在这个集合中;notin:不在这个集合中;exists:要么全在(exists)这个集合中,要么都不在(notexists); 使用标签选择器的操作逻辑: 在使用基于集合的标签选择器同时指定多个选择器之间的逻辑关系为“与”操作(比如:- {key: name,operator: In,values: [zhangsan,lisi]} ,那么只要拥有这两个值的资源,都会被选中); 使用空值的标签选择器,意味着每个资源对象都被选中(如:标签选择器的键是“A”,两个资源对象同时拥有A这个键,但是值不一样,这种情况下,如果使用空值的标签选择器,那么将同时选中这两个资源对象) 空的标签选择器(注意不是上面说的空值,而是空的,都没有定义键的名称),将无法选择出任何资源; 在基于集合的选择器中,使用“In”或者“Notin”操作时,其values可以为空,但是如果为空,这个标签选择器,就没有任何意义了。 两种标签选择器类型(基于等值、基于集合的书写方法): selector: matchLabels: #基于等值 app: nginx matchExpressions: #基于集合 - {key: name,operator: In,values: [zhangsan,lisi]} #key、operator、values这三个字段是固定的 - {key: age,operator: Exists,values:} #如果指定为exists,那么values的值一定要为空 12、 常用的标签分类有哪些? 标签分类是可以自定义的,但是为了能使他人可以达到一目了然的效果,一般会使用以下一些分类: 版本类标签(release):stable(稳定版)、canary(金丝雀版本,可以将其称之为测试版中的测试版)、beta(测试版); 环境类标签(environment):dev(开发)、qa(测试)、production(生产)、op(运维); 应用类(app):ui、as、pc、sc; 架构类(tier):frontend(前端)、backend(后端)、cache(缓存); 分区标签(partition):customerA(客户A)、customerB(客户B); 品控级别(Track):daily(每天)、weekly(每周)。 13、 有几种查看标签的方式? 答:常用的有以下三种查看方式: [root@master ~]# kubectl get pod --show-labels #查看pod,并且显示标签内容 [root@master ~]# kubectl get pod -L env,tier #显示资源对象标签的值 [root@master ~]# kubectl get pod -l env,tier #只显示符合键值资源对象的pod,而“-L”是显示所有的pod 14、 添加、修改、删除标签的命令? #对pod标签的操作 [root@master ~]# kubectl label pod label-pod abc=123 #给名为label-pod的pod添加标签 [root@master ~]# kubectl label pod label-pod abc=456 --overwrite #修改名为label-pod的标签 [root@master ~]# kubectl label pod label-pod abc- #删除名为label-pod的标签 [root@master ~]# kubectl get pod --show-labels #对node节点的标签操作 [root@master ~]# kubectl label nodes node01 disk=ssd #给节点node01添加disk标签 [root@master ~]# kubectl label nodes node01 disk=sss –overwrite #修改节点node01的标签 [root@master ~]# kubectl label nodes node01 disk- #删除节点node01的disk标签 15、 DaemonSet资源对象的特性? DaemonSet这种资源对象会在每个k8s集群中的节点上运行,并且每个节点只能运行一个pod,这是它和deployment资源对象的最大也是唯一的区别。所以,在其yaml文件中,不支持定义replicas,除此之外,与Deployment、RS等资源对象的写法相同。 它的一般使用场景如下: 在去做每个节点的日志收集工作; 监控每个节点的的运行状态; 16、 说说你对Job这种资源对象的了解? 答:Job与其他服务类容器不同,Job是一种工作类容器(一般用于做一次性任务)。使用常见不多,可以忽略这个问题。 #提高Job执行效率的方法: spec: parallelism: 2 #一次运行2个 completions: 8 #最多运行8个 template: metadata: 17、描述一下pod的生命周期有哪些状态? Pending:表示pod已经被同意创建,正在等待kube-scheduler选择合适的节点创建,一般是在准备镜像; Running:表示pod中所有的容器已经被创建,并且至少有一个容器正在运行或者是正在启动或者是正在重启; Succeeded:表示所有容器已经成功终止,并且不会再启动; Failed:表示pod中所有容器都是非0(不正常)状态退出; Unknown:表示无法读取Pod状态,通常是kube-controller-manager无法与Pod通信。 18、 创建一个pod的流程是什么? 客户端提交Pod的配置信息(可以是yaml文件定义好的信息)到kube-apiserver; Apiserver收到指令后,通知给controller-manager创建一个资源对象; Controller-manager通过api-server将pod的配置信息存储到ETCD数据中心中; Kube-scheduler检测到pod信息会开始调度预选,会先过滤掉不符合Pod资源配置要求的节点,然后开始调度调优,主要是挑选出更适合运行pod的节点,然后将pod的资源配置单发送到node节点上的kubelet组件上。 Kubelet根据scheduler发来的资源配置单运行pod,运行成功后,将pod的运行信息返回给scheduler,scheduler将返回的pod运行状况的信息存储到etcd数据中心。 19、 删除一个Pod会发生什么事情? 答:Kube-apiserver会接受到用户的删除指令,默认有30秒时间等待优雅退出,超过30秒会被标记为死亡状态,此时Pod的状态Terminating,kubelet看到pod标记为Terminating就开始了关闭Pod的工作; 关闭流程如下: pod从service的endpoint列表中被移除; 如果该pod定义了一个停止前的钩子,其会在pod内部被调用,停止钩子一般定义了如何优雅的结束进程; 进程被发送TERM信号(kill -14) 当超过优雅退出的时间后,Pod中的所有进程都会被发送SIGKILL信号(kill -9)。 20、 K8s的Service是什么? 答:Pod每次重启或者重新部署,其IP地址都会产生变化,这使得pod间通信和pod与外部通信变得困难,这时候,就需要Service为pod提供一个固定的入口。 Service的Endpoint列表通常绑定了一组相同配置的pod,通过负载均衡的方式把外界请求分配到多个pod上 21、 k8s是怎么进行服务注册的? 答:Pod启动后会加载当前环境所有Service信息,以便不同Pod根据Service名进行通信。 22、 k8s集群外流量怎么访问Pod? 答:可以通过Service的NodePort方式访问,会在所有节点监听同一个端口,比如:30000,访问节点的流量会被重定向到对应的Service上面。 23、 k8s数据持久化的方式有哪些? 答: 1)EmptyDir(空目录): 没有指定要挂载宿主机上的某个目录,直接由Pod内保部映射到宿主机上。类似于docker中的manager volume。 主要使用场景: 只需要临时将数据保存在磁盘上,比如在合并/排序算法中; 作为两个容器的共享存储,使得第一个内容管理的容器可以将生成的数据存入其中,同时由同一个webserver容器对外提供这些页面。 emptyDir的特性: 同个pod里面的不同容器,共享同一个持久化目录,当pod节点删除时,volume的数据也会被删除。如果仅仅是容器被销毁,pod还在,则不会影响volume中的数据。 总结来说:emptyDir的数据持久化的生命周期和使用的pod一致。一般是作为临时存储使用。 2)Hostpath: 将宿主机上已存在的目录或文件挂载到容器内部。类似于docker中的bind mount挂载方式。 这种数据持久化方式,运用场景不多,因为它增加了pod与节点之间的耦合。 一般对于k8s集群本身的数据持久化和docker本身的数据持久化会使用这种方式,可以自行参考apiService的yaml文件,位于:/etc/kubernetes/main…目录下。 3)PersistentVolume(简称PV): 基于NFS服务的PV,也可以基于GFS的PV。它的作用是统一数据持久化目录,方便管理。 在一个PV的yaml文件中,可以对其配置PV的大小,指定PV的访问模式: ReadWriteOnce:只能以读写的方式挂载到单个节点; ReadOnlyMany:能以只读的方式挂载到多个节点; ReadWriteMany:能以读写的方式挂载到多个节点。以及指定pv的回收策略: recycle:清除PV的数据,然后自动回收; Retain:需要手动回收; delete:删除云存储资源,云存储专用; PS:这里的回收策略指的是在PV被删除后,在这个PV下所存储的源文件是否删除)。 若需使用PV,那么还有一个重要的概念:PVC,PVC是向PV申请应用所需的容量大小,K8s集群中可能会有多个PV,PVC和PV若要关联,其定义的访问模式必须一致。定义的storageClassName也必须一致,若群集中存在相同的(名字、访问模式都一致)两个PV,那么PVC会选择向它所需容量接近的PV去申请,或者随机申请。 文章转自网络,如有侵权联系小编,将第一时间删除文章。

优秀的个人博客,低调大师

Kubernetes面试题超详细总结

来自:stackpush |责编:乐乐 链接:blog.csdn.net/huakai_sun/article/details/82378856 正文 一个目标:容器操作;两地三中心;四层服务发现;五种 Pod 共享资源;六个 CNI 常用插件;七层负载均衡;八种隔离维度;九个网络模型原则;十类 IP 地址;百级产品线;千级物理机;万级容器;相如无亿,k8s 有亿:亿级日服务人次。 一个目标:容器操作 Kubernetes(k8s)是自动化容器操作的开源平台。这些容器操作包括:部署、调度和节点集群间扩展。 具体功能: 自动化容器部署和复制。 实时弹性收缩容器规模。 容器编排成组,并提供容器间的负载均衡。 调度:容器在哪个机器上运行。 组成: kubectl:客户端命令行工具,作为整个系统的操作入口。 kube-apiserver:以 REST API 服务形式提供接口,作为整个系统的控制入口。 kube-controller-manager:执行整个系统的后台任务,包括节点状态状况、Pod 个数、Pods 和Service 的关联等。 kube-scheduler:负责节点资源管理,接收来自 kube-apiserver 创建 Pods 任务,并分配到某个节点。 etcd:负责节点间的服务发现和配置共享。 kube-proxy:运行在每个计算节点上,负责 Pod 网络代理。定时从 etcd 获取到 service 信息来做相应的策略。 kubelet:运行在每个计算节点上,作为 agent,接收分配该节点的 Pods 任务及管理容器,周期性获取容器状态,反馈给 kube-apiserver。 DNS:一个可选的 DNS 服务,用于为每个 Service 对象创建 DNS 记录,这样所有的 Pod 就可以通过 DNS 访问服务了。 下面是 k8s 的架构拓扑图: 两地三中心 两地三中心包括本地生产中心、本地灾备中心、异地灾备中心。 两地三中心要解决的一个重要问题就是数据一致性问题。 k8s 使用 etcd 组件作为一个高可用、强一致性的服务发现存储仓库。用于配置共享和服务发现。 它作为一个受到 Zookeeper 和 doozer 启发而催生的项目。除了拥有他们的所有功能之外,还拥有以下 4 个特点: 简单:基于 HTTP+JSON 的 API 让你用 curl 命令就可以轻松使用。 安全:可选 SSL 客户认证机制。 快速:每个实例每秒支持一千次写操作。 可信:使用 Raft 算法充分实现了分布式。 四层服务发现 先一张图解释一下网络七层协议: k8s 提供了两种方式进行服务发现: 环境变量:当创建一个 Pod 的时候,kubelet 会在该 Pod 中注入集群内所有 Service 的相关环境变量。需要注意的是,要想一个 Pod 中注入某个 Service 的环境变量,则必须 Service 要先比该 Pod 创建。这一点,几乎使得这种方式进行服务发现不可用。比如,一个 ServiceName 为 redis-master 的 Service,对应的 ClusterIP:Port 为 10.0.0.11:6379,则对应的环境变量为: DNS:可以通过 cluster add-on 的方式轻松的创建 KubeDNS 来对集群内的 Service 进行服务发现。 以上两种方式,一个是基于 TCP,DNS 基于 UDP,它们都是建立在四层协议之上。 五种 Pod 共享资源 Pod 是 k8s 最基本的操作单元,包含一个或多个紧密相关的容器。 一个 Pod 可以被一个容器化的环境看作应用层的“逻辑宿主机”;一个 Pod 中的多个容器应用通常是紧密耦合的,Pod 在 Node 上被创建、启动或者销毁;每个 Pod 里运行着一个特殊的被称之为 Volume 挂载卷,因此他们之间通信和数据交换更为高效。在设计时我们可以充分利用这一特性将一组密切相关的服务进程放入同一个 Pod 中。 同一个 Pod 里的容器之间仅需通过 localhost 就能互相通信。 一个 Pod 中的应用容器共享五种资源: PID 命名空间:Pod 中的不同应用程序可以看到其他应用程序的进程 ID。 网络命名空间:Pod 中的多个容器能够访问同一个IP和端口范围。 IPC 命名空间:Pod 中的多个容器能够使用 SystemV IPC 或 POSIX 消息队列进行通信。 UTS 命名空间:Pod 中的多个容器共享一个主机名。 Volumes(共享存储卷):Pod 中的各个容器可以访问在 Pod 级别定义的 Volumes。 Pod 的生命周期通过 Replication Controller 来管理;通过模板进行定义,然后分配到一个 Node 上运行,在 Pod 所包含容器运行结束后,Pod 结束。 Kubernetes 为 Pod 设计了一套独特的网络配置,包括为每个 Pod 分配一个IP地址,使用 Pod 名作为容器间通信的主机名等。在公众号顶级架构师回复“架构整洁”,获取惊喜礼包。 六个 CNI 常用插件 CNI(Container Network Interface)容器网络接口是 Linux 容器网络配置的一组标准和库,用户需要根据这些标准和库来开发自己的容器网络插件。CNI 只专注解决容器网络连接和容器销毁时的资源释放,提供一套框架。所以 CNI 可以支持大量不同的网络模式,并且容易实现。 下面用一张图表示六个 CNI 常用插件: 七层负载均衡 提负载均衡就不得不先提服务器之间的通信。 IDC(Internet Data Center)也可称数据中心、机房,用来放置服务器。IDC 网络是服务器间通信的桥梁。 上图里画了很多网络设备,它们都是干啥用的呢? 路由器、交换机、MGW/NAT 都是网络设备,按照性能、内外网划分不同的角色。 内网接入交换机:也称为 TOR(top of rack),是服务器接入网络的设备。每台内网接入交换机下联 40-48 台服务器,使用一个掩码为 /24 的网段作为服务器内网网段。 内网核心交换机:负责 IDC 内各内网接入交换机的流量转发及跨 IDC 流量转发。 MGW/NAT:MGW 即 LVS 用来做负载均衡,NAT 用于内网设备访问外网时做地址转换。 外网核心路由器:通过静态互联运营商或 BGP 互联美团统一外网平台。 先说说各层负载均衡: 二层负载均衡:基于 MAC 地址的二层负载均衡。 三层负载均衡:基于 IP 地址的负载均衡。 四层负载均衡:基于 IP+端口 的负载均衡。 七层负载均衡:基于 URL 等应用层信息的负载均衡。 这里用一张图来说说四层和七层负载均衡的区别: 上面四层服务发现讲的主要是 k8s原生的 kube-proxy 方式。k8s 关于服务的暴露主要是通过 NodePort 方式,通过绑定 minion 主机的某个端口,然后进行 Pod 的请求转发和负载均衡,但这种方式有下面的缺陷: Service 可能有很多个,如果每个都绑定一个 Node 主机端口的话,主机需要开放外围的端口进行服务调用,管理混乱。 无法应用很多公司要求的防火墙规则。 理想的方式是通过一个外部的负载均衡器,绑定固定的端口,比如 80;然后根据域名或者服务名向后面的 Service IP 转发。 Nginx 很好的解决了这个需求,但问题是如果有的新的服务加入,如何去修改并且加载这些Nginx 配置? Kubernetes 给出的方案就是Ingress。这是一个基于七层的方案。 八种隔离维度 k8s 集群调度这边需要对上面从上到下、从粗粒度到细粒度的隔离做相应的调度策略。 九个网络模型原则 k8s 网络模型要符合四个基础原则、三个网络要求原则、一个架构原则、一个 IP 原则。 每个 Pod 都拥有一个独立的 IP 地址,而且假定所有 Pod 都在一个可以直接连通的、扁平的网络空间中,不管是否运行在同一 Node 上都可以通过 Pod 的 IP 来访问。 k8s 中的 Pod 的 IP 是最小粒度 IP。同一个 Pod 内所有的容器共享一个网络堆栈,该模型称为IP-per-Pod 模型。 Pod 由 docker0 实际分配的 IP。 Pod 内部看到的 IP 地址和端口与外部保持一致。 同一个 Pod 内的不同容器共享网络,可以通过localhost来访问对方的端口,类似同一个虚拟机内不同的进程。 IP-per-Pod 模型从端口分配、域名解析、服务发现、负载均衡、应用配置等角度看,Pod 可以看做是一台独立的虚拟机或物理机。 所有容器都可以不用 NAT 的方式同别的容器通信。 所有节点都可以在不同 NAT 方式下同所有容器通信,反之亦然。 容器的地址和别人看到的地址是同一个地址。 要符合下面的架构: 由上图架构引申出来 IP 概念从集群外部到集群内部: 十类IP地址 大家都知道 IP 地址分为 ABCDE 类,另外还有五类特殊用途的 IP。 第一类 A 类:1.0.0.0-1226.255.255.255,默认子网掩码/8,即255.0.0.0。 B 类:128.0.0.0-191.255.255.255,默认子网掩码/16,即255.255.0.0。 C 类:192.0.0.0-223.255.255.255,默认子网掩码/24,即255.255.255.0。 D 类:224.0.0.0-239.255.255.255,一般用于组播。 E 类:240.0.0.0-255.255.255.255(其中255.255.255.255为全网广播地址)。E 类地址一般用于研究用途。 第二类 0.0.0.0 严格来说,0.0.0.0 已经不是一个真正意义上的 IP 地址了。它表示的是这样一个集合:所有不清楚的主机和目的网络。这里的不清楚是指在本机的路由表里没有特定条目指明如何到达。作为缺省路由。 127.0.0.1本机地址。 第三类 224.0.0.1组播地址。 如果你的主机开启了IRDP(internet路由发现,使用组播功能),那么你的主机路由表中应该有这样一条路由。 第四类 169.254.x.x 使用了 DHCP 功能自动获取了 IP 的主机,DHCP 服务器发生故障,或响应时间太长而超出了一个系统规定的时间,系统会为你分配这样一个 IP,代表网络不能正常运行。 第五类 10.xxx、172.16.x.x~172.31.x.x、192.168.x.x私有地址。 大量用于企业内部。保留这样的地址是为了避免亦或是哪个接入公网时引起地址混乱。 PS: 欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,欢迎转发分享给更多人。 版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢! 后台回复“加群”,带你进入高手如云交流群 推荐阅读: kube-proxy 如何与 iptables 配合使用 完美排查入侵者 Kubernetes 万字实战教程 (2021最新版) Kubernetes 常见问题总结 一文详解负载均衡和反向代理的真实区别 经典!Kubernetes 几个常见对象概述图 带宽、延时、吞吐率、PPS 这些都是啥? 一文读懂你身边的网络 Linux 环境变量配置全攻略 如何定位软中断CPU使用率过高的问题? 10大高性能开发利器 安全容器gVisor详解 TCP协议灵魂 12 问,总会用得到 QUIC也不是万能的 超详干货!Linux环境变量配置全攻略 为什么要选择智能网卡? 60,000毫秒内对Linux进行性能诊断 为什么Linux需要Swapping Linux系统常用命令速查手册 一文读懂容器网络发展 一文搞懂CDN加速原理 8 个问题彻底搞透 DNS 协议 三张图彻底搞懂iptables和netfilter 故障排查:K8s中Pod无法正常解析域名 网络排错大讲解~ OVS 和 OVS-DPDK 对比 微软出品的最新K8S学习指南3.0下载 ▼ 喜欢,就给我一个“在看” 10T 技术资源大放送!包括但不限于:云计算、虚拟化、微服务、大数据、网络、Linux、Docker、Kubernetes、Python、Go、C/C++、Shell、PPT 等。在公众号内回复「1024」,即可免费获取!! 本文分享自微信公众号 - Linux云计算网络(cloud_dev)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

面试重点: 来说说Dubbo SPI 机制

SPI是什么 SPI是一种简称,全名叫 Service Provider Interface,Java本身提供了一套SPI机制,SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类,这样可以在运行时,动态为接口替换实现类,这也是很多框架组件实现扩展功能的一种手段。 而今天要说的Dubbo SPI机制和Java SPI还是有一点区别的,Dubbo 并未使用 Java 原生的 SPI 机制,而是对他进行了改进增强,进而可以很容易地对Dubbo进行功能上的扩展。 学东西得带着问题去学,我们先提几个问题,再接着看 1.什么是SPI(开头已经解释了) 2.Dubbo SPI和Java原生的有什么区别 3.两种实现应该如何写出来 Java SPI是如何实现的 先定义一个接口: publicinterfaceCar{voidstartUp();} 然后创建两个类,都实现这个Car接口 publicclassTruckimplementsCar{@OverridepublicvoidstartUp(){System.out.println("Thetruckstarted");}}publicclassTrainimplementsCar{@OverridepublicvoidstartUp(){System.out.println("Thetrainstarted");}} 然后在项目META-INF/services文件夹下创建一个名称为接口的全限定名,com.example.demo.spi.Car。 文件内容写上实现类的全限定名,如下: com.example.demo.spi.Traincom.example.demo.spi.Truck 最后写一个测试代码: publicclassJavaSPITest{@TestpublicvoidtestCar(){ServiceLoader<Car>serviceLoader=ServiceLoader.load(Car.class);serviceLoader.forEach(Car::startUp);}} 执行完的输出结果: ThetrainstartedThetruckstarted Dubbo SPI是如何实现的 Dubbo 使用的SPI并不是Java原生的,而是重新实现了一套,其主要逻辑都在ExtensionLoader类中,逻辑也不难,后面会稍带讲一下 看看使用,和Java的差不了太多,基于前面的例子来看下,接口类需要加上@SPI注解: @SPIpublicinterfaceCar{voidstartUp();} 实现类不需要改动 配置文件需要放在META-INF/dubbo下面,配置写法有些区别,直接看代码: train=com.example.demo.spi.Traintruck=com.example.demo.spi.Truck 最后就是测试类了,先看代码: publicclassJavaSPITest{@TestpublicvoidtestCar(){ExtensionLoader<Car>extensionLoader=ExtensionLoader.getExtensionLoader(Car.class);Carcar=extensionLoader.getExtension("train");car.startUp();}} 执行结果: Thetrainstarted Dubbo SPI中常用的注解 @SPI 标记为扩展接口 @Adaptive自适应拓展实现类标志 @Activate 自动激活条件的标记 总结一下两者区别: 使用上的区别Dubbo使用 ExtensionLoader而不是 ServiceLoader了,其主要逻辑都封装在这个类中 配置文件存放目录不一样,Java的在 META-INF/services,Dubbo在 META-INF/dubbo, META-INF/dubbo/internal Java SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,并且又用不上,会造成大量资源被浪费 Dubbo SPI 增加了对扩展点 IOC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点 Java SPI加载过程失败,扩展点的名称是拿不到的。比如:JDK 标准的 ScriptEngine,getName() 获取脚本类型的名称,如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因是不会有任何提示的,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因 前面的3个问题是不是已经能回答出来了?是不是非常简单 Dubbo SPI源码分析 Dubbo SPI使用上是通过ExtensionLoader的getExtensionLoader方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。这其中,getExtensionLoader 方法用于从缓存中获取与拓展类对应的 ExtensionLoader,如果没有缓存,则创建一个新的实例,直接上代码: publicTgetExtension(Stringname){if(name==null||name.length()==0){thrownewIllegalArgumentException("Extensionname==null");}if("true".equals(name)){//获取默认的拓展实现类returngetDefaultExtension();}//用于持有目标对象Holder<Object>holder=cachedInstances.get(name);if(holder==null){cachedInstances.putIfAbsent(name,newHolder<Object>());holder=cachedInstances.get(name);}Objectinstance=holder.get();//DCLif(instance==null){synchronized(holder){instance=holder.get();if(instance==null){//创建扩展实例instance=createExtension(name);//设置实例到holder中holder.set(instance);}}}return(T)instance;} 上面这一段代码主要做的事情就是先检查缓存,缓存不存在创建扩展对象 接下来我们看看创建的过程: privateTcreateExtension(Stringname){//从配置文件中加载所有的扩展类,可得到“配置项名称”到“配置类”的映射关系表Class<?>clazz=getExtensionClasses().get(name);if(clazz==null){throwfindException(name);}try{Tinstance=(T)EXTENSION_INSTANCES.get(clazz);if(instance==null){//反射创建实例EXTENSION_INSTANCES.putIfAbsent(clazz,clazz.newInstance());instance=(T)EXTENSION_INSTANCES.get(clazz);}//向实例中注入依赖injectExtension(instance);Set<Class<?>>wrapperClasses=cachedWrapperClasses;if(wrapperClasses!=null&&!wrapperClasses.isEmpty()){//循环创建Wrapper实例for(Class<?>wrapperClass:wrapperClasses){//将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。//然后向Wrapper实例中注入依赖,最后将Wrapper实例再次赋值给instance变量instance=injectExtension((T)wrapperClass.getConstructor(type).newInstance(instance));}}returninstance;}catch(Throwablet){thrownewIllegalStateException("Extensioninstance(name:"+name+",class:"+type+")couldn'tbeinstantiated:"+t.getMessage(),t);}} 这段代码看着繁琐,其实也不难,一共只做了4件事情: 1.通过getExtensionClasses获取所有配置扩展类 2.反射创建对象 3.给扩展类注入依赖 4.将扩展类对象包裹在对应的Wrapper对象里面 我们在通过名称获取扩展类之前,首先需要根据配置文件解析出扩展类名称到扩展类的映射关系表,之后再根据扩展项名称从映射关系表中取出相应的拓展类即可。相关过程的代码如下: privateMap<String,Class<?>>getExtensionClasses(){//从缓存中获取已加载的拓展类Map<String,Class<?>>classes=cachedClasses.get();//DCLif(classes==null){synchronized(cachedClasses){classes=cachedClasses.get();if(classes==null){//加载扩展类classes=loadExtensionClasses();cachedClasses.set(classes);}}}returnclasses;} 这里也是先检查缓存,若缓存没有,则通过一次双重锁检查缓存,判空。此时如果 classes 仍为 null,则通过 loadExtensionClasses 加载拓展类。下面是 loadExtensionClasses 方法的代码 privateMap<String,Class<?>>loadExtensionClasses(){//获取SPI注解,这里的type变量是在调用getExtensionLoader方法时传入的finalSPIdefaultAnnotation=type.getAnnotation(SPI.class);if(defaultAnnotation!=null){Stringvalue=defaultAnnotation.value();if((value=value.trim()).length()>0){//对SPI注解内容进行切分String[]names=NAME_SEPARATOR.split(value);//检测SPI注解内容是否合法,不合法则抛出异常if(names.length>1){thrownewIllegalStateException("morethan1defaultextensionnameonextension...");}//设置默认名称,参考getDefaultExtension方法if(names.length==1){cachedDefaultName=names[0];}}}Map<String,Class<?>>extensionClasses=newHashMap<String,Class<?>>();//加载指定文件夹下的配置文件loadDirectory(extensionClasses,DUBBO_INTERNAL_DIRECTORY);loadDirectory(extensionClasses,DUBBO_DIRECTORY);loadDirectory(extensionClasses,SERVICES_DIRECTORY);returnextensionClasses;} loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。SPI 注解解析过程比较简单,无需多说。下面我们来看一下 loadDirectory 做了哪些事情 privatevoidloadDirectory(Map<String,Class<?>>extensionClasses,Stringdir){//fileName=文件夹路径+type全限定名StringfileName=dir+type.getName();try{Enumeration<java.net.URL>urls;ClassLoaderclassLoader=findClassLoader();//根据文件名加载所有的同名文件if(classLoader!=null){urls=classLoader.getResources(fileName);}else{urls=ClassLoader.getSystemResources(fileName);}if(urls!=null){while(urls.hasMoreElements()){java.net.URLresourceURL=urls.nextElement();//加载资源loadResource(extensionClasses,classLoader,resourceURL);}}}catch(Throwablet){logger.error("Exceptionoccurredwhenloadingextensionclass(interface:"+type+",descriptionfile:"+fileName+").",t);}} loadDirectory 方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源。我们继续跟下去,看一下 loadResource 方法的实现 privatevoidloadResource(Map<String,Class<?>>extensionClasses,ClassLoaderclassLoader,java.net.URLresourceURL){try{BufferedReaderreader=newBufferedReader(newInputStreamReader(resourceURL.openStream(),"utf-8"));try{Stringline;//按行读取配置内容while((line=reader.readLine())!=null){//定位#字符finalintci=line.indexOf('#');if(ci>=0){//截取#之前的字符串,#之后的内容为注释,需要忽略line=line.substring(0,ci);}line=line.trim();if(line.length()>0){try{Stringname=null;inti=line.indexOf('=');if(i>0){//以等于号=为界,截取键与值name=line.substring(0,i).trim();line=line.substring(i+1).trim();}if(line.length()>0){//加载类,并通过loadClass方法对类进行缓存loadClass(extensionClasses,resourceURL,Class.forName(line,true,classLoader),name);}}catch(Throwablet){IllegalStateExceptione=newIllegalStateException("Failedtoloadextensionclass...");}}}}finally{reader.close();}}catch(Throwablet){logger.error("Exceptionwhenloadextensionclass...");}} loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法用于主要用于操作缓存,该方法的逻辑如下: privatevoidloadClass(Map<String,Class<?>>extensionClasses,java.net.URLresourceURL,Class<?>clazz,Stringname)throwsNoSuchMethodException{if(!type.isAssignableFrom(clazz)){thrownewIllegalStateException("...");}//检测目标类上是否有Adaptive注解if(clazz.isAnnotationPresent(Adaptive.class)){if(cachedAdaptiveClass==null){//设置cachedAdaptiveClass缓存cachedAdaptiveClass=clazz;}elseif(!cachedAdaptiveClass.equals(clazz)){thrownewIllegalStateException("...");}//检测clazz是否是Wrapper类型}elseif(isWrapperClass(clazz)){Set<Class<?>>wrappers=cachedWrapperClasses;if(wrappers==null){cachedWrapperClasses=newConcurrentHashSet<Class<?>>();wrappers=cachedWrapperClasses;}//存储clazz到cachedWrapperClasses缓存中wrappers.add(clazz);//程序进入此分支,表明clazz是一个普通的拓展类}else{//检测clazz是否有默认的构造方法,如果没有,则抛出异常clazz.getConstructor();if(name==null||name.length()==0){//如果name为空,则尝试从Extension注解中获取name,或使用小写的类名作为namename=findAnnotationName(clazz);if(name.length()==0){thrownewIllegalStateException("...");}}//切分nameString[]names=NAME_SEPARATOR.split(name);if(names!=null&&names.length>0){Activateactivate=clazz.getAnnotation(Activate.class);if(activate!=null){//如果类上有Activate注解,则使用names数组的第一个元素作为键,//存储name到Activate注解对象的映射关系cachedActivates.put(names[0],activate);}for(Stringn:names){if(!cachedNames.containsKey(clazz)){//存储Class到名称的映射关系cachedNames.put(clazz,n);}Class<?>c=extensionClasses.get(n);if(c==null){//存储名称到Class的映射关系extensionClasses.put(n,clazz);}elseif(c!=clazz){thrownewIllegalStateException("...");}}}}} 综上,loadClass方法操作了不同的缓存,比如cachedAdaptiveClass、cachedWrapperClasses和cachedNames等等 到这里基本上关于缓存类加载的过程就分析完了,其他逻辑不难,认真地读下来加上Debug一下都能看懂的。 总结 从设计思想上来看的话,SPI是对迪米特法则和开闭原则的一种实现。 开闭原则:对修改关闭对扩展开放。这个原则在众多开源框架中都非常常见,Spring的IOC容器也是大量使用。 迪米特法则:也叫最小知识原则,可以解释为,不该直接依赖关系的类之间,不要依赖;有依赖关系的类之间,尽量只依赖必要的接口。 那Dubbo的SPI为什么不直接使用Spring的呢,这一点从众多开源框架中也许都能窥探一点端倪出来,因为本身作为开源框架是要融入其他框架或者一起运行的,不能作为依赖被依赖对象存在。 再者对于Dubbo来说,直接用Spring IOC AOP的话有一些架构臃肿,完全没必要,所以自己实现一套轻量级反而是最优解 往期推荐 还在为大数据平台搭建而烦恼吗?一柄神器送给你 还在为 Arthas 命令头疼? 来看看这个插件吧! Spring Cloud认证授权系列(一)基础概念 基础坑!新版Mac Big Sur 干翻了我的Nacos 讲真!这些攻击手段你知道吗 本文分享自微信公众号 - 架构技术专栏(jiagoujishu)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

面试,Parquet文件存储格式香在哪?

一、Parquet的组成 Parquet仅仅是一种存储格式,它是语言、平台无关的,并且不需要和任何一种数据处理框架绑定,目前能够和Parquet适配的组件包括下面这些,可以看出基本上通常使用的查询引擎和计算框架都已适配,并且可以很方便的将其它序列化工具生成的数据转换成Parquet格式。 查询引擎: Hive, Impala, Pig, Presto, Drill, Tajo, HAWQ, IBM Big SQL 计算框架: MapReduce, Spark, Cascading, Crunch, Scalding, Kite 数据模型: Avro, Thrift, Protocol Buffers, POJOs 项目组成 Parquet项目由以下几个子项目组成: parquet-format项目由java实现,它定义了所有Parquet元数据对象,Parquet的元数据是使用Apache Thrift进行序列化并存储在Parquet文件的尾部。 parquet-format项目由java实现,它包括多个模块,包括实现了读写Parquet文件的功能,并且提供一些和其它组件适配的工具,例如Hadoop Input/Output Formats、Hive Serde(目前Hive已经自带Parquet了)、Pig loaders等。 parquet-compatibility项目,包含不同编程语言之间(JAVA和C/C++)读写文件的测试代码。 parquet-cpp项目,它是用于用于读写Parquet文件的C++库。 下图展示了Parquet各个组件的层次以及从上到下交互的方式。 数据存储层定义了Parquet的文件格式,其中元数据在parquet-format中定义,包括Parquet原始类型定义、Page类型、编码类型、压缩类型等等。 对象转换层完成其他对象模型与Parquet内部数据模型的映射和转换,Parquet的编码方式使用的是striping and assembly算法。 对象模型层定义了如何读取Parquet文件的内容,这一层转换包括Avro、Thrift、PB等序列化格式、Hive serde等的适配。并且为了帮助大家理解和使用,Parquet提供了org.apache.parquet.example包实现了java对象和Parquet文件的转换。 数据模型 Parquet支持嵌套的数据模型,类似于Protocol Buffers,每一个数据模型的schema包含多个字段,每一个字段又可以包含多个字段,每一个字段有三个属性:重复数、数据类型和字段名,重复数可以是以下三种:required(出现1次),repeated(出现0次或多次),optional(出现0次或1次)。每一个字段的数据类型可以分成两种:group(复杂类型)和primitive(基本类型)。例如Dremel中提供的Document的schema示例,它的定义如下: message Document { required int64 DocId; optional group Links { repeated int64 Backward; repeated int64 Forward; } repeated group Name { repeated group Language { required string Code; optional string Country; } optional string Url; }} 可以把这个Schema转换成树状结构,根节点可以理解为repeated类型,如下图: 可以看出在Schema中所有的基本类型字段都是叶子节点,在这个Schema中一共存在6个叶子节点,如果把这样的Schema转换成扁平式的关系模型,就可以理解为该表包含六个列。Parquet中没有Map、Array这样的复杂数据结构,但是可以通过repeated和group组合来实现这样的需求。在这个包含6个字段的表中有以下几个字段和每一条记录中它们可能出现的次数: DocId int64 只能出现一次 Links.Backward int64 可能出现任意多次,但是如果出现0次则需要使用NULL标识 Links.Forward int64 同上 Name.Language.Code string 同上 Name.Language.Country string 同上 Name.Url string 同上 由于在一个表中可能存在出现任意多次的列,对于这些列需要标示出现多次或者等于NULL的情况,它是由Striping/Assembly算法实现的。 Striping/Assembly算法 上文介绍了Parquet的数据模型,在Document中存在多个非required列,由于Parquet一条记录的数据分散的存储在不同的列中,如何组合不同的列值组成一条记录是由Striping/Assembly算法决定的,在该算法中列的每一个值都包含三部分:value、repetition level和definition level。 Repetition Levels 为了支持repeated类型的节点,在写入的时候该值等于它和前面的值在哪一层节点是不共享的。在读取的时候根据该值可以推导出哪一层上需要创建一个新的节点,例如对于这样的一个schema和两条记录。 message nested { repeated group leve1 { repeated string leve2; }}r1:[[a,b,c,] , [d,e,f,g]]r2:[[h] , [i,j]] 计算repetition level值的过程如下: value=a是一条记录的开始,和前面的值(已经没有值了)在根节点(第0层)上是不共享的,所以repeated level=0. value=b它和前面的值共享了level1这个节点,但是level2这个节点上是不共享的,所以repeated level=2. 同理value=c, repeated level=2. value=d和前面的值共享了根节点(属于相同记录),但是在level1这个节点上是不共享的,所以repeated level=1. value=h和前面的值不属于同一条记录,也就是不共享任何节点,所以repeated level=0. 根据以上的分析每一个value需要记录的repeated level值如下: 在读取的时候,顺序的读取每一个值,然后根据它的repeated level创建对象,当读取value=a时repeated level=0,表示需要创建一个新的根节点(新记录),value=b时repeated level=2,表示需要创建一个新的level2节点,value=d时repeated level=1,表示需要创建一个新的level1节点,当所有列读取完成之后可以创建一条新的记录。本例中当读取文件构建每条记录的结果如下: 可以看出repeated level=0表示一条记录的开始,并且repeated level的值只是针对路径上的repeated类型的节点,因此在计算该值的时候可以忽略非repeated类型的节点,在写入的时候将其理解为该节点和路径上的哪一个repeated节点是不共享的,读取的时候将其理解为需要在哪一层创建一个新的repeated节点,这样的话每一列最大的repeated level值就等于路径上的repeated节点的个数(不包括根节点)。减小repeated level的好处能够使得在存储使用更加紧凑的编码方式,节省存储空间。 Definition Levels 有了repeated level我们就可以构造出一个记录了,为什么还需要definition levels呢?由于repeated和optional类型的存在,可能一条记录中某一列是没有值的,假设我们不记录这样的值就会导致本该属于下一条记录的值被当做当前记录的一部分,从而造成数据的错误,因此对于这种情况需要一个占位符标示这种情况。 definition level的值仅仅对于空值是有效的,表示在该值的路径上第几层开始是未定义的,对于非空的值它是没有意义的,因为非空值在叶子节点是定义的,所有的父节点也肯定是定义的,因此它总是等于该列最大的definition levels。例如下面的schema。 message ExampleDefinitionLevel { optional group a { optional group b { optional string c; } }} 它包含一个列a.b.c,这个列的的每一个节点都是optional类型的,当c被定义时a和b肯定都是已定义的,当c未定义时我们就需要标示出在从哪一层开始时未定义的,如下面的值: 由于definition level只需要考虑未定义的值,而对于repeated类型的节点,只要父节点是已定义的,该节点就必须定义(例如Document中的DocId,每一条记录都该列都必须有值,同样对于Language节点,只要它定义了Code必须有值),所以计算definition level的值时可以忽略路径上的required节点,这样可以减小definition level的最大值,优化存储。 一个完整的例子 本节我们使用Dremel论文中给的Document示例和给定的两个值r1和r2展示计算repeated level和definition level的过程,这里把未定义的值记录为NULL,使用R表示repeated level,D表示definition level。 首先看DocuId这一列,对于r1,DocId=10,由于它是记录的开始并且是已定义的,所以R=0,D=0,同样r2中的DocId=20,R=0,D=0。 对于Links.Forward这一列,在r1中,它是未定义的但是Links是已定义的,并且是该记录中的第一个值,所以R=0,D=1,在r1中该列有两个值,value1=10,R=0(记录中该列的第一个值),D=2(该列的最大definition level)。 对于Name.Url这一列,r1中它有三个值,分别为url1=’http://A‘,它是r1中该列的第一个值并且是定义的,所以R=0,D=2;value2=’http://B‘,和上一个值value1在Name这一层是不相同的,所以R=1,D=2;value3=NULL,和上一个值value2在Name这一层是不相同的,所以R=1,但它是未定义的,而Name这一层是定义的,所以D=1。r2中该列只有一个值value3=’http://C‘,R=0,D=2. 最后看一下Name.Language.Code这一列,r1中有4个值,value1=’en-us’,它是r1中的第一个值并且是已定义的,所以R=0,D=2(由于Code是required类型,这一列repeated level的最大值等于2);value2=’en’,它和value1在Language这个节点是不共享的,所以R=2,D=2;value3=NULL,它是未定义的,但是它和前一个值在Name这个节点是不共享的,在Name这个节点是已定义的,所以R=1,D=1;value4=’en-gb’,它和前一个值在Name这一层不共享,所以R=1,D=2。在r2中该列有一个值,它是未定义的,但是Name这一层是已定义的,所以R=0,D=1. Parquet文件格式 Parquet文件是以二进制方式存储的,所以是不可以直接读取的,文件中包括该文件的数据和元数据,因此Parquet格式文件是自解析的。在HDFS文件系统和Parquet文件中存在如下几个概念。 HDFS块(Block):它是HDFS上的最小的副本单位,HDFS会把一个Block存储在本地的一个文件并且维护分散在不同的机器上的多个副本,通常情况下一个Block的大小为256M、512M等。 HDFS文件(File):一个HDFS的文件,包括数据和元数据,数据分散存储在多个Block中。 行组(Row Group):按照行将数据物理上划分为多个单元,每一个行组包含一定的行数,在一个HDFS文件中至少存储一个行组,Parquet读写的时候会将整个行组缓存在内存中,所以如果每一个行组的大小是由内存大的小决定的,例如记录占用空间比较小的Schema可以在每一个行组中存储更多的行。 列块(Column Chunk):在一个行组中每一列保存在一个列块中,行组中的所有列连续的存储在这个行组文件中。一个列块中的值都是相同类型的,不同的列块可能使用不同的算法进行压缩。 页(Page):每一个列块划分为多个页,一个页是最小的编码的单位,在同一个列块的不同页可能使用不同的编码方式。 文件格式 通常情况下,在存储Parquet数据的时候会按照Block大小设置行组的大小,由于一般情况下每一个Mapper任务处理数据的最小单位是一个Block,这样可以把每一个行组由一个Mapper任务处理,增大任务执行并行度。Parquet文件的格式如下图所示 上图展示了一个Parquet文件的内容,一个文件中可以存储多个行组,文件的首位都是该文件的Magic Code,用于校验它是否是一个Parquet文件,Footer length了文件元数据的大小,通过该值和文件长度可以计算出元数据的偏移量,文件的元数据中包括每一个行组的元数据信息和该文件存储数据的Schema信息。除了文件中每一个行组的元数据,每一页的开始都会存储该页的元数据,在Parquet中,有三种类型的页:数据页、字典页和索引页。数据页用于存储当前行组中该列的值,字典页存储该列值的编码字典,每一个列块中最多包含一个字典页,索引页用来存储当前行组下该列的索引,目前Parquet中还不支持索引页,但是在后面的版本中增加。 在执行MR任务的时候可能存在多个Mapper任务的输入是同一个Parquet文件的情况,每一个Mapper通过InputSplit标示处理的文件范围,如果多个InputSplit跨越了一个Row Group,Parquet能够保证一个Row Group只会被一个Mapper任务处理。 映射下推(Project PushDown) 说到列式存储的优势,映射下推是最突出的,它意味着在获取表中原始数据时只需要扫描查询中需要的列,由于每一列的所有值都是连续存储的,所以分区取出每一列的所有值就可以实现TableScan算子,而避免扫描整个表文件内容。 在Parquet中原生就支持映射下推,执行查询的时候可以通过Configuration传递需要读取的列的信息,这些列必须是Schema的子集,映射每次会扫描一个Row Group的数据,然后一次性得将该Row Group里所有需要的列的Cloumn Chunk都读取到内存中,每次读取一个Row Group的数据能够大大降低随机读的次数,除此之外,Parquet在读取的时候会考虑列是否连续,如果某些需要的列是存储位置是连续的,那么一次读操作就可以把多个列的数据读取到内存。 谓词下推(Predicate PushDown) 在数据库之类的查询系统中最常用的优化手段就是谓词下推了,通过将一些过滤条件尽可能的在最底层执行可以减少每一层交互的数据量,从而提升性能,例如”select count(1) from A Join B on A.id = B.id where A.a > 10 and B.b < 100”SQL查询中,在处理Join操作之前需要首先对A和B执行TableScan操作,然后再进行Join,再执行过滤,最后计算聚合函数返回,但是如果把过滤条件A.a > 10和B.b < 100分别移到A表的TableScan和B表的TableScan的时候执行,可以大大降低Join操作的输入数据。 无论是行式存储还是列式存储,都可以在将过滤条件在读取一条记录之后执行以判断该记录是否需要返回给调用者,在Parquet做了更进一步的优化,优化的方法时对每一个Row Group的每一个Column Chunk在存储的时候都计算对应的统计信息,包括该Column Chunk的最大值、最小值和空值个数。通过这些统计值和该列的过滤条件可以判断该Row Group是否需要扫描。另外Parquet未来还会增加诸如Bloom Filter和Index等优化数据,更加有效的完成谓词下推。 在使用Parquet的时候可以通过如下两种策略提升查询性能:1、类似于关系数据库的主键,对需要频繁过滤的列设置为有序的,这样在导入数据的时候会根据该列的顺序存储数据,这样可以最大化的利用最大值、最小值实现谓词下推。2、减小行组大小和页大小,这样增加跳过整个行组的可能性,但是此时需要权衡由于压缩和编码效率下降带来的I/O负载。 性能 相比传统的行式存储,Hadoop生态圈近年来也涌现出诸如RC、ORC、Parquet的列式存储格式,它们的性能优势主要体现在两个方面:1、更高的压缩比,由于相同类型的数据更容易针对不同类型的列使用高效的编码和压缩方式。2、更小的I/O操作,由于映射下推和谓词下推的使用,可以减少一大部分不必要的数据扫描,尤其是表结构比较庞大的时候更加明显,由此也能够带来更好的查询性能 上图是展示了使用不同格式存储TPC-H和TPC-DS数据集中两个表数据的文件大小对比,可以看出Parquet较之于其他的二进制文件存储格式能够更有效的利用存储空间,而新版本的Parquet(2.0版本)使用了更加高效的页存储方式,进一步的提升存储空间 上图展示了Twitter在Impala中使用不同格式文件执行TPC-DS基准测试的结果,测试结果可以看出Parquet较之于其他的行式存储格式有较明显的性能提升。 上图展示了criteo公司在Hive中使用ORC和Parquet两种列式存储格式执行TPC-DS基准测试的结果,测试结果可以看出在数据存储方面,两种存储格式在都是用snappy压缩的情况下量中存储格式占用的空间相差并不大,查询的结果显示Parquet格式稍好于ORC格式,两者在功能上也都有优缺点,Parquet原生支持嵌套式数据结构,而ORC对此支持的较差,这种复杂的Schema查询也相对较差;而Parquet不支持数据的修改和ACID,但是ORC对此提供支持,但是在OLAP环境下很少会对单条数据修改,更多的则是批量导入。 项目发展 自从2012年由Twitter和Cloudera共同研发Parquet开始,该项目一直处于高速发展之中,并且在项目之初就将其贡献给开源社区,2013年,Criteo公司加入开发并且向Hive社区提交了向hive集成Parquet的patch(HIVE-5783),在Hive 0.13版本之后正式加入了Parquet的支持;之后越来越多的查询引擎对此进行支持,也进一步带动了Parquet的发展。 目前Parquet正处于向2.0版本迈进的阶段,在新的版本中实现了新的Page存储格式,针对不同的类型优化编码算法,另外丰富了支持的原始类型,增加了Decimal、Timestamp等类型的支持,增加更加丰富的统计信息,例如Bloon Filter,能够尽可能得将谓词下推在元数据层完成。 总结 本文介绍了一种支持嵌套数据模型对的列式存储系统Parquet,作为大数据系统中OLAP查询的优化方案,它已经被多种查询引擎原生支持,并且部分高性能引擎将其作为默认的文件存储格式。通过数据编码和压缩,以及映射下推和谓词下推功能,Parquet的性能也较之其它文件格式有所提升,可以预见,随着数据模型的丰富和Ad hoc查询的需求,Parquet将会被更广泛的使用。 欢迎加入 数据分析|数仓技术交流群 。 进群方式:请加微信(微信号:dataclub_bigdata),回复:数据,通过审核会拉你进群。 (备注:行业-职位-城市) 历史好文推荐 从0到1搭建大数据平台之计算存储系统 从0到1搭建大数据平台之调度系统 从0到1搭建大数据平台之数据采集系统 如何从0到1搭建大数据平台 从0到1搭建自助分析平台 福利时刻 01. 后台回复「数据」,即可领取大数据经典资料。 02.后台回复「转型」,即可传统数据仓库转型大数据必学资料。 03. 后台回复「加群」,或添加一哥微信ID:dataclub_bigdata拉您入群(大数据|数仓|分析)或领取资料。 Q:关于大数据,你还想了解什么? 欢迎大家扫描下方二维码订阅「数据社」内容并推荐给更多数据方向的朋友,希望有更多机会和大家交流。 !关注不迷路~ 各种福利、资源定期分享! 你也「 在看 」吗? 👇 本文分享自微信公众号 - 数据社(DataClub)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

JVM面试掌握这些,就稳了

Java运行时数据区: Java虚拟机在执行Java程序的过程中会将其管理的内存划分为若干个不同的数据区域,这些区域有各自的用途、创建和销毁的时间,有些区域随虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束来建立和销毁。Java虚拟机所管理的内存包括以下几个运行时数据区域,如图: 1、程序计数器:指向当前线程正在执行的字节码指令。线程私有的。 2、虚拟机栈:虚拟机栈是Java执行方法的内存模型。每个方法被执行的时候,都会创建一个栈帧,把栈帧压人栈,当方法正常返回或者抛出未捕获的异常时,栈帧就会出栈。 (1)栈帧:栈帧存储方法的相关信息,包含局部变量数表、返回值、操作数栈、动态链接 a、局部变量表:包含了方法执行过程中的所有变量。局部变量数组所需要的空间在编译期间完成分配,在方法运行期间不会改变局部变量数组的大小。 b、返回值:如果有返回值的话,压入调用者栈帧中的操作数栈中,并且把PC的值指向 方法调用指令 后面的一条指令地址。 c、操作数栈:操作变量的内存模型。操作数栈的最大深度在编译的时候已经确定(写入方法区code属性的max_stacks项中)。操作数栈的的元素可以是任意Java类型,包括long和double,32位数据占用栈空间为1,64位数据占用2。方法刚开始执行的时候,栈是空的,当方法执行过程中,各种字节码指令往栈中存取数据。 d、动态链接:每个栈帧都持有在运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。 (2)线程私有; 3、本地方法栈: (1)调用本地native的内存模型。 (2)线程独享。 4、方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据 (1)线程共享的; (2)运行时常量池: A、是方法区的一部分B、存放编译期生成的各种字面量和符号引用C、Class文件中除了存有类的版本、字段、方法、接口等描述信息,还有一项是常量池,存有这个类的 编译期生成的各种字面量和符号引用,这部分内容将在类加载后,存放到方法区的运行时常量池中。 5、堆(Heap):Java对象存储的地方 (1)Java堆是虚拟机管理的内存中最大的一块 (2)Java堆是所有线程共享的区域 (3)在虚拟机启动时创建 (4)此内存区域的唯一目的就是存放对象实例,几乎所有对象实例都在这里分配内存。存放new生成的对象和数组 (5)Java堆是垃圾收集器管理的内存区域,因此很多时候称为“GC堆” JMM Java内存模型: 1、 Java的并发采用“共享内存”模型,线程之间通过读写内存的公共状态进行通讯。多个线程之间是不能通过直接传递数据交互的,它们之间交互只能通过共享变量实现。 2、 主要目的是定义程序中各个变量的访问规则。 3、 Java内存模型规定所有变量都存储在主内存中,每个线程还有自己的工作内存。 (1) 线程的工作内存中保存了被该线程使用到的变量的拷贝(从主内存中拷贝过来),线程对变量的所有操作都必须在工作内存中执行,而不能直接访问主内存中的变量。 (2) 不同线程之间无法直接访问对方工作内存的变量,线程间变量值的传递都要通过主内存来完成。 (3) 主内存主要对应Java堆中实例数据部分。工作内存对应于虚拟机栈中部分区域。 4、Java线程之间的通信由内存模型JMM(Java Memory Model)控制。 (1)JMM决定一个线程对变量的写入何时对另一个线程可见。 (2)线程之间共享变量存储在主内存中 (3)每个线程有一个私有的本地内存,里面存储了读/写共享变量的副本。 (4)JMM通过控制每个线程的本地内存之间的交互,来为程序员提供内存可见性保证。 5、可见性、有序性: (1)当一个共享变量在多个本地内存中有副本时,如果一个本地内存修改了该变量的副本,其他变量应该能够看到修改后的值,此为可见性。 (2)保证线程的有序执行,这个为有序性。(保证线程安全) 6、内存间交互操作: (1)lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。 (2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。 (3)read(读取):作用于主内存变量,把主内存的一个变量读取到工作内存中。 (4)load(载入):作用于工作内存,把read操作读取到工作内存的变量载入到工作内存的变量副本中 (5)use(使用):作用于工作内存的变量,把工作内存中的变量值传递给一个执行引擎。 (6)assign(赋值):作用于工作内存的变量。把执行引擎接收到的值赋值给工作内存的变量。 (7)store(存储):把工作内存的变量的值传递给主内存 (8)write(写入):把store操作的值入到主内存的变量中 6.1、注意: (1)不允许read、load、store、write操作之一单独出现 (2)不允许一个线程丢弃assgin操作 (3)不允许一个线程不经过assgin操作,就把工作内存中的值同步到主内存中 (4)一个新的变量只能在主内存中生成 (5)一个变量同一时刻只允许一条线程对其进行lock操作。但lock操作可以被同一条线程执行多次,只有执行相同次数的unlock操作,变量才会解锁 (6)如果对一个变量进行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或者assgin操作初始化变量的值。 (7)如果一个变量没有被锁定,不允许对其执行unlock操作,也不允许unlock一个被其他线程锁定的变量 (8)对一个变量执行unlock操作之前,需要将该变量同步回主内存中 堆的内存划分: Java堆的内存划分如图所示,分别为年轻代、Old Memory(老年代)、Perm(永久代)。其中在Jdk1.8中,永久代被移除,使用MetaSpace代替。 1、新生代: (1)使用复制清除算法(Copinng算法),原因是年轻代每次GC都要回收大部分对象。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。 (2)分为Eden、Survivor From、Survivor To,比例默认为8:1:1 (3)内存不足时发生Minor GC2 2、老年代: (1)采用标记-整理算法(mark-compact),原因是老年代每次GC只会回收少部分对象。 3、Perm:用来存储类的元数据,也就是方法区。 (1)Perm的废除:在jdk1.8中,Perm被替换成MetaSpace,MetaSpace存放在本地内存中。原因是永久代进场内存不够用,或者发生内存泄漏。 (2)MetaSpace(元空间):元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。 4、堆内存的划分在JVM里面的示意图: GC垃圾回收: 一、 判断对象是否要回收的方法:可达性分析法 1、 可达性分析法:通过一系列“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象不一定会成为可回收对象。进入DEAD状态的线程还可以恢复,GC不会回收它的内存。(把一些对象当做root对象,JVM认为root对象是不可回收的,并且root对象引用的对象也是不可回收的) 2、 以下对象会被认为是root对象:(1) 虚拟机栈(栈帧中本地变量表)中引用的对象(2) 方法区中静态属性引用的对象(3) 方法区中常量引用的对象(4) 本地方法栈中Native方法引用的对象 3、 对象被判定可被回收,需要经历两个阶段:(1) 第一个阶段是可达性分析,分析该对象是否可达(2) 第二个阶段是当对象没有重写finalize()方法或者finalize()方法已经被调用过,虚拟机认为该对象不可以被救活,因此回收该对象。(finalize()方法在垃圾回收中的作用是,给该对象一次救活的机会) 4、 方法区中的垃圾回收:(1) 常量池中一些常量、符号引用没有被引用,则会被清理出常量池(2) 无用的类:被判定为无用的类,会被清理出方法区。判定方法如下:A、 该类的所有实例被回收B、 加载该类的ClassLoader被回收C、 该类的Class对象没有被引用 5、 finalize():(1) GC垃圾回收要回收一个对象的时候,调用该对象的finalize()方法。然后在下一次垃圾回收的时候,才去回收这个对象的内存。(2) 可以在该方法里面,指定一些对象在释放前必须执行的操作。 二、 发现虚拟机频繁full GC时应该怎么办:(full GC指的是清理整个堆空间,包括年轻代和永久代) (1) 首先用命令查看触发GC的原因是什么 jstat –gccause 进程id (2) 如果是System.gc(),则看下代码哪里调用了这个方法 (3) 如果是heap inspection(内存检查),可能是哪里执行jmap –histo[:live]命令 (4) 如果是GC locker,可能是程序依赖的JNI库的原因 三、常见的垃圾回收算法: 1、Mark-Sweep(标记-清除算法): (1)思想:标记清除算法分为两个阶段,标记阶段和清除阶段。标记阶段任务是标记出所有需要回收的对象,清除阶段就是清除被标记对象的空间。 (2)优缺点:实现简单,容易产生内存碎片 2、Copying(复制清除算法): (1)思想:将可用内存划分为大小相等的两块,每次只使用其中的一块。当进行垃圾回收的时候了,把其中存活对象全部复制到另外一块中,然后把已使用的内存空间一次清空掉。 (2)优缺点:不容易产生内存碎片;可用内存空间少;存活对象多的话,效率低下。 3、Mark-Compact(标记-整理算法): (1)思想:先标记存活对象,然后把存活对象向一边移动,然后清理掉端边界以外的内存。 (2)优缺点:不容易产生内存碎片;内存利用率高;存活对象多并且分散的时候,移动次数多,效率低下 4、分代收集算法:(目前大部分JVM的垃圾收集器所采用的算法): 思想:把堆分成新生代和老年代。(永久代指的是方法区) (1) 因为新生代每次垃圾回收都要回收大部分对象,所以新生代采用Copying算法。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。 (2) 由于老年代每次只回收少量的对象,因此采用mark-compact算法。 (3) 在堆区外有一个永久代。对永久代的回收主要是无效的类和常量 5、GC使用时对程序的影响?垃圾回收会影响程序的性能,Java虚拟机必须要追踪运行程序中的有用对象,然后释放没用对象,这个过程消耗处理器时间 6、几种不同的垃圾回收类型: (1)Minor GC:从年轻代(包括Eden、Survivor区)回收内存。 A、当JVM无法为一个新的对象分配内存的时候,越容易触发Minor GC。所以分配率越高,内存越来越少,越频繁执行Minor GCB、执行Minor GC操作的时候,不会影响到永久代(Tenured)。从永久代到年轻代的引用,被当成GC Roots,从年轻代到老年代的引用在标记阶段直接被忽略掉。 (2)Major GC:清理整个老年代,当eden区内存不足时触发。 (3)Full GC:清理整个堆空间,包括年轻代和老年代。当老年代内存不足时触发 HotSpot 虚拟机详解: 1、 Java对象创建过程: (1)虚拟机遇到一条new指令时,首先检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经加载、连接和初始化。如果没有,就执行该类的加载过程。 (2)为该对象分配内存。 A、假设Java堆是规整的,所有用过的内存放在一边,空闲的内存放在另外一边,中间放着一个指针作为分界点的指示器。那分配内存只是把指针向空闲空间那边挪动与对象大小相等的距离,这种分配称为“指针碰撞”。 B、假设Java堆不是规整的,用过的内存和空闲的内存相互交错,那就没办法进行“指针碰撞”。虚拟机通过维护一个列表,记录哪些内存块是可用的,在分配的时候找出一块足够大的空间分配给对象实例,并更新表上的记录。这种分配方式称为“空闲列表“。 C、使用哪种分配方式由Java堆是否规整决定。Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。 D、分配对象保证线程安全的做法:虚拟机使用CAS失败重试的方式保证更新操作的原子性。(实际上还有另外一种方案:每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,TLAB。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才进行同步锁定。虚拟机是否使用TLAB,由-XX:+/-UseTLAB参数决定) (3)虚拟机为分配的内存空间初始化为零值(默认值) (4)虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到对象的元数据信息、对象的Hash码、对象的GC分代年龄等信息。这些信息存放在对象的对象头中。 (5) 执行方法,把对象按照程序员的意愿进行初始化。 2、 对象的定位访问的方式(通过引用如何去定位到堆上的具体对象的位置): (1)句柄:使用句柄的方式,Java堆中将会划分出一块内存作为作为句柄池,引用中存储的就是对象的句柄的地址。而句柄中包含了对象实例数据和对象类型数据的地址。 (2)直接指针:使用直接指针的方式,引用中存储的就是对象的地址。Java堆对象的布局必须必须考虑如何去访问对象类型数据。 (3)两种方式各有优点:A、使用句柄访问的好处是引用中存放的是稳定的句柄地址,当对象被移动(比如说垃圾回收时移动对象),只会改变句柄中实例数据指针,而引用本身不会被修改。B、使用直接指针,节省了一次指针定位的时间开销。 3、HotSpot的GC算法实现: (1)HotSpot怎么快速找到GC Root?HotSpot使用一组称为OopMap的数据结构。在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在栈和寄存器中哪些位置是引用。这样子,在GC扫描的时候,就可以直接知道哪些是可达对象了。 (2)安全点:A、HotSpot只在特定的位置生成OopMap,这些位置称为安全点。B、程序执行过程中并非所有地方都可以停下来开始GC,只有在到达安全点是才可以暂停。C、安全点的选定基本上以“是否具有让程序长时间执行“的特征选定的。比如说方法调用、循环跳转、异常跳转等。具有这些功能的指令才会产生Safepoint。 (3)中断方式: A、抢占式中断:在GC发生时,首先把所有线程中断,如果发现有线程不在安全点上,就恢复线程,让它跑到安全点上。B、主动式中断:GC需要中断线程时,不直接对线程操作,仅仅设置一个标志,各个线程执行时主动去轮询这个标志,当发现中断标记为真就自己中断挂起。轮询标记的地方和安全点是重合的。 (5)安全区域:一段代码片段中,对象的引用关系不会发生变化,在这个区域中任何地方开始GC都是安全的。在线程进入安全区域时,它首先标志自己已经进入安全区域,在这段时间里,当JVM发起GC时,就不用管进入安全区域的线程了。在线程将要离开安全区域时,它检查系统是否完成了GC过程,如果完成了,它就继续前行。否则,它就必须等待直到收到可以离开安全区域的信号。 4、 GC时为什么要停顿所有Java线程? 因为GC先进行可达性分析。可达性分析是判断GC Root对象到其他对象是否可达,假如分析过程中对象的引用关系在不断变化,分析结果的准确性就无法得到保证。 5、 CMS收集器: (1)一种以获取最短回收停顿时间为目标的收集器。 (2)一般用于互联网站或者B/S系统的服务端 (3)基于标记-清除算法的实现,不过更为复杂,整个过程为4个步骤: A、初始标记:标记GC Root能直接引用的对象B、并发标记:利用多线程对每个GC Root对象进行tracing搜索,在堆中查找其下所有能关联到的对象。C、重新标记:为了修正并发标记期间,用户程序继续运作而导致标志产生变动的那一部分对象的标记记录。D、并发清除:利用多个线程对标记的对象进行清除 (4)由于耗时最长的并发标记和并发清除操作都是用户线程一起工作,所以总体来说,CMS的内存回收工作是和用户线程一起并发执行的。 (5)缺点: A、对CPU资源占用比较多。可能因为占用一部分CPU资源导致应用程序响应变慢。B、CMS无法处理浮动垃圾。在并发清除阶段,用户程序继续运行,可能产生新的内存垃圾,这一部分垃圾出现在标记过程之后,因此,CMS无法清除。这部分垃圾称为“浮动垃圾“C、需要预留一部分内存,在垃圾回收时,给用户程序使用。D、基于标记-清除算法,容易产生大量内存碎片,导致full GC(full GC进行内存碎片的整理) 6、 对象头部分的内存布局:HotSpot的对象头分为两部分,第一部分用于存储对象自身的运行时数据,比如哈希码、GC分代年龄等。另外一部分用于指向方法区对象类型数据的指针。 7、 偏向锁:偏向锁偏向于第一个获取它的线程,如果在接下来的执行过程,没有其他线程获取该锁,则持有偏向锁的线程永远不需要同步。(当一个线程获取偏向锁,它每次进入这个锁相关的同步块,虚拟机不在进行任何同步操作。当有另外一个线程尝试获取这个锁时,偏向模式宣告结束) JVM优化: 1、一般来说,当survivor区不够大或者占用量达到50%,就会把一些对象放到老年区。通过设置合理的eden区,survivor区及使用率,可以将年轻对象保存在年轻代,从而避免full GC,使用-Xmn设置年轻代的大小 2、对于占用内存比较多的大对象,一般会选择在老年代分配内存。如果在年轻代给大对象分配内存,年轻代内存不够了,就要在eden区移动大量对象到老年代,然后这些移动的对象可能很快消亡,因此导致full GC。通过设置参数:-XX:PetenureSizeThreshold=1000000,单位为B,标明对象大小超过1M时,在老年代(tenured)分配内存空间。 3、一般情况下,年轻对象放在eden区,当第一次GC后,如果对象还存活,放到survivor区,此后,每GC一次,年龄增加1,当对象的年龄达到阈值,就被放到tenured老年区。这个阈值可以同构-XX:MaxTenuringThreshold设置。如果想让对象留在年轻代,可以设置比较大的阈值。 4、设置最小堆和最大堆:-Xmx和-Xms稳定的堆大小堆垃圾回收是有利的,获得一个稳定的堆大小的方法是设置-Xms和-Xmx的值一样,即最大堆和最小堆一样,如果这样子设置,系统在运行时堆大小理论上是恒定的,稳定的堆空间可以减少GC次数,因此,很多服务端都会将这两个参数设置为一样的数值。稳定的堆大小虽然减少GC次数,但是增加每次GC的时间,因为每次GC要把堆的大小维持在一个区间内。 5、一个不稳定的堆并非毫无用处。在系统不需要使用大内存的时候,压缩堆空间,使得GC每次应对一个较小的堆空间,加快单次GC次数。基于这种考虑,JVM提供两个参数,用于压缩和扩展堆空间。 (1)-XX:MinHeapFreeRatio 参数用于设置堆空间的最小空闲比率。默认值是40,当堆空间的空闲内存比率小于40,JVM便会扩展堆空间 (2)-XX:MaxHeapFreeRatio 参数用于设置堆空间的最大空闲比率。默认值是70, 当堆空间的空闲内存比率大于70,JVM便会压缩堆空间。 (3)当-Xmx和-Xmx相等时,上面两个参数无效 6、通过增大吞吐量提高系统性能,可以通过设置并行垃圾回收收集器。 (1)-XX:+UseParallelGC:年轻代使用并行垃圾回收收集器。这是一个关注吞吐量的收集器,可以尽可能的减少垃圾回收时间。 (2)-XX:+UseParallelOldGC:设置老年代使用并行垃圾回收收集器。 7、尝试使用大的内存分页:使用大的内存分页增加CPU的内存寻址能力,从而系统的性能。-XX:+LargePageSizeInBytes 设置内存页的大小 8、使用非占用的垃圾收集器。-XX:+UseConcMarkSweepGC老年代使用CMS收集器降低停顿。 9、-XXSurvivorRatio=3,表示年轻代中的分配比率:survivor:eden = 2:3 10、JVM性能调优的工具: (1)jps(Java Process Status):输出JVM中运行的进程状态信息(现在一般使用jconsole) (2)jstack:查看java进程内线程的堆栈信息。 (3)jmap:用于生成堆转存快照 (4)jhat:用于分析jmap生成的堆转存快照(一般不推荐使用,而是使用Ecplise Memory Analyzer) (5)jstat是JVM统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等。 (6)VisualVM:故障处理工具 类加载机制: 一、 概念: 类加载器把class文件中的二进制数据读入到内存中,存放在方法区,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。 类加载的步骤如下: 1、加载:查找并加载类的二进制数据(把class文件里面的信息加载到内存里面) 2、连接:把内存中类的二进制数据合并到虚拟机的运行时环境中 (1)验证:确保被加载的类的正确性。包括: A、类文件的结构检查:检查是否满足Java类文件的固定格式B、语义检查:确保类本身符合Java的语法规范C、字节码验证:确保字节码流可以被Java虚拟机安全的执行。字节码流是操作码组成的序列。每一个操作码后面都会跟着一个或者多个操作数。字节码检查这个步骤会检查每一个操作码是否合法。D、二进制兼容性验证:确保相互引用的类之间是协调一致的。 (2)准备:为类的静态变量分配内存,并将其初始化为默认值 (3)解析:把类中的符号引用转化为直接引用(比如说方法的符号引用,是有方法名和相关描述符组成,在解析阶段,JVM把符号引用替换成一个指针,这个指针就是直接引用,它指向该类的该方法在方法区中的内存位置) 3、初始化:为类的静态变量赋予正确的初始值。当静态变量的等号右边的值是一个常量表达式时,不会调用static代码块进行初始化。只有等号右边的值是一个运行时运算出来的值,才会调用static初始化。 二、双亲委派模型: 1、当一个类加载器收到类加载请求的时候,它首先不会自己去加载这个类的信息,而是把该请求转发给父类加载器,依次向上。所以所有的类加载请求都会被传递到父类加载器中,只有当父类加载器中无法加载到所需的类,子类加载器才会自己尝试去加载该类。当当前类加载器和所有父类加载器都无法加载该类时,抛出ClassNotFindException异常。 2、意义:提高系统的安全性。用户自定义的类加载器不可能加载应该由父加载器加载的可靠类。(比如用户定义了一个恶意代码,自定义的类加载器首先让系统加载器去加载,系统加载器检查该代码不符合规范,于是就不继续加载了) 3、定义类加载器:如果某个类加载器能够加载一个类,那么这个类加载器就叫做定义类加载器 4、初始类加载器:定义类加载器及其所有子加载器都称作初始类加载器。 5、运行时包:(1)由同一个类加载器加载并且拥有相同包名的类组成运行时包(2)只有属于同一个运行时包的类,才能访问包可见(default)的类和类成员。作用是 限制用户自定义的类冒充核心类库的类去访问核心类库的包可见成员。 6、加载两份相同的class对象的情况:A和B不属于父子类加载器关系,并且各自都加载了同一个类。 三、特点: 1、全盘负责:当一个类加载器加载一个类时,该类所依赖的其他类也会被这个类加载器加载到内存中。 2、缓存机制:所有的Class对象都会被缓存,当程序需要使用某个Class时,类加载器先从缓存中查找,找不到,才从class文件中读取数据,转化成Class对象,存入缓存中。 四、 类加载器:两种类型的类加载器: 1、 JVM自带的类加载器(3种):(1)根类加载器(Bootstrap):a、C++编写的,程序员无法在程序中获取该类b、负责加载虚拟机的核心库,比如java.lang.Objectc、没有继承ClassLoader类(2)扩展类加载器(Extension):a、Java编写的,从指定目录中加载类库b、父加载器是根类加载器c、是ClassLoader的子类d、如果用户把创建的jar文件放到指定目录中,也会被扩展加载器加载。(3)系统加载器(System)或者应用加载器(App):a、Java编写的b、父加载器是扩展类加载器c、从环境变量或者class.path中加载类d、是用户自定义类加载的默认父加载器e、是ClassLoader的子类 2、用户自定义的类加载器: (1)Java.lang.ClassLoader类的子类 (2)用户可以定制类的加载方式 (3)父类加载器是系统加载器 (4)编写步骤:A、继承ClassLoaderB、重写findClass方法。从特定位置加载class文件,得到字节数组,然后利用defineClass把字节数组转化为Class对象(5)为什么要自定义类加载器?A、可以从指定位置加载class文件,比如说从数据库、云端加载class文件B、加密:Java代码可以被轻易的反编译,因此,如果需要对代码进行加密,那么加密以后的代码,就不能使用Java自带的ClassLoader来加载这个类了,需要自定义ClassLoader,对这个类进行解密,然后加载。 问题:Java程序对类的执行有几种方式:1、 主动使用(6种情况):JVM必须在每个类“首次 主动使用”的时候,才会初始化这些类。(1) 创建类的实例(2) 读写某个类或者接口的静态变量(3) 调用类的静态方法(4) 同过反射的API(Class.forName())获取类(5) 初始化一个类的子类(6) JVM启动的时候,被标明启动类的类(包含Main方法的类)只有当程序使用的静态变量或者静态方法确实在该类中定义时,该可以认为是对该类或者接口的主动使用。 2、 被动使用:除了主动使用的6种情况,其他情况都是被动使用,都不会导致类的初始化。 3、 JVM规范允许类加载器在预料某个类将要被使用的时候,就预先加载它。如果该class文件缺失或者存在错误,则在程序“首次 主动使用”的时候,才报告这个错误。(Linkage Error错误)。如果这个类一直没有被程序“主动使用”,就不会报错。 类加载机制与接口: 1、 当Java虚拟机初始化一个类时,不会初始化该类实现的接口。 2、 在初始化一个接口时,不会初始化这个接口父接口。 3、 只有当程序首次使用该接口的静态变量时,才导致该接口的初始化。 ClassLoader: 1、 调用Classloader的loadClass方法去加载一个类,不是主动使用,因此不会进行类的初始化。 类的卸载: 1、 有JVM自带的三种类加载器(根、扩展、系统)加载的类始终不会卸载。因为JVM始终引用这些类加载器,这些类加载器使用引用他们所加载的类,因此这些Class类对象始终是可到达的。 2、 由用户自定义类加载器加载的类,是可以被卸载的。 补充: JDK和JRK (1)JDK :Java Development Kit,开发的时候用到的类包。(2)JRE :Java Runtime Environment,Java运行的基础,包含运行时需要的所有类库。 图解java文件转化成机器码 JVM虚拟机先将java文件编译成class文件(字节码文件),然后再将class文件转换成所有操作系统都能运行的机器指令。 最后 感谢大家看到这里,文章有不足,欢迎大家指出;如果你觉得写得不错,那就给我一个赞吧。

优秀的个人博客,低调大师

Java并发编程相关面试

一、Java开发中用过哪些锁1、乐观锁乐观锁顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS(Compare and Swap 比较并交换)实现的 乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升; 乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。 2、悲观锁悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如Java里面的同步原语synchronized关键字的实现就是悲观锁。 悲观锁适合写操作非常多的场景; 悲观锁在Java中的使用,就是利用各种锁; 3、独享锁独享锁是指该锁一次只能被一个线程所持有。 独享锁通过AQS来实现的,通过实现不同的方法,来实现独享锁。 对于Synchronized而言,当然是独享锁。 4、共享锁共享锁是指该锁可被多个线程所持有。 读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。 共享锁也是通过AQS来实现的,通过实现不同的方法,来实现共享锁。 5、互斥锁互斥锁在Java中的具体实现就是ReentrantLock。 6、读写锁读写锁在Java中的具体实现就是ReadWriteLock。 7、可重入锁重入锁也叫作递归锁,指的是同一个线程外层函数获取到一把锁后,内层函数同样具有这把锁的控制权限;synchronized和ReentrantLock就是重入锁对应的实现;synchronized重量级的锁 ;ReentrantLock轻量级的锁; 8、公平锁公平锁是指多个线程按照申请锁的顺序来获取锁。 对于Java ReetrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。 9、非公平锁非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。 对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。 10、分段锁分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。 我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7和JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。 当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。 但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。 分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。 11、偏向锁偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。 12、轻量级锁轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。 13、重量级锁重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。 14、自旋锁在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。 二、synchronized关键字理解使用了synchronized关键字可以轻松地解决多线程共享数据同步问题。 synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。 synchronized取得的锁都是对象;每个对象只有一个锁(lock)与之相关联;实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。 synchronized的4种用法: 方法声明时使用,线程获得的是成员锁; 对某一代码块使用,synchronized后跟括号,括号里是变量,线程获得的是成员锁; synchronized后面括号里是一对象,此时,线程获得的是对象锁; synchronized后面括号里是类,此时,线程获得的是对象锁; 三、CAS无锁机制CAS:Compare and Swap,即比较交换; jdk1.5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronized同步锁的一种乐观锁。jdk1.5之前java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是悲观锁; 本身无锁,采用乐观锁的思想,在数据操作时对比数据是否一致,如果一致代表之前没有线程操作该数据,那么就会更新数据,如果不一致代表有县城更新则重试; CAS当中包含三个参数CAS(V,E,N),V标识要更新的变量,E标识预期值,N标识新值 运行过程:1.线程访问时,先会将主内存中的数据同步到线程的工作内存当中; 2.假设线程A和线程B都有对数据进行更改,那么假如线程A先获取到执行权限; 3.线程A先会对比工作内存当中的数据和主内存当中的数据是否一致,如果一致(V==E)则进行更新,不一致则刷新数据,重新循环判断; 4.这时更新完毕后,线程B也要进行数据更新,主内存数据和工作内存数据做对比,如果一致则进行更新,不一致则将主内存数据重新更新到工作内存,然后循环再次对比两个内存中的数据,直到一致为止; CAS无锁机制存在一个问题ABA问题,如果将原来A的值改为了B,然后又改回了A,虽然最终结果没有发生改变,但是在过程中是对该数据进行了修改操作 解决该问题:在Java中并发包下有一个原子类:AtomicStampedReference,在该类当中通过版本控制判断值到底是否被修改 解释:如果对值进行了更改则版本号+1,那么在CAS当中不仅仅对比变量的值,还要对比版本号,如果值和版本号都相等则代表没有被修改,如果有一方不相等代表进行过更改 那么就从主内存中重新刷新数据到工作内存然后循环对比,直到成功为止~ 四、AQSAQS:全称AbstractQueueSynchronizer,抽象队列同步器,这个类在java.util.concurrent.locks包下 它是一个底层同步工具类,比如CountDownLatch,Sammphore,ReentrantLock,ReentrantReadWriteLock等等都是基于AQS 底层三个内容: 1.state(用于计数器) 2.线程标记(哪一个线程加的锁) 3.阻塞队列(用于存放阻塞线程) AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,如下图所示。AQS为一系列同步器依赖于一个单独的原子变量(state)的同步器提供了一个非常有用的基础。子类们必须定义改变state变量的protected方法,这些方法定义了state是如何被获取或释放的。 J.U.C是基于AQS实现的,AQS是一个同步器,设计模式是模板模式。 核心数据结构:双向链表 + state(锁状态) 底层操作:CAS 五、ReentrantLock底层实现ReentrantLock是基于AQS的,AQS是Java并发包中众多同步组件的构建基础,它通过一个int类型的状态变量state和一个FIFO队列来完成共享资源的获取,线程的排队等待等。AQS是个底层框架,采用模板方法模式,它定义了通用的较为复杂的逻辑骨架,比如线程的排队,阻塞,唤醒等,将这些复杂但实质通用的部分抽取出来,这些都是需要构建同步组件的使用者无需关心的,使用者仅需重写一些简单的指定的方法即可(其实就是对于共享变量state的一些简单的获取释放的操作)。 无参构造器(默认为非公平锁) public ReentrantLock() { sync = new NonfairSync();//默认是非公平的 }synchronized是ReentrantLock内部实现的一个同步组件,它是Reentrantlock的一个静态内部类,继承于AQS; 带布尔值的构造器(是否公平) public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();//fair为true,公平锁;反之,非公平锁 }此处可以指定是否采用公平锁,FailSync和NonFailSync亦为Reentrantlock的静态内部类,都继承于synchronized; 六、ReentrantLock和synchronized之间的区别synchronized 竞争锁时会一直等待;ReentrantLock 可以尝试获取锁,并得到获取结果synchronized 获取锁无法设置超时;ReentrantLock 可以设置获取锁的超时时间synchronized 无法实现公平锁;ReentrantLock 可以满足公平锁,即先等待先获取到锁synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法synchronized 是 JVM 层面实现的;ReentrantLock 是 JDK 代码层面实现synchronized 在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock 不会自动释放锁,需要在 finally{} 代码块显示释放七、ReentrantReadWriteLock(读写锁) 相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些。 假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(译者注:也就是说:读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。 Java5在java.util.concurrent包中已经包含了读写锁。 package com.zn.lockTest; import java.util.HashMap;import java.util.Map;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteLock { //创建一个集合 static Map<String,String> map=new HashMap<String,String>(); //创建一个读写锁 static ReentrantReadWriteLock lock=new ReentrantReadWriteLock(); //获取读锁 static Lock readLock=lock.readLock(); //获取写锁 static Lock writeLock=lock.writeLock(); //写操作 public Object put(String key,String value){ writeLock.lock(); try { System.out.println("Write正在执行写操作~"); Thread.sleep(100); String put = map.put(key, value); System.out.println("Write写操作执行完毕~"); return put; } catch (InterruptedException e) { e.printStackTrace(); }finally { writeLock.unlock(); } return null; } //写操作 public Object get(String key){ readLock.lock(); try { System.out.println("Read正在执行读操作~"); Thread.sleep(100); String value = map.get(key); System.out.println("Read读操作执行完毕~"); return value; } catch (InterruptedException e) { e.printStackTrace(); }finally { readLock.unlock(); } return null; } public static void main(String[] args) { ReadWriteLock lock=new ReadWriteLock(); for (int i = 0; i < 10; i++) { int finalI = i; new Thread(()->{ try { //写操作 lock.put(finalI +"","value"+finalI); //读操作 System.out.println(lock.get(finalI+"")); } catch (Exception e) { e.printStackTrace(); } }).start(); } } } 控制台效果: 八、BlockingQueue阻塞队列的实现方式阻塞队列(BlockingQueue)是一个支持两个附加操作的队列,这两个附加的操作是: 在队列为空时,获取元素的线程会等待队列变为非空; 当队列满时,存储元素的线程会等待队列可用; 阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器拿元素; 在java中,BlockingQueue的接口位于java.util.concurrent包中,阻塞队列是线程安全的; 在新增呢的concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题,通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利; 常用的队列主要由以下两种: 先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能,从某种程度上来说这种队列也体现了一种公平性; 后进后出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件; 1.ArrayBlockingQueueArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组,有边界意思就是它的容量是有限的,我们必须在其初始化的时候执行它的容量大小,容量大小一旦执行就不可改变; ArrayBlockingQueue是以先进先出的方式存储数据,最新插入的对象是尾部,最新移除的对象是头部; package com.zn.queueTest; import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.TimeUnit; public class ArrayBlockingQueueTest { public static void main(String[] args) throws InterruptedException { ArrayBlockingQueue<String> arrays=new ArrayBlockingQueue<String>(3); arrays.add("张三"); arrays.add("李四"); arrays.add("王五"); //添加阻塞队列 arrays.offer("赵六",1, TimeUnit.SECONDS); //poll方法相当于消费了队列中的数据,队列的数据就会删除 System.out.println(arrays.poll()); System.out.println(arrays.poll()); System.out.println(arrays.poll()); System.out.println(arrays.poll()); } } 控制台效果: 如果先出队一条数据,此时被阻塞的数据就可以添加进来: package com.zn.queueTest; import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.TimeUnit; public class ArrayBlockingQueueTest { //出队一条数据 public static void main(String[] args) throws InterruptedException { ArrayBlockingQueue<String> arrays=new ArrayBlockingQueue<String>(3); arrays.add("张三"); arrays.add("李四"); arrays.add("王五"); //出队一条数据 System.out.println(arrays.poll()); //添加阻塞队列 arrays.offer("赵六",1, TimeUnit.SECONDS); //poll方法相当于消费了队列中的数据,队列的数据就会删除 System.out.println(arrays.poll()); System.out.println(arrays.poll()); System.out.println(arrays.poll()); } } 控制台效果: 2.LinkedBlockingQueueLinkedBlockingQueue阻塞队列大小的配置时可选的,如果我们初始化时指定大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE容量,它的内部是一个链表; 和ArrayBlockingQueue一样,LinkedBlockingQueue也是以先进先出的方式存储数据,最新插入的对象是尾部,最新移除的对象是头部; package com.zn.queueTest; import java.util.concurrent.LinkedBlockingQueue; public class LinkedBlockingQueueTest { public static void main(String[] args) throws InterruptedException { LinkedBlockingQueue linkedBlockingQueue=new LinkedBlockingQueue(3); linkedBlockingQueue.add("A"); linkedBlockingQueue.add("B"); linkedBlockingQueue.add("C"); System.out.println(linkedBlockingQueue.poll()); System.out.println(linkedBlockingQueue.size()); } } 控制台效果: 3.PriorityBlockingQueuePriorityBlockingQueue是一个没有边界的队列,它的排序规则和java.util.PriorityQueue一样。需要注意,PriorityBlockingQueue中国允许插入null对象; 所有插入PriorityBlockingQueue的对象必须实现java.lang.Comparable接口,队列优先级的排序规则就是按照我们对这个接口的实现来定义的; 另外,我们可以从PriorityBlockingQueue获得一个迭代器Iterator,但这个迭代器并不保证按照优先级顺序进行迭代; package com.zn.queueTest; import java.util.concurrent.PriorityBlockingQueue; public class PriorityBlockingQueueTest { public static void main(String[] args) throws InterruptedException { PriorityBlockingQueue<String> priorityBlockingQueue=new PriorityBlockingQueue<String>(3); priorityBlockingQueue.add("AA"); priorityBlockingQueue.add("BB"); priorityBlockingQueue.add("CC"); System.out.println(priorityBlockingQueue.poll()); System.out.println(priorityBlockingQueue.size()); } } 控制台效果: 4.SynchronousQueueSynchronousQueue队列内部仅容纳一个元素,当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费; 九、ConcurrentLinkedQueueConcurrentLinkedQueue:是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue,它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。头是最先加入的,尾是最近加入的,该队列不允许null元素; ConcurrentLinkedQueue重要方法: add()和offer()都是加入元素的方法(在ConcurrentLinkedQueue中这两个方法没有任务区别); poll()和peek()都是取头元素节点,区别在于前者会删除元素,后者不会; package com.zn.queueTest; import java.util.concurrent.ConcurrentLinkedQueue; public class ConcurrentLinkedQueueTest { public static void main(String[] args) { //准备队列 ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>(); //存放数据 queue.offer("张三"); queue.offer("李四"); queue.offer("王五"); //获取队列中数据个数 System.out.println("队列中当前有:"+queue.size()+"个数据~"); //获取队列中头数据 poll()方法相当于消费了队列中的数据,队列数据会随之删除 System.out.println("获取队列中的数据:"+queue.poll()); System.out.println("队列中当前有:"+queue.size()+"个数据~"); //获取队列中数据,但是不会删除 System.out.println("获取队列中的数据:"+queue.peek()); System.out.println("获取队列中的数据:"+queue.peek()); System.out.println("队列中当前有:"+queue.size()+"个数据~"); } } 控制台效果: 原文地址https://www.cnblogs.com/Zzzzn/p/12586656.html

优秀的个人博客,低调大师

Android 高级面试题目整理

阿里云双十一拼团活动:https://www.aliyun.com/1111/2019/group-buying-share 1. ThreadLocal的理解 可以保证线程的安全。在多个线程共享相同的数据的时候,会为每个线程创建单独的副本,在单独的副本上进行数据的操作,不会对其它线程的数据产生影响,保证了线程安全。 2. HashMap HashSet HashTable的区别? 都是集合,底层都是Hash算法实现的。HashMap是Hashtable的替代品,这两个都是双列集合,而HashSet是单列集合。HashMap线程不安全、效率高、可以存储null键和null值;Hashtable线程安全,效率低,不可以存储null键和null值。 3. 如何让HashMap可以线程安全? HashMap 在并发执行 put 操作时会引起死循环,导致 CPU 利用率接近100%。因为多线程会导致 HashMap 的 Node 链表形成环形数据结构,一旦形成环形数据结构,Node 的 next 节点永远不为空,就会在获取 Node 时产生死循环。使用下面三种替换方式:HashtableConcurrentHashMapSynchronized Map 4. Android对HashMap做了优化后推出的新的容器类是什么? SparseArray它要比 HashMap 节省内存,某些情况下比HashMap性能更好,按照官方问答的解释,主要是因为SparseArray不需要对key和value进行auto-boxing(将原始类型封装为对象类型,比如把int类型封装成Integer类型),结构比HashMap简单(SparseArray内部主要使用两个一维数组来保存数据,一个用来存key,一个用来存value)不需要额外的额外的数据结构(主要是针对HashMap中的HashMapEntry而言的)。 5. Java多线程之间如何通信 等待唤醒机制 6. 线程池的实现机制 向线程池提交任务,会依次启动核心线程,如果提交的任务数超过了核心线程数,会将任务保存到阻塞队列中,如果阻塞队列也满了,且继续提交任务,则会创建新线程执行任务,直到任务数达到最大线程数。此时如果再提交任务的话会抛出异常或者直接丢弃任务。通过Executor.execute()无法得到返回值,通过ExecutorService.submit()可以得到返回值。 7. RxJava中map和flatmap操作符的区别及底层实现 Map返回的是结果集,flatmap返回的是包含结果集的Observable。Map只能一对一,flatmap可以一对多、多对多。RxJava是通过观察者模式实现的。 8. 对消息机制中Looper的理解 Looper在消息机制中扮演的角色是创造无限循环从Messagequeue中取得消息然后分发。 9. 单例模式有哪些实现方式 饿汉模式(线程安全,调用效率高,但是不能延时加载)懒汉模式(线程安全,调用效率不高,但是能延时加载)双重检测锁模式(由于JVM底层模型原因,偶尔会出问题,不建议使用)静态内部类式(线程安全,调用效率高,可以延时加载)枚举类(线程安全,调用效率高,不能延时加载,可以天然的防止反射和反序列化调用) 10. 通过静态内部类实现单例模式有哪些优点 线程安全,调用效率高,可以延时加载 11. synchronized volatile关键字有什么区别?以及还有哪些同样功能的关键字 (1) volatile是变量修饰符,而synchronized则作用于一段代码或者方法。(2) volatile只是在线程内存和main memory(主内存)间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。const、final、lock 12. 界面卡顿的原因有哪些? UI线程(main)有耗时操作视图渲染时间过长,导致卡顿 13. 造成OOM/ANR 的原因? OOM: (1)不恰当地使用static关键字 (2)内部类对Activity的引用 (3)大量Bitmap的使用会导致程序包运行时的内存消耗变大 (4)游标Cursor对象用完应该及时关闭 (5)加载对象过大 (6)相应资源过多,来不及释放。ANR: (1)在5秒内没有响应输入的事件(IO操作耗时、数据库操作复杂耗时、主线程非主线程产生死锁等待、网络加载/图片操作耗时、硬件操作耗时) (2)BroadcastReceiver在10秒内没有执行完毕(Service binder数量达到上限、Service忙导致超时无响应) 14. Activity与Fragment生命周期有何联系 在创建的过程中,是Activity带领着Fragment,在销毁的过程中,是Fragment带领着Activity。这里写图片描述 15. Glide三级缓存 内存缓存,磁盘缓存、网络缓存(由于网络缓存严格来说不算是缓存的一种,故也称为二级缓存)。缓存的资源分为两种:原图(SOURCE)、处理图(RESULT)(默认)。内存缓存:默认开启的,可以通过调用skipMemoryCache(true)来设置跳过内存缓存,缓存最大空间:每个进程可用的最大内存*0.4。(低配手机0.33)磁盘缓存:分为四种:ALL(缓存原图)、NONE(什么都不缓存)、SOURCE(只缓存原图)、RESULT(之后处理图),通过diskCacheStrategy(DiskCacheStrategy.ALL)来设置,缓存大小250M。 16. MVC、MVP、MVVM的原理 (1) MVC,Model View Controller,是软件架构中最常见的一种框架,简单来说就是通过controller的控制去操作model层的数据,并且返回给view层展示。当用户发出事件的时候,view层会发送指令到controller层,接着controller去通知model层更新数据,model层更新完数据以后直接显示在view层上,这就是MVC的工作原理。这里写图片描述(2) MVP是MVC的演化。MVP的model层相对于MVC是一样的,而activity和fragment不再是controller层,而是纯粹的view层,所有关于用户事件的转发全部交由presenter层处理。presenter层充当了桥梁的作用,用于操作view层发出的事件传递到presenter层中,presenter层去操作model层,并且将数据返回给view层。这里写图片描述(3) MVVM和MVP的区别貌似不大,只不过是presenter层换成了viewmodel层,还有一点就是view层和viewmodel层是相互绑定的关系,这意味着当你更新viewmodel层的数据的时候,view层会相应的变动ui。这里写图片描述 17. 数据库的操作类型有哪些,如何导入外部数据库? (1) 增删改查(2) 将外部数据库放在项目的res/raw目录下。因为安卓系统下数据库要放在data/data/packagename/databases的目录下,然后要做的就是将外部数据库导入到该目录下,操作方法是通过FileInputStream读取外部数据库,再用FileOutputStrean把读取到的东西写入到该目录下。 18. 是否使用过 IntentService,作用是什么, AIDL 解决了什么问题? (1) IntentService继承自Service。由于Service运行在主线程,无法进行耗时操作。所以你需要在Service中开启一个子线程,并且在子线程中运行。为了简化这一操作,Android中提供了IntentService来进行这一处理。通过查看IntentService的源码可以看到,在onCreate中,我们开启了一个HandlerThread线程,之后获取HandlerThread线程中的Looper,并通过这个Looper创建了一个Handler。然后在onStart方法中通过这个Handler将intent与startId作为Message的参数进行发送到消息队列中,然后交由Handler中的handleMessage中进行处理。由于在onStart方法是在主线程内运行的,而Handler是通过工作者线程HandlerThread中的Looper创建的。所以也就是在主线程中发送消息,在工作者接收到消息后便可以进行一些耗时的操作。(2) 进程间通信 19. 是否使用过本地广播,和全局广播有什么差别? 本地广播的数据在本应用范围内传播,不用担心隐私数据泄露的问题。不用担心别的应用伪造广播,造成安全隐患。相比在系统内发送全局广播,它更高效。 20. Activity、 Window、 View 三者的差别, fragment 的特点? (1) Activity像一个工匠(控制单元),Window像窗户(承载模型),View像窗花(显示视图) LayoutInflater像剪刀,Xml配置像窗花图纸。(2) a. Fragment可以作为Activity界面的一部分组成出现; 可以在一个Activity中同时出现多个Fragment,并且一个Fragment也可以在多个Activity中使用; 在Activity运行过程中,可以添加、移除或者替换Fragment; Fragment可以响应自己的输入事件,并且有自己的生命周期,它们的生命周期会受宿主Activity的生命周期影响。 21. Handler、 Thread 和 HandlerThread 的差别 从Android中Thread(java.lang.Thread -> java.lang.Object)描述可以看出,Android的Thread没有对Java的Thread做任何封装,但是Android提供了一个继承自Thread的类HandlerThread(android.os.HandlerThread -> java.lang.Thread),这个类对Java的Thread做了很多便利Android系统的封装。android.os.Handler可以通过Looper对象实例化,并运行于另外的线程中,Android提供了让Handler运行于其它线程的线程实现,也是就HandlerThread。HandlerThread对象start后可以获得其Looper对象,并且使用这个Looper对象实例Handler。 22. 低版本 SDK 实现高版本 api 自己实现或使用注解@TargetApi annotation 23. launch mode 应用场景 (1) standard:标准的启动模式。 这里写图片描述(2) singleTop:单一顶部模式 如果Activity已经被开启,并且处于任务栈的栈顶,就不会创建新的Activity,而是复用这个已经开启的Activity。为了防止出现一些奇怪的用户体验,推荐使用单一顶部模式,整个任务栈可以有多个实例存在.应用场景:短信发送界面.这里写图片描述(3)singletask:单一任务栈 在整个任务栈里面只允许有一个当前Activity的实例存在如果要开启的Activity在任务栈中已经存在,直接复用这个已经存在的Activity,并且把这个Activity上面的所有的其他Activity给清空应用场景:如果一个Activity非常消耗内存和cpu资源,建议把这个Activity做成singletask的模式。浏览器的browserActivity这里写图片描述(4)singleinstance:单一实例. 整个手机操作系统只有一个实例存在,并且是运行在自己单独的任务栈里面.应用场景:通话界面的Activity这里写图片描述 24. touch 事件传递流程 事件处理包括三种情况,分别为:传递—-dispatchTouchEvent()函数、拦截——onInterceptTouchEvent()函数、消费—-onTouchEvent()函数和OnTouchListener。Android事件传递流程:(1) 事件都是从Activity.dispatchTouchEvent()开始传递(2) 事件由父View传递给子View,ViewGroup可以通过onInterceptTouchEvent()方法对事件拦截,停止其向子view传递(3) 如果事件从上往下传递过程中一直没有被停止,且最底层子View没有消费事件,事件会反向往上传递,这时父View(ViewGroup)可以进行消费,如果还是没有被消费的话,最后会到Activity的onTouchEvent()函数。(4) 如果View没有对ACTION_DOWN进行消费,之后的其他事件不会传递过来,也就是说ACTION_DOWN必须返回true,之后的事件才会传递进来(5) OnTouchListener优先于onTouchEvent()对事件进行消费 View不处理事件流程图View不处理事件流程图 View处理事件流程图View处理事件流程图 事件拦截事件拦截 25.Android性能优化 一、代码优化 1.使用AndroidLint分析结果进行相应优化2.不使用枚举及IOC框架,反射性能低3.常量加static4.静态方法5.减少不必要的对象、成员变量6.尽量使用线程池7.适当使用软引用和弱引用8.尽量使用静态内部类,避免潜在的内存泄露9.图片缓存,采用内存缓存LRUCache和硬盘缓存DiskLRUCache10.Bitmap优化,采用适当分辨率大小并及时回收 二、布局优化 避免OverDraw过渡绘制优化布局层级避免嵌套过多无用布局当我们在画布局的时候,如果能实现相同的功能,优先考虑相对布局,然后在考虑别的布局,不要用绝对布局。使用标签把复杂的界面需要抽取出来使用标签,因为它在优化UI结构时起到很重要的作用。目的是通过删减多余或者额外的层级,从而优化整个Android Layout的结构。核心功能就是减少冗余的层次从而达到优化UI的目的!ViewStub 是一个隐藏的,不占用内存空间的视图对象,它可以在运行时延迟加载布局资源文件。 三、ListView和GridView优化 1.采用ViewHolder复用convertView2.避免在getView中执行耗时操作3.列表在滑动状态时不加载图片4.开启硬件加速 26.Android内存泄漏 内存泄漏简单地说就是申请了一块内存空间,使用完毕后没有释放掉。它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄露了。可能的原因有:1.注册没取消造成内存泄露,如:广播2.静态变量持有Activity的引用3.单例模式持有Activity的引用4.查询数据库后没有关闭游标cursor5.构造Adapter时,没有使用 convertView 重用6.Bitmap对象不在使用时调用recycle()释放内存7.对象被生命周期长的对象引用,如activity被静态集合引用导致activity不能释放8.使用Handler造成的内存泄露 原文地址:http://cloud.yundashi168.com/archives/964

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。