首页 文章 精选 留言 我的

精选列表

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

javascript基础修炼(1)——一道十面埋伏的原型链面试

在基础面前,一切技巧都是浮云。 题目是这样的 要求写出控制台的输出. function Parent() { this.a = 1; this.b = [1, 2, this.a]; this.c = { demo: 5 }; this.show = function () { console.log(this.a , this.b , this.c.demo ); } } function Child() { this.a = 2; this.change = function () { this.b.push(this.a); this.a = this.b.length; this.c.demo = this.a++; } } Child.prototype = new Parent(); var parent = new Parent(); var child1 = new Child(); var child2 = new Child(); child1.a = 11; child2.a = 12; parent.show(); child1.show(); child2.show(); child1.change(); child2.change(); parent.show(); child1.show(); child2.show(); 题目涉及的知识点 this的指向 原型机原型链 类的继承 原始类型和引用类型的区别 每一个知识点都可以拿出来做单独的专题研究。 解题需要的知识点细节 1.构造函数,都有一个prototype属性,指向构造函数的原型对象,实例会共享同一个原型对象; 2.实例生成时,会在内存中产生一块新的堆内存,对实例的一般操作将不影响其他实例,因为在堆内存里占据不同空间,互不影响; 3.每一个实例都有一个隐式原型__proto__指向构造函数的原型对象; 4.this的指向问题,常见的情况包含如下几种: 4.1 作为对象方法时,谁调用就指向谁(本题中主要涉及这一条) 4.2 作为函数调用时,指向全局顶层变量window 4.3 作为构造函数调用时,即new操作符生成实例时,构造函数中的this指向实例 4.4 call和apply方法中,显示指定this的绑定为指定上下文 5.字面量的方式(也有资料将literal翻译为直接量,个人认为后一种翻译其实更直观更形象)进行对象和数组赋值(数组本质也是对象)时,都是引用,即在堆内存生成资源,在栈内存生成变量,然后变量指向资源的地址。 6.原型链的查找规则遵循最短路径原则,即先查找实例属性,然后顺着原型链去查找指定的属性,直至原型链末端的Object.prototype和null,如果实例自身及整个原型链都不存在所查找的属性则返回undefined 7.赋值语句对于原始值赋值和引用类型赋值时的细节区别. 开始剖题 1.parent.show() 基本没什么可解释的。 直接取值就能得出答案1 [1,2,1] 5; 2.child1.show() Child的构造函数原本是指向Child的 题目中显式将Child类的原型对象指向了Parent类的一个实例,这是javascript面向对象编程中常见的继承方式之一。此处需要注意Child.prototype指向的是Parent的实例parent,而不是指向Parent这个类 直接在控制台操作输出答案可得11 [1,2,1] 5 此处令人迷惑的是this.b指向的数组最后一列为什么是1而不是11? 先来看一下child1的样子: 当执行child1.show()这个方法时,由于child1作为Child的实例,是拥有a这个属性的,所以show()方法中的this.a会直接指向这个属性的值,也就是11,而不会继续沿原型链取到__proto__所指的对象上的a属性; 接着寻找this.b,由于child1是没有b这个属性的,所以会沿原型链取到parent上的b属性,其值是一个数组,前2项是常量没什么好说的,数组的最后一项是一个引用,而此处的指针并不是一个动态指向,因为在new Parent()这一步的时候它已经被执行过一次,确定指向了parent.a所指向的资源,也就是child1.__proto__中的a属性所指向的资源,即数值1。 延伸思考 需要注意的是: 1.从代码上看,child1.__proto__.b数组的第三项是指向child1.__proto__.a的,那我们此时修改child1.__proto__.a的值,是否会影响child1.show()的结果呢: 答案是木有影响,为什么看起来指向同一个地址的属性却出现值不一样的情形?因为parent实例生成的时候,this.a指向了一个原始值2,所以this.b中的第三项实际上是被赋值了一个原始值,故此处乍看起来像是引用类型的赋值,实则不是。原始值赋值会开辟新的存储空间,使得this.a和this.b[2]的值相等,但是却指向了堆内存里的不同地址。更多详细解释可以参见【扩展阅读】中推荐的博文。 2.那怎样让child1.__proto__.b数组的第三项也输出11呢? 实例化后修改 由于在Parent类定义中,b属性数组的第三项是指向a属性的值的,意味着在Parent实例化之前这个引用是动态指向的,所以只要在Parent实例化之前改变类定义中this.a的值,就可以达到想要的效果,如果在Parent已经实例化,则只能显式修改*.b[2]这个属性的值。 get/set方法同步 另一种方式是通过为a属性设置get/set方法,是的每当a属性的值发生变化时,同步修改b[2]的值,代码和运行结果如下所示: 3.child2.show() 如果理解了上面的解释,那么此处同理即可得出答案:12 [1,2,1] 5 接着代码执行了: child1.change(); child2.change(); 4.parent.show() parent是一个Parent类的实例,Child.prorotype指向的是Parent类的另一个实例,两者在堆内存中是两份资源,互不影响,所以上述操作不影响parent实例, 输出结果保持不变:1 [1,2,1] 5; 5.child1.show(),child2.show() child1执行了change()方法后,发生了怎样的变化呢? this.b.push(this.a) 由于this的动态指向特性,this.b会指向Child.prototype上的b数组,this.a会指向child1的a属性,所以Child.prototype.b变成了[1,2,1,11]; this.a = this.b.length 这条语句中this.a和this.b的指向与上一句一致,故结果为child1.a变为4; this.c.demo = this.a++ 由于child1自身属性并没有c这个属性,所以此处的this.c会指向Child.prototype.c,this.a值为4,为原始类型,故赋值操作时会直接赋值,Child.prototype.c.demo的结果为4,而this.a随后自增为5(4 + 1 = 5). 接着,child2执行了change()方法, 而child2和child1均是Child类的实例,所以他们的原型链指向同一个原型对象Child.prototype,也就是同一个parent实例,所以child2.change()中所有影响到原型对象的语句都会影响child1的最终输出结果 this.b.push(this.a) 由于this的动态指向特性,this.b会指向Child.prototype上的b数组,this.a会指向child2的a属性,所以Child.prototype.b变成了[1,2,1,11,12]; this.a = this.b.length 这条语句中this.a和this.b的指向与上一句一致,故结果为child2.a变为5; this.c.demo = this.a++ 由于child2自身属性并没有c这个属性,所以此处的this.c会指向Child.prototype.c,故执行结果为Child.prototype.c.demo的值变为child2.a的值5,而child2.a最终自增为6(5 + 1 = 6). 接下来执行输出命令,最终结果将输出:child1.show():5 [1,2,1,11,12] 5child2.show():6 [1,2,1,11,12] 5 延伸思考 自己在解题时,在this.c.demo = this.a++出错,本以为这里会传引用,但实际是传了值,分析后明白因为this.a指向的是一个原始值,故此处相当于将原始值赋值给对象属性,所以赋值后child.c.demo的值不会再受到child.a的变化的影响。如果child.a是一个引用类型,那么结果会变成什么样子呢? 我们对源码做一些修改,将child.a指向一个对象(即引用类型): 然后运行后就会发现,Child.prototype.c的值会随着child1.a的变化而变化,因为此时child1.a的值是一个引用类型,赋值过程会使得Child.prototype.c和child1.a指向同一份资源的内存空间地址。对于原始类型和引用类型更详细的解说,可以参考篇尾扩展阅读中的博客。 收获和反思 1.基础知识本来就是零散的细节,必须本着死磕到底的心态进行学习。 2.基础知识是最枯燥的,也是真正拉开人和人之间差距的东西,也是你想进入大厂必须要跨过的门槛,重要却不紧急。同样是菜鸟,有的人3-5年后成为了前端架构师,有的人3-5年后还在用层出不穷的新框架给按钮绑事件,想成为怎样的人,就要付出怎样的努力,大多数时候都是没毛病的。基础很重要!很重要!很重要! 3.基础这个东西是要不断看的,像红宝书(javascript高级程序设计)和犀牛书(javascript权威指南)这种书,最好多过几遍,一些难以理解的现象,往往是由于对底层原理理解不到位造成的,买来新书直接用来垫高显示器你不心疼的吗?喜马拉雅上有一个免费的陪你读书系列节目,30多期的音频通篇讲解了红宝书的内容,对不喜欢看书的童鞋绝对是一大福音。 扩展阅读 JavaScript数据操作--原始值和引用值的操作本质 [javascript高级程序设计]第4章

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

最新整理的运维工程师面试真的太给力了,整整50道,速度收藏!

1、请简述OSI七层网络模型有哪些层及各自的含义? 物理层:底层数据传输,比如网线、网卡标准 数据链路层:定义数据的基本格式,如何传输,如何标识。比如网卡MAC地址 网络层:定义IP编码,定义路由功能,比如不同设备的数据转发 传输层:端到端传输数据的基本功能,比如TCP、UDP 会话层:控制应用程序之间会话能力,比如不同软件数据分发给不停软件 表示层:数据格式标识,基本压缩加密功能。 应用层:各种应用软件,包括 Web 应用。 2、在Linux的LVM分区格式下,请简述给根分区磁盘扩容的步骤? 这个分3种 第一种方法: growpart/dev/vda1 resize2fs/dev/vda1 第二种方法: partpeobe/dev/sda resize2fs/dev/vda1 第三种方法: fdisk/dev/sdb#np11回车回车t8ew pvcreate/dev/sdb1 vgextenddatavg/dev/sdb1 lvextend-r-L+100%free/dev/mapper/datavg-lv01 3、讲述一下Tomcat8005、8009、8080三个端口的含义? 8005 关闭时使用 8009为AJP端口,即容器使用,如Apache能通过AJP协议访问Tomcat的8009端口来实现功能 8080 一般应用使用 4、简述DNS进行域名解析的过程? 迭代查询(返回最优结果)、递归查询(本地找DNS)用户要访问 www.baidu.com,会先找本机的host文件,再找本地设置的DNS服务器,如果也没有找到,就去网络中找根服务器,根服务器反馈结果,说只能提供一级域名服务器.cn,就去找一级域名服务器,一级域名服务器说只能提供二级域名服务器.com.cn,就去找二级域名服务器,二级域服务器只能提供三级域名服务器.baidu.com.cn,就去找三级域名服务器,三级域名服务器正好有这个网站www.baidu.com,然后发给请求的服务器,保存一份之后,再发给客户端。 5、讲一下Keepalived的工作原理? 在一个虚拟路由器中,只有作为MASTER的VRRP(虚拟路由冗余协议)路由器会一直发送VRRP通告信息,BACKUP不会抢占MASTER,除非它的优先级更高。当MASTER不可用时(BACKUP收不到通告信息)多台BACKUP中优先级最高的这台会被抢占为MASTER。这种抢占是非常快速的(<1s),以保证服务的连续性由于安全性考虑,VRRP包使用了加密协议进行加密。BACKUP不会发送通告信息,只会接收通告信息。 6、LVS、Nginx、HAproxy有什么区别?工作中你怎么选择? LVS: 抗负载能力强、工作在第4层仅作分发之用,没有流量的产生,这个特点也决定了它在负载均衡软件里的性能最强的;无流量,同时保证了均衡器IO的性能不会受到大流量的影响; 工作稳定,自身有完整的双机热备方案,如LVS+Keepalived和LVS+Heartbeat; 应用范围比较广,可以对所有应用做负载均衡; 配置简单,因为没有可太多配置的东西,所以并不需要太多接触,大大减少了人为出错的几率; LVS的缺点: 软件本身不支持正则处理,不能做动静分离,这就凸显了Nginx/HAProxy+Keepalived的优势。 如果网站应用比较庞大,LVS/DR+Keepalived就比较复杂了,特别是后面有Windows Server应用的机器,实施及配置还有维护过程就比较麻烦,相对而言,Nginx/HAProxy+Keepalived就简单多了。 Nginx: 工作在第7层,应用层,可以针对http应用做一些分流的策略。比如针对域名、目录结构。它的正则比HAProxy更为强大和灵活; Nginx对网络的依赖非常小,理论上能ping通就就能进行负载功能 Nginx安装和配置简单 可以承担高的负载压力且稳定,一般能支撑超过几万次的并发量; Nginx不仅仅是一款优秀的负载均衡器/反向代理软件,它同时也是功能强大的Web应用服务器。Nginx在处理静态页面、特别是抗高并发方面相对apache有优势; Nginx作为Web反向代理加速缓存越来越成熟,速度比传统的Squid服务器更快 Nginx的缺点: Nginx不支持url来检测。 Nginx仅能支持http、https和Email协议 Nginx的Session的保持,Cookie的引导能力相对欠缺。 HAProxy: HAProxy是支持虚拟主机的,可以工作在4、7层(支持多网段); 能够补充Nginx的一些缺点比如Session的保持,Cookie的引导等工作; 支持url检测后端的服务器; 它跟LVS一样,本身仅仅就只是一款负载均衡软件;单纯从效率上来讲HAProxy更会比Nginx有更出色的负载均衡速度,在并发处理上也是优于Nginx的; HAProxy可以对Mysql读进行负载均衡,对后端的MySQL节点进行检测和负载均衡,不过在后端的MySQL slaves数量超过10台时性能不如LVS; HAProxy的算法较多,达到8种; 工作选择: HAproxy和Nginx由于可以做七层的转发,所以URL和目录的转发都可以做在很大并发量的时候我们就要选择LVS,像中小型公司的话并发量没那么大选择HAproxy或者Nginx足已,由于HAproxy由是专业的代理服务器配置简单,所以中小型企业推荐使用HAproxy。 7、docker的工作原理是什么,讲一下 docker是一个Client-Server结构的系统,docker守护进程运行在宿主机上,守护进程从客户端接受命令并管理运行在主机上的容器,容器是一个运行时环境,这就是我们说的集装箱。 8、docker的组成包含哪几大部分 一个完整的docker有以下几个部分组成: docker client,客户端,为用户提供一系列可执行命令,用户用这些命令实现跟 docker daemon 交互; docker daemon,守护进程,一般在宿主主机后台运行,等待接收来自客户端的请求消息; docker image,镜像,镜像run之后就生成为docker容器; docker container,容器,一个系统级别的服务,拥有自己的ip和系统目录结构;运行容器前需要本地存在对应的镜像,如果本地不存在该镜像则就去镜像仓库下载。 docker 使用客户端-服务器 (C/S) 架构模式,使用远程api来管理和创建docker容器。docker 容器通过 docker 镜像来创建。容器与镜像的关系类似于面向对象编程中的对象与类。 9、docker与传统虚拟机的区别什么? 传统虚拟机是需要安装整个操作系统的,然后再在上面安装业务应用,启动应用,通常需要几分钟去启动应用,而docker是直接使用镜像来运行业务容器的,其容器启动属于秒级别; Docker需要的资源更少,Docker在操作系统级别进行虚拟化,Docker容器和内核交互,几乎没有性能损耗,而虚拟机运行着整个操作系统,占用物理机的资源就比较多; Docker更轻量,Docker的架构可以共用一个内核与共享应用程序库,所占内存极小;同样的硬件环境,Docker运行的镜像数远多于虚拟机数量,对系统的利用率非常高; 与虚拟机相比,Docker隔离性更弱,Docker属于进程之间的隔离,虚拟机可实现系统级别隔离; Docker的安全性也更弱,Docker的租户root和宿主机root相同,一旦容器内的用户从普通用户权限提升为root权限,它就直接具备了宿主机的root权限,进而可进行无限制的操作。虚拟机租户root权限和宿主机的root虚拟机权限是分离的,并且虚拟机利用如Intel的VT-d和VT-x的ring-1硬件隔离技术,这种技术可以防止虚拟机突破和彼此交互,而容器至今还没有任何形式的硬件隔离; Docker的集中化管理工具还不算成熟,各种虚拟化技术都有成熟的管理工具,比如:VMware vCenter提供完备的虚拟机管理能力; Docker对业务的高可用支持是通过快速重新部署实现的,虚拟化具备负载均衡,高可用、容错、迁移和数据保护等经过生产实践检验的成熟保障机制,Vmware可承诺虚拟机99.999%高可用,保证业务连续性; 虚拟化创建是分钟级别的,Docker容器创建是秒级别的,Docker的快速迭代性,决定了无论是开发、测试、部署都可以节省大量时间; 虚拟机可以通过镜像实现环境交付的一致性,但镜像分发无法体系化,Docker在Dockerfile中记录了容器构建过程,可在集群中实现快速分发和快速部署。from wljslmz 10、docker技术的三大核心概念是什么? 镜像:镜像是一种轻量级、可执行的独立软件包,它包含运行某个软件所需的所有内容,我们把应用程序和配置依赖打包好形成一个可交付的运行环境(包括代码、运行时需要的库、环境变量和配置文件等),这个打包好的运行环境就是image镜像文件。 容器:容器是基于镜像创建的,是镜像运行起来之后的一个实例,容器才是真正运行业务程序的地方。如果把镜像比作程序里面的类,那么容器就是对象。 镜像仓库:存放镜像的地方,研发工程师打包好镜像之后需要把镜像上传到镜像仓库中去,然后就可以运行有仓库权限的人拉取镜像来运行容器了。 11、centos镜像几个G,但是docker centos镜像才几百兆,这是为什么? 一个完整的Linux操作系统包含Linux内核和rootfs根文件系统,即我们熟悉的/dev、/proc/、/bin等目录。我们平时看到的centOS除了rootfs,还会选装很多软件,服务,图形桌面等,所以centOS镜像有好几个G也不足为奇。 而对于容器镜像而言,所有容器都是共享宿主机的Linux 内核的,而对于docker镜像而言,docker镜像只需要提供一个很小的rootfs即可,只需要包含最基本的命令,工具,程序库即可,所有docker镜像才会这么小。 12、讲一下镜像的分层结构以及为什么要使用镜像的分层结构? 一个新的镜像其实是从 base 镜像一层一层叠加生成的。每安装一个软件,dockerfile中使用RUM命令,就会在现有镜像的基础上增加一层,这样一层一层的叠加最后构成整个镜像。所以我们docker pull拉取一个镜像的时候会看到docker是一层层拉去的。 分层机构最大的一个好处就是 :共享资源。比如:有多个镜像都从相同的 base 镜像构建而来,那么 Docker Host 只需在磁盘上保存一份 base 镜像;同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享。 13、讲一下容器的copy-on-write特性,修改容器里面的内容会修改镜像吗? 我们知道,镜像是分层的,镜像的每一层都可以被共享,同时,镜像是只读的。当一个容器启动时,一个新的可写层被加载到镜像的顶部,这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。 所有对容器的改动 - 无论添加、删除、还是修改文件,都只会发生在容器层中,因为只有容器层是可写的,容器层下面的所有镜像层都是只读的。镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如 /a,上层的 /a 会覆盖下层的 /a,也就是说用户只能访问到上层中的文件 /a。在容器层中,用户看到的是一个叠加之后的文件系统。 添加文件:在容器中创建文件时,新文件被添加到容器层中。 读取文件:在容器中读取某个文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后打开并读入内存。 修改文件:在容器中修改已存在的文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后修改之。 删除文件:在容器中删除文件时,Docker 也是从上往下依次在镜像层中查找此文件。找到后,会在容器层中记录下此删除操作。 只有当需要修改时才复制一份数据,这种特性被称作 Copy-on-Write。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。 14、简单描述一下Dockerfile的整个构建镜像过程 首先,创建一个目录用于存放应用程序以及构建过程中使用到的各个文件等; 然后,在这个目录下创建一个Dockerfile文件,一般建议Dockerfile的文件名就是Dockerfile; 编写Dockerfile文件,编写指令,如,使用FORM指令指定基础镜像,COPY指令复制文件,RUN指令指定要运行的命令,ENV设置环境变量,EXPOSE指定容器要暴露的端口,WORKDIR设置当前工作目录,CMD容器启动时运行命令,等等指令构建镜像; Dockerfile编写完成就可以构建镜像了,使用docker build -t 镜像名:tag . 命令来构建镜像,最后一个点是表示当前目录,docker会默认寻找当前目录下的Dockerfile文件来构建镜像,如果不使用默认,可以使用-f参数来指定dockerfile文件,如:docker build -t 镜像名:tag -f /xx/xxx/Dockerfile ; 使用docker build命令构建之后,docker就会将当前目录下所有的文件发送给docker daemon,顺序执行Dockerfile文件里的指令,在这过程中会生成临时容器,在临时容器里面安装RUN指定的命令,安装成功后,docker底层会使用类似于docker commit命令来将容器保存为镜像,然后删除临时容器,以此类推,一层层的构建镜像,运行临时容器安装软件,直到最后的镜像构建成功。 15、Dockerfile构建镜像出现异常,如何排查? 首先,Dockerfile是一层一层的构建镜像,期间会产生一个或多个临时容器,构建过程中其实就是在临时容器里面安装应用,如果因为临时容器安装应用出现异常导致镜像构建失败,这时容器虽然被清理掉了,但是期间构建的中间镜像还在,那么我们可以根据异常时上一层已经构建好的临时镜像,将临时镜像运行为容器,然后在容器里面运行安装命令来定位具体的异常。 16、Dockerfile的基本指令有哪些? FROM指定基础镜像(必须为第一个指令,因为需要指定使用哪个基础镜像来构建镜像); MAINTAINER设置镜像作者相关信息,如作者名字,日期,邮件,联系方式等; COPY复制文件到镜像; ADD复制文件到镜像(ADD与COPY的区别在于,ADD会自动解压tar、zip、tgz、xz等归档文件,而COPY不会,同时ADD指令还可以接一个url下载文件地址,一般建议使用COPY复制文件即可,文件在宿主机上是什么样子复制到镜像里面就是什么样子这样比较好); ENV设置环境变量; EXPOSE暴露容器进程的端口,仅仅是提示别人容器使用的哪个端口,没有过多作用; VOLUME数据卷持久化,挂载一个目录; WORKDIR设置工作目录,如果目录不在,则会自动创建目录; RUN在容器中运行命令,RUN指令会创建新的镜像层,RUN指令经常被用于安装软件包; CMD指定容器启动时默认运行哪些命令,如果有多个CMD,则只有最后一个生效,另外,CMD指令可以被docker run之后的参数替换; ENTRYOINT指定容器启动时运行哪些命令,如果有多个ENTRYOINT,则只有最后一个生效,另外,如果Dockerfile中同时存在CMD和ENTRYOINT,那么CMD或docker run之后的参数将被当做参数传递给ENTRYOINT; 17、如何进入容器?使用哪个命令 进入容器有两种方法:docker attach、docker exec。 18、什么是k8s?说出你的理解 K8s是kubernetes的简称,其本质是一个开源的容器编排系统,主要用于管理容器化的应用,其目标是让部署容器化的应用简单并且高效(powerful),Kubernetes提供了应用部署,规划,更新,维护的一种机制。 说简单点:k8s就是一个编排容器的系统,一个可以管理容器应用全生命周期的工具,从创建应用,应用的部署,应用提供服务,扩容缩容应用,应用更新,都非常的方便,而且还可以做到故障自愈,所以,k8s是一个非常强大的容器编排系统。 19、k8s的组件有哪些,作用分别是什么? k8s主要由master节点和node节点构成。master节点负责管理集群,node节点是容器应用真正运行的地方。 master节点包含的组件有:kube-api-server、kube-controller-manager、kube-scheduler、etcd。 node节点包含的组件有:kubelet、kube-proxy、container-runtime。 kube-api-server:以下简称api-server,api-server是k8s最重要的核心组件之一,它是k8s集群管理的统一访问入口,提供了RESTful API接口, 实现了认证、授权和准入控制等安全功能;api-server还是其他组件之间的数据交互和通信的枢纽,其他组件彼此之间并不会直接通信,其他组件对资源对象的增、删、改、查和监听操作都是交由api-server处理后,api-server再提交给etcd数据库做持久化存储,只有api-server才能直接操作etcd数据库,其他组件都不能直接操作etcd数据库,其他组件都是通过api-server间接的读取,写入数据到etcd。 kube-controller-manager:以下简称controller-manager,controller-manager是k8s中各种控制器的的管理者,是k8s集群内部的管理控制中心,也是k8s自动化功能的核心;controller-manager内部包含replication controller、node controller、deployment controller、endpoint controller等各种资源对象的控制器,每种控制器都负责一种特定资源的控制流程,而controller-manager正是这些controller的核心管理者。 kube-scheduler:以下简称scheduler,scheduler负责集群资源调度,其作用是将待调度的pod通过一系列复杂的调度算法计算出最合适的node节点,然后将pod绑定到目标节点上。shceduler会根据pod的信息(关注微信公众号:网络技术联盟站),全部节点信息列表,过滤掉不符合要求的节点,过滤出一批候选节点,然后给候选节点打分,选分最高的就是最佳节点,scheduler就会把目标pod安置到该节点。 Etcd:etcd是一个分布式的键值对存储数据库,主要是用于保存k8s集群状态数据,比如,pod,service等资源对象的信息;etcd可以是单个也可以有多个,多个就是etcd数据库集群,etcd通常部署奇数个实例,在大规模集群中,etcd有5个或7个节点就足够了;另外说明一点,etcd本质上可以不与master节点部署在一起,只要master节点能通过网络连接etcd数据库即可。 kubelet:每个node节点上都有一个kubelet服务进程,kubelet作为连接master和各node之间的桥梁,负责维护pod和容器的生命周期,当监听到master下发到本节点的任务时,比如创建、更新、终止pod等任务,kubelet 即通过控制docker来创建、更新、销毁容器;每个kubelet进程都会在api-server上注册本节点自身的信息,用于定期向master汇报本节点资源的使用情况。 kube-proxy:kube-proxy运行在node节点上,在Node节点上实现Pod网络代理,维护网络规则和四层负载均衡工作,kube-proxy会监听api-server中从而获取service和endpoint的变化情况,创建并维护路由规则以提供服务IP和负载均衡功能。简单理解此进程是Service的透明代理兼负载均衡器,其核心功能是将到某个Service的访问请求转发到后端的多个Pod实例上。 container-runtime:容器运行时环境,即运行容器所需要的一系列程序,目前k8s支持的容器运行时有很多,如docker、rkt或其他,比较受欢迎的是docker,但是新版的k8s已经宣布弃用docker。 20、kubelet的功能、作用是什么?(重点,经常会问) kubelet部署在每个node节点上的,它主要有4个功能: 节点管理。kubelet启动时会向api-server进行注册,然后会定时的向api-server汇报本节点信息状态,资源使用状态等,这样master就能够知道node节点的资源剩余,节点是否失联等等相关的信息了。master知道了整个集群所有节点的资源情况,这对于 pod 的调度和正常运行至关重要。 pod管理。kubelet负责维护node节点上pod的生命周期,当kubelet监听到master的下发到自己节点的任务时,比如要创建、更新、删除一个pod,kubelet 就会通过CRI(容器运行时接口)插件来调用不同的容器运行时来创建、更新、删除容器;常见的容器运行时有docker、containerd、rkt等等这些容器运行时,我们最熟悉的就是docker了,但在新版本的k8s已经弃用docker了,k8s1.24版本中已经使用containerd作为容器运行时了。 容器健康检查。pod中可以定义启动探针、存活探针、就绪探针等3种,我们最常用的就是存活探针、就绪探针,kubelet 会定期调用容器中的探针来检测容器是否存活,是否就绪,如果是存活探针,则会根据探测结果对检查失败的容器进行相应的重启策略; Metrics Server资源监控。在node节点上部署Metrics Server用于监控node节点、pod的CPU、内存、文件系统、网络使用等资源使用情况,而kubelet则通过Metrics Server获取所在节点及容器的上的数据。 21、kube-api-server的端口是多少?各个pod是如何访问kube-api-server的? kube-api-server的端口是8080和6443,前者是http的端口,后者是https的端口,以我本机使用kubeadm安装的k8s为例: 在命名空间的kube-system命名空间里,有一个名称为kube-api-master的pod,这个pod就是运行着kube-api-server进程,它绑定了master主机的ip地址和6443端口,但是在default命名空间下,存在一个叫kubernetes的服务,该服务对外暴露端口为443,目标端口6443,这个服务的ip地址是clusterip地址池里面的第一个地址,同时这个服务的yaml定义里面并没有指定标签选择器,也就是说这个kubernetes服务所对应的endpoint是手动创建的,该endpoint也是名称叫做kubernetes,该endpoint的yaml定义里面代理到master节点的6443端口,也就是kube-api-server的IP和端口。这样一来,其他pod访问kube-api-server的整个流程就是:pod创建后嵌入了环境变量,pod获取到了kubernetes这个服务的ip和443端口,请求到kubernetes这个服务其实就是转发到了master节点上的6443端口的kube-api-server这个pod里面。 22、k8s中命名空间的作用是什么? amespace是kubernetes系统中的一种非常重要的资源,namespace的主要作用是用来实现多套环境的资源隔离,或者说是多租户的资源隔离。 k8s通过将集群内部的资源分配到不同的namespace中,可以形成逻辑上的隔离,以方便不同的资源进行隔离使用和管理。不同的命名空间可以存在同名的资源,命名空间为资源提供了一个作用域。 可以通过k8s的授权机制,将不同的namespace交给不同的租户进行管理,这样就实现了多租户的资源隔离,还可以结合k8s的资源配额机制,限定不同的租户能占用的资源,例如CPU使用量、内存使用量等等来实现租户可用资源的管理。 23、pod资源控制器类型有哪些? Deployments:Deployment为Pod和ReplicaSet提供声明式的更新能力。 ReplicaSet:ReplicaSet的目的是维护一组在任何时候都处于运行状态的Pod副本的稳定集合。因此,它通常用来保证给定数量的、完全相同的Pod的可用性。 StatefulSets:和Deployment类似,StatefulSet管理基于相同容器规约的一组Pod。但和Deployment不同的是,StatefulSet为它们的每个Pod维护了一个有粘性的ID。这些Pod是基于相同的规约来创建的,但是不能相互替换:无论怎么调度,每个Pod都有一个永久不变的ID。 DaemonSet:DaemonSet确保全部(或者某些)节点上运行一个Pod的副本。当有节点加入集群时,也会为他们新增一个Pod。当有节点从集群移除时,这些Pod也会被回收。删除DaemonSet将会删除它创建的所有Pod。 Jobs:Job会创建一个或者多个Pod,并将继续重试Pod的执行,直到指定数量的Pod成功终止。随着Pod成功结束,Job跟踪记录成功完成的Pod个数。当数量达到指定的成功个数阈值时,任务(即Job)结束。删除Job的操作会清除所创建的全部Pod。挂起Job的操作会删除Job的所有活跃Pod,直到Job被再次恢复执行。 Automatic Clean-up for Finished Jobs:TTL-after-finished控制器提供了一种TTL机制来限制已完成执行的资源对象的生命周期。TTL控制器目前只处理Job。 CronJob:一个CronJob对象就像crontab(crontable)文件中的一行。它用Cron格式进行编写,并周期性地在给定的调度时间执行Job。 ReplicationController:ReplicationController确保在任何时候都有特定数量的Pod副本处于运行状态。换句话说,ReplicationController确保一个Pod或一组同类的Pod总是可用的。 24、nginx算法策略 轮询(默认) 加权轮询(轮询+weight) ip_hash 每一个请求的访问IP,都会映射成一个hash,再通过hash算法(hash值%node_count),分配到不同的后端服务器,访问ip相同的请求会固定访问同一个后端服务器,这样可以做到会话保持,解决session同步问题。 least_conn(最少连接) 使用最少连接的负载平衡,nginx将尝试不会使繁忙的应用程序服务器超载请求过多,而是将新请求分发给不太繁忙的服务器。 25、nignx常用模块 upstream rewrite location proxy_pass 26、如何查看并且杀死僵尸进程? top —> task (line)—> zombie. 把父进程杀掉,父进程死后,过继给1号进程init,init 始终负责清理僵尸进程,它产生的所有僵尸进程跟着消失;如果你使用kill ,一般都不能杀掉 defunct进程.。用了kill -15,kill -9以后 之后反而会多出更多的僵尸进程。 27、搜索某个用户运行的进程 pgrep-auneteagle 28、查看某个端口正在被哪个进程使用 lsof-i:[port] 29、端口转发 iptables-tnat-APREROUTING-d10.0.0.8-ptcp--dport80-jREDIRECT--to-ports8080 30、查看http的并发请求数与其TCP连接状态 etstat-n|awk'/^tcp/{++b[$NF]}END{for(ainb)printa,b[a]}' 31、查看/var/log目录下文件数 ls/var/log/-lR|grep"^-"|wc-l 32、linux系统启动流程 第一步:开机自检,加载BIOS 第二步:读取MBR 第三步:Boot Loader grub引导菜单 第四步:加载kernel内核 第五步:init进程依据inittab文件夹来设定运行级别 第六步:init进程执行rc.sysinit 第七步:启动内核模块 第八步:执行不同运行级别的脚本程序 第九步:执行/etc/rc.d/rc.lo 33、Linux文件类型 -:常规文件,即file d:目录文件 b:block device 即块设备文件,如硬盘;支持以block为单位进行随机访问 c:character device 即字符设备文件,如键盘支持以character为单位进行线性访问 l:symbolic link 即符号链接文件(关注微信公众号:网络技术联盟站),又称软链接文件 p:pipe 即命名管道文件 s:socket 即套接字文件,用于实现两个进程进行通信 34、简述lvm,如何给使用lvm的/分区扩容? 功能:可以对磁盘进行动态管理。动态按需调整大小 概念: PV 物理卷:物理卷在逻辑卷管理中处于最底层,它可以是实际物理硬盘上的分区,也可以是整个物理硬盘,也可以是raid设备。 VG 卷组:卷组建立在物理卷之上,一个卷组中至少要包括一个物理卷,在卷组建立之后可动态添加物理卷到卷组中。一个逻辑卷管理系统工程中可以只有一个卷组,也可以拥有多个卷组。 LV 逻辑卷:逻辑卷建立在卷组之上,卷组中的未分配空间可以用于建立新的逻辑卷,逻辑卷建立后可以动态地扩展和缩小空间。系统中的多个逻辑卷可以属于同一个卷组,也可以属于不同的多个卷组。 给/分区扩容步骤: 添加磁盘 使用fdisk命令对新增加的磁盘进行分区 分区完成后修改分区类型为lvm 使用pvcreate创建物理卷 使用vgextend命令将新增加的分区加入到根目录分区中 使用lvextend命令进行扩容 使用xfs_growfs调整卷分区大小 35、如何在文本里面进行复制、粘贴,删除行,删除全部,按行查找和按字母查找。 以下操作全部在vi/vim命令行状态操作,不要在编辑状态操作: 在文本里 移动到想要复制的行按yy想复制到哪就移动到哪,然后按P就黏贴了 删除行 移动到改行 按dd 删除全部dG这里注意G一定要大写 按行查找 :90 这样就是找到第90行 按字母查找 /path 这样就是找到path这个单词所在的位置,文本里可能存在多个,多次查找会显示在不同的位置。 36、符号链接与硬链接的区别 我们可以把符号链接,也就是软连接 当做是 windows系统里的 快捷方式。 硬链接 就好像是 又复制了一份. ln 3.txt 4.txt 这是硬链接,相当于复制,不可以跨分区,但修改3,4会跟着变,若删除3,4不受任何影响。 ln -s 3.txt 4.txt 这是软连接,相当于快捷方式。修改4,3也会跟着变,若删除3,4就坏掉了。不可以用了。 37、什么是正向代理? 一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。 客户端才能使用正向代理。正向代理总结就一句话:代理端代理的是客户端。例如说:我们使用的OpenVPN 等等。 38、什么是反向代理? 反向代理(Reverse Proxy)方式,是指以代理服务器来接受 Internet上的连接请求,然后将请求,发给内部网络上的服务器并将从服务器上得到的结果返回给 Internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。 反向代理总结就一句话:代理端代理的是服务端。 39、什么是动态资源、静态资源分离? 动态资源、静态资源分离,是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路。 动态资源、静态资源分离简单的概括是:动态文件与静态文件的分离。 在我们的软件开发中,有些请求是需要后台处理的(如:.jsp,.do 等等),有些请求是不需要经过后台处理的(如:css、html、jpg、js 等等文件),这些不需要经过后台处理的文件称为静态文件,否则动态文件。 因此我们后台处理忽略静态文件。这会有人又说那我后台忽略静态文件不就完了吗?当然这是可以的,但是这样后台的请求次数就明显增多了。在我们对资源的响应速度有要求的时候,我们应该使用这种动静分离的策略去解决动、静分离将网站静态资源(HTML,JavaScript,CSS,img等文件)与后台应用分开部署,提高用户访问静态代码的速度,降低对后台应用访问 这里我们将静态资源放到 Nginx 中,动态资源转发到 Tomcat 服务器中去。 当然,因为现在七牛、阿里云等 CDN 服务已经很成熟,主流的做法,是把静态资源缓存到 CDN 服务中,从而提升访问速度。 相比本地的 Nginx 来说,CDN 服务器由于在国内有更多的节点,可以实现用户的就近访问。并且,CDN 服务可以提供更大的带宽,不像我们自己的应用服务,提供的带宽是有限的。 40、网站登陆缓慢是什么原因? 网络带宽,这是一个很常见的瓶颈。 cpu、硬盘、内存配置过低,服务器负载不起来。 网站的开发代码不够完善,例如mysql语句没有进行优化,导致数据库的读写相当耗费时间。 数据库的瓶颈。当我们的数据库的数据变得越来越多的时候,那么对于数据库的读写压力肯定会变大。 41、a与b服务器不在同一网段怎么设置?设置完还ping不通怎么排查? AB服务器不在同一个网段 首先把不同IP段的服务器划分给不同的vlan 在通过通过三层交换机添加虚拟IP路由实在不同网段的vlan的连接 42、在AB两台服务器之间通过一个服务器c做软路由使用给路由器c配置两块网卡并开启自身的路由功能 vi/etc/sysconfig/network-scripts/ifcfg-eth0 查看网卡状况IP -a -s 网卡的名字 A服务器设置相关网卡信息 子网掩码:255.255.255.0 IP=10.0.0.1 网关=10.0.0.254 重启网卡生效 查看路由信息 route-n 添加对应路由 routeadd-net10.0.1.0/24gw10.0.0.11 B服务器的设置相关信息 IP=10.0.1.10 网关10.0.1.254 重启网卡生效 route-n 添加对应的路由 routeadd-net10.0.0.0/24gw10.0.1.11 C服务器的两块网卡 网卡1 IP=10.0.0.11 网关=10.0.0.254 网卡2 IP=10.0.1.11 网关=10.0.1.254 重启网卡生效 route-n vi/etc/sysctl.conf net.ipv4.ip_forword=1 43、如果PING不通怎么排查 首先先看看是不是网路接口故障水晶头或是网卡接口接触不良造成,其次检查交换机和路由等网络设备是有故障 是否关闭了防火墙和selinux机制 然后查看网卡和路由和网关是否配置正确 44、docker容器ping不通是什么原因? ifconfig 查看一下docker0网桥,ping一下网桥看看是否通,有可能是网桥配置问题 weave路由器端口6783 安装docker容器的服务器没有关闭防火墙(访问一下安装docker物理机的,是否能访问,如果不能访问就变不能访问docker) docker在创建镜像的时候没有做端口映射(出现这种情况能访问物理机不能访问docker)使用dockers ps 查看镜像的端口映射情况 端口映射不正确 查看网络配置ping网桥看是否能ping通,有可能是网桥的原因 45、如果一台办公室内主机无法上网(打不开网站),请给出你的排查步骤? 首先确定物理链路是否联通正常。 查看本机IP,路由,DNS的设置情况是否达标。 telnet检查服务器的WEB有没有开启以及防火墙是否阻拦。 ping一下网关,进行最基础的检查,通了,表示能够到达服务器。 测试到网关或路由器的通常情况,先测网关,然后再测路由器一级一级的测试。 测试ping公网ip的通常情况(记住几个外部IP), 测试DNS的通畅。ping出对应IP。 通过以上检查后,还在网管的路由器上进行检查。 46、如果我们的网站打开速度慢请说下您的排查思路? 判断原因 首先我要以用户的身份登录我们的网站,判断问题出现在我们自身原因,还是用户那边的原因。 如果是用户问题有以下几个原因: 用户那边的带宽 用户的浏览器器版本低,安装插件太多 中毒和电脑里的垃圾文件过多 用户主机的主机的性能和操作系统 如果是我们的网站自身问题有以下几个原因 网络带宽 服务器的cpu、硬盘、内存过低服务器负载不起来也就是说服务器自身的性能方面 网站代码不够完善。如mysql语句没有进行优化导致数据库读写耗时 服务器未开启图片压缩 网页下死连接过多插件使用及js文件调用频繁网站服务器的速度或是租用空间所在的服务器速度 解决思路 1、检测服务器速度的快慢 ping命令查看连接到服务器的时间和丢包情况(ping 测试网址的) 查看丢包率(1000个包没有丢一个是最理想的、一般一个速度好的机房丢包率不超过1%) ping值要小同城电信adsl ping平均值绝对不能超过20,一般都在10,跨省的平均值20-40属于正常 ping值要均匀最小值和最大值相差太大说明路由不稳定的表现 2、查看服务器自身性能 查看cpu的使用率uptime 查看内存情况free -m 查看I/O读写iostat 磁盘I/O读写等看看是那个进程大量占用系统资源导致我的服务器变慢 3、看看访问最多的URL和IP有什么特征,如果是恶意URL和IP就把他屏蔽掉如果是善意的就限流有可能是CDN回源量大造成网站无法访问 4、查看同台服务器上其他网站的打开速度,可以通过查询工具查看和自己在同一台服务器上的网站个数和网址可以看他们打开快慢 5、电信和联通互访的问题 如果是空间打开时快时慢,有时打不开那就是空间不稳定找空间商解决或是换空间伤,如果是有的地方快有的地方慢应该是网络线路问题,比如电信用户访问放在联通服务器上的网站,联通用户访问放在电信服务器上的网站,解决办吧是:使用双线空间或是多线空间 6、从网站自身的原因 网站的程序设计结构是否合理是否由于幻灯片代码影响网站打开速度(找程序设计相关人士解决) 网页的设计结构和代码错误(请专业人士进行修改) 网页的内容如:大尺寸图片、大尺寸flash、过多的引用其他网站内容,如果被引用内容的网站速度慢,也影响自身网站把。譬如友情连接可以把对方 的图片放到自己网站上 解决办法 优化图片,限制图片大小尺寸,降低图片质量,减少图片数量 限定图片的格式:jpg,png,gif 减少http的请求数(当打开网页时浏览器会发出很多对象请求,每个对象的加载都会有所延时,如果网页上的对象很多就会花费大量的时间,去除不必要的对象,将临近的图片合成一张,合并css文件) f r o m :w l j s l m z 46、如何查看二进制文件的内容 我们一般通过 hexdump 命令 来查看二进制文件的内容。 hexdump -C XXX(文件名) -C 是参数 不同的参数有不同的意义 -C 是比较规范的 十六进制和 ASCII 码显示 -c 是单字节字符显示 -b 单字节八进制显示 -o 是双字节八进制显示 -d 是双字节十进制显示 -x 是双字节十六进制显示 等等等等 47、你是怎么备份数据的,包括数据库备份? 在生产环境下,不管是应用数据、还是数据库数据首先在部署的时候就会有主从架构、或者集群,这本身就是属于数据的热备份;其实考虑冷备份,用专门一台服务器做为备份服务器,比如可以用rsync+inotify配合计划任务来实现数据的冷备份,如果是发版的包备份,正常情况下有台发布服务器,每次发版都会保存好发版的包。 48、zabbix常用术语你知道几个? 主机(host):要监控的网络设备,可由IP或DNS名称指定; 主机组(hostgroup):主机的逻辑容器,可以包含主机和模板,但同一个组织内的主机和模板不能互相链接;主机组通常在给用户或用户组指派监控权限时使用; 监控项(item):一个特定监控指标的相关的数据;这些数据来自于被监控对象;item是zabbix进行数据收集的核心,相对某个监控对象,每个item都由"key"标识; 触发器(trigger):一个表达式,用于评估某监控对象的特定item内接收到的数据是否在合理范围内,也就是阈值;接收的数据量大于阈值时,触发器状态将从"OK"转变为"Problem",当数据再次恢复到合理范围,又转变为"OK"; 事件(event):触发一个值得关注的事情,比如触发器状态转变,新的agent或重新上线的agent的自动注册等; 动作(action):指对于特定事件事先定义的处理方法,如发送通知,何时执行操作; 报警升级(escalation):发送警报或者执行远程命令的自定义方案,如每隔5分钟发送一次警报,共发送5次等; 媒介(media):发送通知的手段或者通道,如Email、Jabber或者SMS等; 通知(notification):通过选定的媒介向用户发送的有关某事件的信息;远程命令(remote command):预定义的命令,可在被监控主机处于某特定条件下时自动执行; 模板(template):用于快速定义被监控主机的预设条目集合,通常包含了item、trigger、graph、screen、application以及low-level discovery rule;模板可以直接链接至某个主机;应用(application):一组item的集合; web场景(webscennario):用于检测web站点可用性的一个活多个HTTP请求;前端(frontend):Zabbix的web接口; 49、虚拟化技术有哪些表现形式 完全拟化技术:通过软件实现对操作系统的资源再分配,比较成熟,完全虚拟化代表技术:KVM、ESXI、Hyper-V。 半虚拟化技术:通过代码修改已有的系统,形成一种新的可虚拟化的系统,调用硬件资源去安装多个系统,整体速度上相对高一点,半虚拟化代表技术:Xen。 轻量级虚拟化:介于完全虚拟化、半虚拟化之间,轻量级虚拟化代表技术:Docker。 50、修改线上业务配置文件流程 先告知运维经理和业务相关开发人员 在测试环境测试,并备份之前的配置文件 测试无误后修改生产环境配置 观察生产环境是否正常,是否有报警 完成配置文件更改 可关注小编,发布更多精彩内容。 文章转自:网络技术联盟站(版权归原作者所有,侵删)

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

面试官从Dubbo泛化调用问到设计模式,我们聊了三十分钟

欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我个人微信「java_front」一起交流学习 1 泛化调用实例 对于JAVA服务端开发者而言在使用Dubbo时并不经常使用泛化调用,通常方法是在生产者发布服务之后,消费者可以通过引入生产者提供的client进行调用。那么泛化调用使用场景是什么呢? 第一种场景是消费者不希望引入生产者提供的client依赖,只希望关注调用哪个方法,需要传什么参数即可。第二种场景是消费者不是使用Java语言,而是使用例如Python语言,那么如何调用使用Java语言生产者提供的服务呢?这时我们可以选择泛化调用。 泛化调用使用方法并不复杂,下面我们编写一个泛化调用实例。首先生产者发布服务,这与普通服务发布没有任何区别。 packagecom.java.front.dubbo.demo.provider; publicinterfaceHelloService{ publicStringsayHelloGeneric(Personperson,Stringmessage); } publicclassHelloServiceImplimplementsHelloService{ @Override publicStringsayHelloGeneric(Personperson,Stringmessage)throwsException{ Stringresult="hello["+person+"],message="+message; returnresult; } } Person类声明: packagecom.java.front.dubbo.demo.provider.model; publicclassPersonimplementsSerializable{ privateStringname; } provider.xml文件内容: <beansxmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd"> <!--提供方应用信息,用于计算依赖关系--> <dubbo:applicationname="java-front-provider"/> <!--连接注册中心--> <dubbo:registryaddress="zookeeper://127.0.0.1:2181"/> <!--生产者9999在端口暴露服务--> <dubbo:protocolname="dubbo"port="9999"/> <!--Bean--> <beanid="helloService"class="com.java.front.dubbo.demo.provider.HelloServiceImpl"/> <!--暴露服务--> <dubbo:serviceinterface="com.java.front.dubbo.demo.provider.HelloService"ref="helloService"/> </beans> 消费者代码有所不同: importorg.apache.dubbo.config.ApplicationConfig; importorg.apache.dubbo.config.ReferenceConfig; importorg.apache.dubbo.config.RegistryConfig; importorg.apache.dubbo.rpc.RpcContext; importorg.apache.dubbo.rpc.service.GenericService; publicclassConsumer{ publicstaticvoidtestGeneric(){ ReferenceConfig<GenericService>reference=newReferenceConfig<GenericService>(); reference.setApplication(newApplicationConfig("java-front-consumer")); reference.setRegistry(newRegistryConfig("zookeeper://127.0.0.1:2181")); reference.setInterface("com.java.front.dubbo.demo.provider.HelloService"); reference.setGeneric(true); GenericServicegenericService=reference.get(); Map<String,Object>person=newHashMap<String,Object>(); person.put("name","微信公众号「JAVA前线」"); Stringmessage="你好"; Objectresult=genericService.$invoke("sayHelloGeneric",newString[]{"com.java.front.dubbo.demo.provider.model.Person","java.lang.String"},newObject[]{person,message}); System.out.println(result); } } 2 Invoker 我们通过源码分析讲解泛化调用原理,我们首先需要了解Invoker这个Dubbo重量级概念。在生产者暴露服务流程总体分为两步,第一步是接口实现类转换为Invoker,第二步是Invoker转换为Exporter并放入ExporterMap,我们看看生产者暴露服务流程图: 生产者通过ProxyFactory.getInvoker方法创建Invoker(AbstractProxyInvoker): publicclassJdkProxyFactoryextendsAbstractProxyFactory{ @Override public<T>Invoker<T>getInvoker(Tproxy,Class<T>type,URLurl){ returnnewAbstractProxyInvoker<T>(proxy,type,url){ @Override protectedObjectdoInvoke(Tproxy,StringmethodName, Class<?>[]parameterTypes, Object[]arguments)throwsThrowable{ //proxy为被代理对象->com.java.front.dubbo.demo.provider.HelloServiceImpl Methodmethod=proxy.getClass().getMethod(methodName,parameterTypes); returnmethod.invoke(proxy,arguments); } }; } } 我们再看看消费者引用服务流程图: 消费者Invoker通过显示实例化创建,例如本地暴露和远程暴露都是通过显示初始化的方法创建Invoker(AbstractInvoker): newInjvmInvoker<T>(serviceType,url,url.getServiceKey(),exporterMap) newDubboInvoker<T>(serviceType,url,getClients(url),invokers) 再通过ProxyFactory.getProxy创建代理: publicclassJdkProxyFactoryextendsAbstractProxyFactory{ @Override public<T>TgetProxy(Invoker<T>invoker,Class<?>[]interfaces){ InvokerInvocationHandlerinvokerInvocationHandler=newInvokerInvocationHandler(invoker); return(T)Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),interfaces,invokerInvocationHandler); } } 无论是生产者还是消费者的Invoker都实现自org.apache.dubbo.rpc.Invoker: publicabstractclassAbstractInvoker<T>implementsInvoker<T>{ } publicabstractclassAbstractProxyInvoker<T>implementsInvoker<T>{ } 3 装饰器模式 为什么生产者和消费者都要转换为Invoker而不是不直接调用呢?我认为Invoker正是Dubbo设计精彩之处:真实调用都转换为Invoker,Dubbo就可以通过装饰器模式增强Invoker功能。我们看看什么是装饰器模式。 装饰器模式可以动态将责任附加到对象上,在不改变原始类接口情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。实现装饰器模式需要以下组件: Component(抽象构件) 核心业务抽象:可以使用接口或者抽象类 ConcreteComponent(具体构件) 实现核心业务:最终执行的业务代码 Decorator(抽象装饰器) 抽象装饰器类:实现Component并且组合一个Component对象 ConcreteDecorator(具体装饰器) 具体装饰内容:装饰核心业务代码 我们分析一个装饰器实例。有一名足球运动员要去踢球,我们用球鞋和球袜为他装饰一下,这样可以使战力值增加。 (1) Component /** *抽象构件(可以用接口替代) */ publicabstractclassComponent{ /** *踢足球(业务核心方法) */ publicabstractvoidplayFootBall(); } (2) ConcreteComponent /** *具体构件 */ publicclassConcreteComponentextendsComponent{ @Override publicvoidplayFootBall(){ System.out.println("球员踢球"); } } (3) Decorator /** *抽象装饰器 */ publicabstractclassDecoratorextendsComponent{ privateComponentcomponent=null; publicDecorator(Componentcomponent){ this.component=component; } @Override publicvoidplayFootBall(){ this.component.playFootBall(); } } (4) ConcreteDecorator /** *球袜装饰器 */ publicclassConcreteDecoratorAextendsDecorator{ publicConcreteDecoratorA(Componentcomponent){ super(component); } /** *定义球袜装饰逻辑 */ privatevoiddecorateMethod(){ System.out.println("换上球袜战力值增加"); } /** *重写父类方法 */ @Override publicvoidplayFootBall(){ this.decorateMethod(); super.playFootBall(); } } /** *球鞋装饰器 */ publicclassConcreteDecoratorBextendsDecorator{ publicConcreteDecoratorB(Componentcomponent){ super(component); } /** *定义球鞋装饰逻辑 */ privatevoiddecorateMethod(){ System.out.println("换上球鞋战力值增加"); } /** *重写父类方法 */ @Override publicvoidplayFootBall(){ this.decorateMethod(); super.playFootBall(); } } (5) 测试代码 publicclassTestDecoratorDemo{ publicstaticvoidmain(String[]args){ Componentcomponent=newConcreteComponent(); component=newConcreteDecoratorA(component); component=newConcreteDecoratorB(component); component.playFootBall(); } } //换上球鞋战力值增加 //换上球袜战力值增加 //球员踢球 4 过滤器链路 Dubbo为Invoker增强了哪些功能?过滤器链是我认为增强的最重要的功能之一,我们继续分析源码: publicclassProtocolFilterWrapperimplementsProtocol{ @Override public<T>Exporter<T>export(Invoker<T>invoker)throwsRpcException{ if(Constants.REGISTRY_PROTOCOL.equals(invoker.getUrl().getProtocol())){ returnprotocol.export(invoker); } //增加过滤器链 Invoker<T>invokerChain=buildInvokerChain(invoker,Constants.SERVICE_FILTER_KEY,Constants.PROVIDER); returnprotocol.export(invokerChain); } @Override public<T>Invoker<T>refer(Class<T>type,URLurl)throwsRpcException{ if(Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())){ returnprotocol.refer(type,url); } //增加过滤器链 Invoker<T>invoker=protocol.refer(type,url); Invoker<T>result=buildInvokerChain(invoker,Constants.REFERENCE_FILTER_KEY,Constants.CONSUMER); returnresult; } } 无论是生产者还是消费者都会创建过滤器链,我们看看buildInvokerChain这个方法: publicclassProtocolFilterWrapperimplementsProtocol{ privatefinalProtocolprotocol; publicProtocolFilterWrapper(Protocolprotocol){ if(protocol==null){ thrownewIllegalArgumentException("protocol==null"); } this.protocol=protocol; } privatestatic<T>Invoker<T>buildInvokerChain(finalInvoker<T>invoker,Stringkey,Stringgroup){ Invoker<T>last=invoker; //(1)加载所有包含Activate注解的过滤器 //(2)根据group过滤得到过滤器列表 //(3)Invoker最终被放到过滤器链尾部 List<Filter>filters=ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(),key,group); if(!filters.isEmpty()){ for(inti=filters.size()-1;i>=0;i--){ finalFilterfilter=filters.get(i); finalInvoker<T>next=last; //构造一个简化Invoker last=newInvoker<T>(){ @Override publicClass<T>getInterface(){ returninvoker.getInterface(); } @Override publicURLgetUrl(){ returninvoker.getUrl(); } @Override publicbooleanisAvailable(){ returninvoker.isAvailable(); } @Override publicResultinvoke(Invocationinvocation)throwsRpcException{ //构造过滤器链路 Resultresult=filter.invoke(next,invocation); if(resultinstanceofAsyncRpcResult){ AsyncRpcResultasyncResult=(AsyncRpcResult)result; asyncResult.thenApplyWithContext(r->filter.onResponse(r,invoker,invocation)); returnasyncResult; }else{ returnfilter.onResponse(result,invoker,invocation); } } @Override publicvoiddestroy(){ invoker.destroy(); } @Override publicStringtoString(){ returninvoker.toString(); } }; } } returnlast; } } 加载所有包含Activate注解的过滤器,根据group过滤得到过滤器列表,Invoker最终被放到过滤器链尾部,生产者最终生成链路: EchoFilter->ClassloaderFilter->GenericFilter->ContextFilter->TraceFilter->TimeoutFilter->MonitorFilter->ExceptionFilter->AbstractProxyInvoker 消费者最终生成链路: ConsumerContextFilter->FutureFilter->MonitorFilter->GenericImplFilter->DubboInvoker 5 泛化调用原理 我们终于即将看到泛化调用核心原理,我们在生产者链路看到GenericFilter过滤器,消费者链路看到GenericImplFilter过滤器,正是这两个过滤器实现了泛化调用。 (1) GenericImplFilter @Activate(group=Constants.CONSUMER,value=Constants.GENERIC_KEY,order=20000) publicclassGenericImplFilterimplementsFilter{ @Override publicResultinvoke(Invoker<?>invoker,Invocationinvocation)throwsRpcException{ //方法名=$invoke //invocation.getArguments()=("sayHelloGeneric",newString[]{"com.java.front.dubbo.demo.provider.model.Person","java.lang.String"},newObject[]{person,"你好"}); if(invocation.getMethodName().equals(Constants.$INVOKE) &&invocation.getArguments()!=null &&invocation.getArguments().length==3 &&ProtocolUtils.isGeneric(generic)){ //第一个参数表示方法名 //第二个参数表示参数类型 //第三个参数表示参数值->[{name=微信公众号「JAVA前线」},你好] Object[]args=(Object[])invocation.getArguments()[2]; if(ProtocolUtils.isJavaGenericSerialization(generic)){ for(Objectarg:args){ if(!(byte[].class==arg.getClass())){ error(generic,byte[].class.getName(),arg.getClass().getName()); } } }elseif(ProtocolUtils.isBeanGenericSerialization(generic)){ for(Objectarg:args){ if(!(arginstanceofJavaBeanDescriptor)){ error(generic,JavaBeanDescriptor.class.getName(),arg.getClass().getName()); } } } //附加参数generic值设置为true ((RpcInvocation)invocation).setAttachment(Constants.GENERIC_KEY,invoker.getUrl().getParameter(Constants.GENERIC_KEY)); } //继续执行过滤器链路 returninvoker.invoke(invocation); } } (2) GenericFilter @Activate(group=Constants.PROVIDER,order=-20000) publicclassGenericFilterimplementsFilter{ @Override publicResultinvoke(Invoker<?>invoker,Invocationinv)throwsRpcException{ //RpcInvocation[methodName=$invoke,parameterTypes=[classjava.lang.String,class[Ljava.lang.String;,class[Ljava.lang.Object;],arguments=[sayHelloGeneric,[Ljava.lang.String;@14e77f6b,[Ljava.lang.Object;@51e5f393],attachments={path=com.java.front.dubbo.demo.provider.HelloService,input=451,dubbo=2.0.2,interface=com.java.front.dubbo.demo.provider.HelloService,version=0.0.0,generic=true}] if(inv.getMethodName().equals(Constants.$INVOKE) &&inv.getArguments()!=null &&inv.getArguments().length==3 &&!GenericService.class.isAssignableFrom(invoker.getInterface())){ //sayHelloGeneric Stringname=((String)inv.getArguments()[0]).trim(); //[com.java.front.dubbo.demo.provider.model.Person,java.lang.String] String[]types=(String[])inv.getArguments()[1]; //[{name=微信公众号「JAVA前线」},你好] Object[]args=(Object[])inv.getArguments()[2]; //RpcInvocation[methodName=sayHelloGeneric,parameterTypes=[classcom.java.front.dubbo.demo.provider.model.Person,classjava.lang.String],arguments=[Person(name=JAVA前线),abc],attachments={path=com.java.front.dubbo.demo.provider.HelloService,input=451,dubbo=2.0.2,interface=com.java.front.dubbo.demo.provider.HelloService,version=0.0.0,generic=true}] RpcInvocationrpcInvocation=newRpcInvocation(method,args,inv.getAttachments()); Resultresult=invoker.invoke(rpcInvocation); } } } 6 文章总结 本文首先介绍了如何使用泛化调用,并引出泛化调用为什么生效这个问题。第二点介绍了重量级概念Invoker,并引出为什么Dubbo要创建Invoker这个问题。第三点介绍了装饰器模式如何增强功能。最后我们通过源码分析知道了过滤器链增强了Invoker功能并且是实现泛化调用的核心,希望本文对大家有所帮助。 欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我个人微信「java_front」一起交流学习

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

面试官:你说说互斥锁、自旋锁、读写锁、悲观锁、乐观锁的应用场景

前言 生活中用到的锁,用途都比较简单粗暴,上锁基本是为了防止外人进来、电动车被偷等等。 但生活中也不是没有 BUG 的,比如加锁的电动车在「广西 - 窃·格瓦拉」面前,锁就是形同虚设,只要他愿意,他就可以轻轻松松地把你电动车给「顺走」,不然打工怎么会是他这辈子不可能的事情呢?牛逼之人,必有牛逼之处。 那在编程世界里,「锁」更是五花八门,多种多样,每种锁的加锁开销以及应用场景也可能会不同。 如何用好锁,也是程序员的基本素养之一了。 高并发的场景下,如果选对了合适的锁,则会大大提高系统的性能,否则性能会降低。 所以,知道各种锁的开销,以及应用场景是很有必要的。 接下来,就谈一谈常见的这几种锁: 正文 多线程访问共享资源的时候,避免不了资源竞争而导致数据错乱的问题,所以我们通常为了解决这一问题,都会在访问共享资源之前加锁。 最常用的就是互斥锁,当然还有很多种不同的锁,比如自旋锁、读写锁、乐观锁等,不同种类的锁自然适用于不同的场景。 如果选择了错误的锁,那么在一些高并发的场景下,可能会降低系统的性能,这样用户体验就会非常差了。 所以,为了选择合适的锁,我们不仅需要清楚知道加锁的成本开销有多大,还需要分析业务场景中访问的共享资源的方式,再来还要考虑并发访问共享资源时的冲突概率。 对症下药,才能减少锁对高并发性能的影响。 那接下来,针对不同的应用场景,谈一谈「互斥锁、自旋锁、读写锁、乐观锁、悲观锁」的选择和使用。 互斥锁与自旋锁:谁更轻松自如? 最底层的两种就是会「互斥锁和自旋锁」,有很多高级的锁都是基于它们实现的,你可以认为它们是各种锁的地基,所以我们必须清楚它俩之间的区别和应用。 加锁的目的就是保证共享资源在任意时间里,只有一个线程访问,这样就可以避免多线程导致共享数据错乱的问题。 当已经有一个线程加锁后,其他线程加锁则就会失败,互斥锁和自旋锁对于加锁失败后的处理方式是不一样的: 互斥锁加锁失败后,线程会释放 CPU ,给其他线程; 自旋锁加锁失败后,线程会忙等待,直到它拿到锁; 互斥锁是一种「独占锁」,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,于是就会释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞。 对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行。如下图: 所以,互斥锁加锁失败时,会从用户态陷入到内核态,让内核帮我们切换线程,虽然简化了使用锁的难度,但是存在一定的性能开销成本。 那这个开销成本是什么呢?会有两次线程上下文切换的成本: 当线程加锁失败时,内核会把线程的状态从「运行」状态设置为「睡眠」状态,然后把 CPU 切换给其他线程运行; 接着,当锁被释放时,之前「睡眠」状态的线程会变为「就绪」状态,然后内核会在合适的时间,把 CPU 切换给该线程运行。 线程的上下文切换的是什么?当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。 上下切换的耗时有大佬统计过,大概在几十纳秒到几微秒之间,如果你锁住的代码执行时间比较短,那可能上下文切换的时间都比你锁住的代码执行时间还要长。 所以,如果你能确定被锁住的代码执行时间很短,就不应该用互斥锁,而应该选用自旋锁,否则使用互斥锁。 自旋锁是通过 CPU 提供的 CAS 函数(Compare And Swap),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。 一般加锁的过程,包含两个步骤: 第一步,查看锁的状态,如果锁是空闲的,则执行第二步; 第二步,将锁设置为当前线程持有; CAS 函数就把这两个步骤合并成一条硬件级指令,形成原子指令,这样就保证了这两个步骤是不可分割的,要么一次性执行完两个步骤,要么两个步骤都不执行。 使用自旋锁的时候,当发生多线程竞争锁的情况,加锁失败的线程会「忙等待」,直到它拿到锁。这里的「忙等待」可以用 while 循环等待实现,不过最好是使用 CPU 提供的 PAUSE 指令来实现「忙等待」,因为可以减少循环等待时的耗电量。 自旋锁是最比较简单的一种锁,一直自旋,利用 CPU 周期,直到锁可用。需要注意,在单核 CPU 上,需要抢占式的调度器(即不断通过时钟中断一个线程,运行其他线程)。否则,自旋锁在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU。 自旋锁开销少,在多核系统下一般不会主动产生线程切换,适合异步、协程等在用户态切换请求的编程方式,但如果被锁住的代码执行时间过长,自旋的线程会长时间占用 CPU 资源,所以自旋的时间和被锁住的代码执行的时间是成「正比」的关系,我们需要清楚的知道这一点。 自旋锁与互斥锁使用层面比较相似,但实现层面上完全不同:当加锁失败时,互斥锁用「线程切换」来应对,自旋锁则用「忙等待」来应对。 它俩是锁的最基本处理方式,更高级的锁都会选择其中一个来实现,比如读写锁既可以选择互斥锁实现,也可以基于自旋锁实现。 读写锁:读和写还有优先级区分? 读写锁从字面意思我们也可以知道,它由「读锁」和「写锁」两部分构成,如果只读取共享资源用「读锁」加锁,如果要修改共享资源则用「写锁」加锁。 所以,读写锁适用于能明确区分读操作和写操作的场景。 读写锁的工作原理是: 当「写锁」没有被线程持有时,多个线程能够并发地持有读锁,这大大提高了共享资源的访问效率,因为「读锁」是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。 但是,一旦「写锁」被线程持有后,读线程的获取读锁的操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞。 所以说,写锁是独占锁,因为任何时刻只能有一个线程持有写锁,类似互斥锁和自旋锁,而读锁是共享锁,因为读锁可以被多个线程同时持有。 知道了读写锁的工作原理后,我们可以发现,读写锁在读多写少的场景,能发挥出优势。 另外,根据实现的不同,读写锁可以分为「读优先锁」和「写优先锁」。 读优先锁期望的是,读锁能被更多的线程持有,以便提高读线程的并发性,它的工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 仍然可以成功获取读锁,最后直到读线程 A 和 C 释放读锁后,写线程 B 才可以成功获取读锁。如下图: 而写优先锁是优先服务写线程,其工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 获取读锁时会失败,于是读线程 C 将被阻塞在获取读锁的操作,这样只要读线程 A 释放读锁后,写线程 B 就可以成功获取读锁。如下图: 读优先锁对于读线程并发性更好,但也不是没有问题。我们试想一下,如果一直有读线程获取读锁,那么写线程将永远获取不到写锁,这就造成了写线程「饥饿」的现象。 写优先锁可以保证写线程不会饿死,但是如果一直有写线程获取写锁,读线程也会被「饿死」。 既然不管优先读锁还是写锁,对方可能会出现饿死问题,那么我们就不偏袒任何一方,搞个「公平读写锁」。 公平读写锁比较简单的一种方式是:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥饿」的现象。 互斥锁和自旋锁都是最基本的锁,读写锁可以根据场景来选择这两种锁其中的一个进行实现。 乐观锁与悲观锁:做事的心态有何不同? 前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁。 悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。 那相反的,如果多线程同时修改共享资源的概率比较低,就可以采用乐观锁。 乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。 放弃后如何重试,这跟业务场景息息相关,虽然重试的成本很高,但是冲突的概率足够低的话,还是可以接受的。 可见,乐观锁的心态是,不管三七二十一,先改了资源再说。另外,你会发现乐观锁全程并没有加锁,所以它也叫无锁编程。 这里举一个场景例子:在线文档。 我们都知道在线文档可以同时多人编辑的,如果使用了悲观锁,那么只要有一个用户正在编辑文档,此时其他用户就无法打开相同的文档了,这用户体验当然不好了。 那实现多人同时编辑,实际上是用了乐观锁,它允许多个用户打开同一个文档进行编辑,编辑完提交之后才验证修改的内容是否有冲突。 怎么样才算发生冲突?这里举个例子,比如用户 A 先在浏览器编辑文档,之后用户 B 在浏览器也打开了相同的文档进行编辑,但是用户 B 比用户 A 提交改动,这一过程用户 A 是不知道的,当 A 提交修改完的内容时,那么 A 和 B 之间并行修改的地方就会发生冲突。 服务端要怎么验证是否冲突了呢?通常方案如下: 由于发生冲突的概率比较低,所以先让用户编辑文档,但是浏览器在下载文档时会记录下服务端返回的文档版本号; 当用户提交修改时,发给服务端的请求会带上原始文档版本号,服务器收到后将它与当前版本号进行比较,如果版本号一致则修改成功,否则提交失败。 实际上,我们常见的 SVN 和 Git 也是用了乐观锁的思想,先让用户编辑代码,然后提交的时候,通过版本号来判断是否产生了冲突,发生了冲突的地方,需要我们自己修改后,再重新提交。 乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。 总结 开发过程中,最常见的就是互斥锁的了,互斥锁加锁失败时,会用「线程切换」来应对,当加锁失败的线程再次加锁成功后的这一过程,会有两次线程上下文切换的成本,性能损耗比较大。 如果我们明确知道被锁住的代码的执行时间很短,那我们应该选择开销比较小的自旋锁,因为自旋锁加锁失败时,并不会主动产生线程切换,而是一直忙等待,直到获取到锁,那么如果被锁住的代码执行时间很短,那这个忙等待的时间相对应也很短。 如果能区分读操作和写操作的场景,那读写锁就更合适了,它允许多个读线程可以同时持有读锁,提高了读的并发性。根据偏袒读方还是写方,可以分为读优先锁和写优先锁,读优先锁并发性很强,但是写线程会被饿死,而写优先锁会优先服务写线程,读线程也可能会被饿死,那为了避免饥饿的问题,于是就有了公平读写锁,它是用队列把请求锁的线程排队,并保证先入先出的原则来对线程加锁,这样便保证了某种线程不会被饿死,通用性也更好点。 互斥锁和自旋锁都是最基本的锁,读写锁可以根据场景来选择这两种锁其中的一个进行实现。 另外,互斥锁、自旋锁、读写锁都属于悲观锁,悲观锁认为并发访问共享资源时,冲突概率可能非常高,所以在访问共享资源前,都需要先加锁。 相反的,如果并发访问共享资源时,冲突概率非常低的话,就可以使用乐观锁,它的工作方式是,在访问共享资源时,不用先加锁,修改完共享资源后,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。 但是,一旦冲突概率上升,就不适合使用乐观锁了,因为它解决冲突的重试成本非常高。 不管使用的哪种锁,我们的加锁的代码范围应该尽可能的小,也就是加锁的粒度要小,这样执行速度会比较快。再来,使用上了合适的锁,就会快上加快了。 絮叨 这周末忙里偷闲了下,看了三部电影,简单说一下感受。 首先看了「利刃出鞘」,这部电影是悬疑类型,也是豆瓣高分电影,电影虽然没有什么大场面,但是单纯靠缜密的剧情铺设,全程无尿点,结尾也各种翻转,如果喜欢悬疑类电影朋友,不妨抽个时间看看。 再来,看了「花木兰」,这电影我特喵无法可说,烂片中的战斗鸡,演员都是中国人却全在说英文(导演是美国迪士尼的),这种感觉就很奇怪很别扭,好比你看西游记、水浒传英文版那样的别扭。别扭也就算了,关键剧情平淡无奇,各种无厘头的地方,反正看完之后,我非常后悔把我生命中非常珍贵的 2 个小时献给了它,如果能重来,我选择用这 2 小时睡觉。 最后,当然看了「信条」,诺兰用巨资拍摄出来的电影,花钱买飞机来撞,画面非常震撼,可以说非常有诚意了。诺兰钟爱时间的概念,这次则以时间倒流方式来呈现,非常的烧脑,反正我看完后脑袋懵懵的,我就是要这种感觉,嘻嘻。 大家好,我是小林,一个专为大家图解的工具人,如果觉得文章对你有帮助,欢迎分享给你的朋友,我们下次见! 推荐阅读 多个线程为了同个资源打起架来了,该如何让他们安分? 本文分享自微信公众号 - 小林coding(CodingLin)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Mario

Mario

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

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Sublime Text

Sublime Text

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