时间 版本 说明
2019-2-27 v 1.0 初版发布
2019-3-2 v 2.0 对于第一版进行了大幅度更新,除了修改了一些小错误之外,还增加了一些内容。
2019-4-18 v3.0 修复错误,完善内容,增加了少部分内容。
必看
本文档由 SnailClimb 整理,文章大部分内容来源于本人的开源项目 JavaGuide,你可以把这个文档看做JavaGuide
的精简版,适合面试前的突击。更多精彩内容,欢迎关注我的公众号:JavaGuide。如需转载对应的文章,请附上下
面一段内容:
本文转载自JavaGuide,地址:https://github.com/Snailclimb/JavaGuide,作者:SnailClimb
历史更新记录
建议阅读本文档的方式
本文档提供详细的目录,建议大家使用电脑阅读。如果大家用手机阅读的话,可以下载一个不错的PDF阅读器,比如
很多人常用的福昕PDF阅读器。
本文档提供详细的目录,大家可以根据自己的实际需要选择自己薄弱的知识章节阅读。
前言
不论是校招还是社招都避免不了各种面试、笔试,如何去准备这些东西就显得格外重要。不论是笔试还是面试都是有
章可循的,我这个“有章可循”说的意思只是说应对技术面试是可以提前准备。
运筹帷幄之后,决胜千里之外!不打毫无准备的仗,我觉得大家可以先从下面几个方面来准备面试:
- 自我介绍。(你可千万这样介绍:“我叫某某,性别,来自哪里,学校是那个,自己爱干什么”,记住:多说点简
历上没有的,多说点自己哪里比别人强!) - 自己面试中可能涉及哪些知识点、那些知识点是重点。
- 面试中哪些问题会被经常问到、面试中自己改如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多
少?能记住多久?第二:背题的方式的学习很难坚持下去!) 4. 自己的简历该如何写。
“80%的offer掌握在20%的人手中” 这句话也不是不无道理的。决定你面试能否成功的因素中实力固然占有很大一部
分比例,但是如果你的心态或者说运气不好的话,依然无法拿到满意的 offer。运气暂且不谈,就拿心态来说,千万
不要因为面试失败而气馁或者说怀疑自己的能力,面试失败之后多总结一下失败的原因,后面你就会发现自己会越来
越强大。
另外,大家要明确的很重要的几点是: - 写在简历上的东西一定要慎重,这可能是面试官大量提问的地方;
- 大部分应届生找工作的硬伤是没有工作经验或实习经历;
- 将自己的项目经历完美的展示出来非常重要。
笔主能力有限,如果有不对的地方或者和你想法不同的地方,敬请雅正、不舍赐教。
一 面试前的准备
1.1 如何准备一场面试
不论是校招还是社招都避免不了各种面试、笔试,如何去准备这些东西就显得格外重要。不论是笔试还是面试都是有
章可循的,我这个“有章可循”说的意思只是说应对技术面试是可以提前准备。 我其实特别不喜欢那种临近考试就提前
背啊记啊各种题的行为,非常反对!我觉得这种方法特别极端,而且在稍有一点经验的面试官面前是根本没有用的。
建议大家还是一步一个脚印踏踏实实地走。
1.1.1 如何获取大厂面试机会?
在讲如何获取大厂面试机会之前,先来给大家科普/对比一下两个校招非常常见的概念——春招和秋招。 - 招聘人数 :秋招多于春招 ; 2. 招聘时间 : 秋招一般7月左右开始,大概一直持续到10月底。但是大厂(如BAT)都会早开始早结束,所以一
定要把握好时间。春招最佳时间为3月,次佳时间为4月,进入5月基本就不会再有春招了(金三银四)。 - 应聘难度 :秋招略大于春招;
- 招聘公司: 秋招数量多,而春招数量较少,一般为秋招的补充。
综上,一般来说,秋招的含金量明显是高于春招的。
下面我就说一下我自己知道的一些方法,不过应该也涵盖了大部分获取面试机会的方法。 - 关注大厂官网,随时投递简历(走流程的网申);
- 线下参加宣讲会,直接投递简历;
- 找到师兄师姐/认识的人,帮忙内推(能够让你避开网申简历筛选,笔试筛选,还是挺不错的,不过也还是需要
你的简历够棒); - 博客发文被看中/Github优秀开源项目作者,大厂内部人员邀请你面试;
- 求职类网站投递简历(不是太推荐,适合海投);
除了这些方法,我也遇到过这样的经历:有些大公司的一些部门可能暂时没招够人,然后如果你的亲戚或者朋友刚好
在这个公司,而你正好又在寻求offer,那么面试机会基本上是有了,而且这种面试的难度好像一般还普遍比其他正
规面试低很多。
1.1.2 面试必知
下面几点概括起来就是:了解自己的能力、要应聘的公司、自己要应聘的岗位,提前做好自己我介绍以及项目介绍等
等方面的功课,确保你能在面试过程中简短清晰的回答出来(可以用Star法则来组织自己的语言)。
- 准备自己的自我介绍
从HR面、技术面到高管面/部门主管面,面试官一般会让你先自我介绍一下,所以好好准备自己的自我介绍真的非常
重要。网上一般建议的是准备好两份自我介绍:一份对hr说的,主要讲能突出自己的经历,会的编程技术一语带过;
另一份对技术面试官说的,主要讲自己会的技术细节,项目经验,经历那些就一语带过。
我这里简单分享一下我自己的自我介绍的一个简单的模板吧:
面试官,您好!我叫某某。大学时间我主要利用课外时间学习某某。在校期间参与过一个某某系统的开发,另
外,自己学习过程中也写过很多系统比如某某系统。在学习之余,我比较喜欢通过博客整理分享自己所学知
识。我现在是某某社区的认证作者,写过某某很不错的文章。另外,我获得过某某奖,我的Github上开源的某个
项目已经有多少Star了。 - 关于着装
穿西装、打领带、小皮鞋?NO!NO!NO!这是互联网公司面试又不是去走红毯,所以你只需要穿的简单大方就
好,不需要太正式。 - 随身带上自己的成绩单和简历
有的公司在面试前都会让你交一份成绩单和简历当做面试中的参考。 - 如果需要笔试就提前刷一些笔试题
平时空闲时间多的可以刷一下笔试题目(牛客网上有很多)。但是不要只刷面试题,不动手code,程序员不是为了
考试而存在的。 - 花时间一些逻辑题
面试中发现有些公司都有逻辑题测试环节,并且都把逻辑笔试成绩作为很重要的一个参考。 - 准备好自己的项目介绍
如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑:
- 对项目整体设计的一个感受(面试官可能会让你画系统的架构图)
- 在这个项目中你负责了什么、做了什么、担任了什么角色
- 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用
- 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个
棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用redis做缓存提高
访问速度和并发量、使用消息队列削峰和降流等等。
- 提前了解公司以及要应聘的岗位
面试之前一定要提前对要应聘的公司以及岗位有所了解,这一点对于喜欢海投的同学来说要格外注意。如果你去一个
公司面试连公司的主要业务或者主要产品都不了解的话,那么面试官打心里肯定会觉得你并没有很重视他们公司,所
以他们为什么要重视你呢?你也要提前了解你所要应聘岗位对你的专业能力或者其他能力的要求,比如有的岗位就是
需要英语水平比较高,需要你通过六级或者托福雅思,假如你不满足的话,那就没必要再去投递简历面试了。
1.1.3 提前准备技术面试
搞清楚自己面试中可能涉及哪些知识点、那些知识点是重点。面试中哪些问题会被经常问到、自己改如何回答。(强
烈不推荐背题,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!)
1.1.4 面试之前做好定向复习
所谓定向复习就是专门针对你要面试的公司来复习。比如你在面试之前可以在网上找找有没有你要面试的公司的面
经。
举个栗子:在我面试 ThoughtWorks 的前几天我就在网上找了一些关于 ThoughtWorks 的技术面的一些文章。然后
知道了 ThoughtWorks 的技术面会让我们在之前做的作业的基础上增加一个或两个功能,所以我提前一天就把我之
前做的程序重新重构了一下。然后在技术面的时候,简单的改了几行代码之后写个测试就完事了。如果没有提前准
备,我觉得 20 分钟我很大几率会完不成这项任务。
1.1.5 面试之后复盘
如果失败,不要灰心;如果通过,切勿狂喜。面试和工作实际上是两回事,可能很多面试未通过的人,工作能力比你
强的多,反之亦然。我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试
失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!
1.2 简历该如何写
俗话说的好:“工欲善其事,必先利其器”。准备一份好的简历对于能不能找到一份好工作起到了至关重要的作
用。
1.2.1 为什么说简历很重要?
先从面试前来说
假如你是网申,你的简历必然会经过HR的筛选,一张简历HR可能也就花费10秒钟看一下,然后HR就会决定你这一
关是Fail还是Pass。
假如你是内推,如果你的简历没有什么优势的话,就算是内推你的人再用心,也无能为力。
另外,就算你通过了筛选,后面的面试中,面试官也会根据你的简历来判断你究竟是否值得他花费很多时间去面试。
所以,简历就像是我们的一个门面一样,它在很大程度上决定了你能否进入到下一轮的面试中。
再从面试中来说
我发现大家比较喜欢看面经 ,这点无可厚非,但是大部分面经都没告诉你很多问题都是在特定条件下才问的。举个
简单的例子:一般情况下你的简历上注明你会的东西才会被问到(Java、数据结构、网络、算法这些基础是每个人必
问的),比如写了你会 redis,那面试官就很大概率会问你 redis 的一些问题。比如:redis的常见数据类型及应用场
景、redis是单线程为什么还这么快、 redis 和 memcached 的区别、redis 内存淘汰机制等等。
所以,首先,你要明确的一点是:你不会的东西就不要写在简历上。另外,你要考虑你该如何才能让你的亮点在简历
中凸显出来,比如:你在某某项目做了什么事情解决了什么问题(只要有项目就一定有要解决的问题)、你的某一个
项目里使用了什么技术后整体性能和并发量提升了很多等等。
面试和工作是两回事,聪明的人会把面试官往自己擅长的领域领,其他人则被面试官牵着鼻子走。虽说面试和工作是
两回事,但是你要想要获得自己满意的 offer ,你自身的实力必须要强。
1.2.2 这3点你必须知道
- 大部分公司的HR都说我们不看重学历(骗你的!),但是如果你的学校不出众的话,很难在一堆简历中脱颖而
出,除非你的简历上有特别的亮点,比如:某某大厂的实习经历、获得了某某大赛的奖等等。 - 大部分应届生找工作的硬伤是没有工作经验或实习经历,所以如果你是应届生就不要错过秋招和春招。一旦错
过,你后面就极大可能会面临社招,这个时候没有工作经验的你可能就会面临各种碰壁,导致找不到一个好的
工作 - 写在简历上的东西一定要慎重,这是面试官大量提问的地方;
- 将自己的项目经历完美的展示出来非常重要。
1.2.3 你必须知道的两大法则
①STAR法则(Situation Task Action Result):
Situation: 事情是在什么情况下发生;
Task:: 你是如何明确你的任务的;
Action: 针对这样的情况分析,你采用了什么行动方式;
Result: 结果怎样,在这样的情况下你学习到了什么。
简而言之,STAR法则,就是一种讲述自己故事的方式,或者说,是一个清晰、条理的作文模板。不管是什么,合理
熟练运用此法则,可以轻松的对面试官描述事物的逻辑方式,表现出自己分析阐述问题的清晰性、条理性和逻辑性。
下面这段内容摘自百度百科,我觉得写的非常不错:
STAR法则,500强面试题回答时的技巧法则,备受面试者成功者和500强HR的推崇。 由于这个法则被广泛应用
于面试问题的回答,尽管我们还在写简历阶段,但是,写简历时能把面试的问题就想好,会使自己更加主动和
自信,做到简历,面试关联性,逻辑性强,不至于在一个月后去面试,却把简历里的东西都忘掉了(更何况有
些朋友会稍微夸大简历内容)。在我们写简历时,每个人都要写上自己的工作经历,活动经历,想必每一个同
学,都会起码花上半天甚至更长的时间去搜寻脑海里所有有关的经历,争取找出最好的东西写在简历上。但是
此时,我们要注意了,简历上的任何一个信息点都有可能成为日后面试时的重点提问对象,所以说,不能只管
写上让自己感觉最牛的经历就完事了,要想到今后,在面试中,你所写的经历万一被面试官问到,你真的能回
答得流利,顺畅,且能通过这段经历,证明自己正是适合这个职位的人吗?
②FAB 法则(Feature Advantage Benefit):
Feature: 是什么;
Advantage: 比别人好在哪些地方;
Benefit: 如果雇佣你,招聘方会得到什么好处。
简单来说,这个法则主要是让你的面试官知道你的优势、招了你之后对公司有什么帮助。
1.2.4 项目经历怎么写?
简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。对于项目经历大家可以考虑
从如下几点来写:
简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。对于项目经历大家可以考虑
从如下几点来写: - 对项目整体设计的一个感受
- 在这个项目中你负责了什么、做了什么、担任了什么角色
- 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用
- 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个
棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用redis做缓存提高
访问速度和并发量、使用消息队列削峰和降流等等。
1.2.5 专业技能该怎么写?
先问一下你自己会什么,然后看看你意向的公司需要什么。一般HR可能并不太懂技术,所以他在筛选简历的时候可
能就盯着你专业技能的关键词来看。对于公司有要求而你不会的技能,你可以花几天时间学习一下,然后在简历上可
以写上自己了解这个技能。比如你可以这样写:
Dubbo:精通
Spring:精通
Docker:掌握
SOA分布式开发 :掌握
Spring Cloud:了解
1.2.6 开源程序员简历模板分享
分享一个Github上开源的程序员简历模板。包括PHP程序员简历模板、iOS程序员简历模板、Android程序员简历模
板、Web前端程序员简历模板、Java程序员简历模板、C/C++程序员简历模板、NodeJS程序员简历模板、架构师简历
模板以及通用程序员简历模板 。 Github地址:https://github.com/geekcompany/ResumeSample
如果想学如何用 Markdown 写简历写一份高质量简历,请看这里:https://github.com/Snailclimb/JavaGuide/blob/master/面试必备/手把手教你用Markdown写一份高质量的简历.md
1.2.7 其他的一些关于写简历的小tips - 尽量避免主观表述,少一点语义模糊的形容词,尽量要简洁明了,逻辑结构清晰。
- 注意排版(不需要花花绿绿的),尽量使用Markdown语法。
- 如果自己有博客或者个人技术栈点的话,写上去会为你加分很多。
- 如果自己的Github比较活跃的话,写上去也会为你加分很多。
- 注意简历真实性,一定不要写自己不会的东西,或者带有欺骗性的内容
- 项目经历建议以时间倒序排序,另外项目经历不在于多,而在于有亮点。
- 如果内容过多的话,不需要非把内容压缩到一页,保持排版干净整洁就可以了。
- 简历最后最好能加上:“感谢您花时间阅读我的简历,期待能有机会和您共事。”这句话,显的你会很有礼貌。
1.3 如果面试官问你“你有什么问题问我吗?”时,你该如何回答
我还记得当时我去参加面试的时候,几乎每一场面试,特别是HR面和高管面的时候,面试官总是会在结尾问我:“问了
你这么多问题了,你有什么问题问我吗?”。这个时候很多人内心就会陷入短暂的纠结中:我该问吗?不问的话面试官
会不会对我影响不好?问什么问题?问这个问题会不会让面试官对我的影响不好啊?
1.3.1 这个问题对最终面试结果的影响到底大不大?
就技术面试而言,回答这个问题的时候,只要你不是触碰到你所面试的公司的雷区,那么我觉得这对你能不能拿到最
终offer来说影响确实是不大的。我说这些并不代表你就可以直接对面试官说:“我没问题了。”,笔主当时面试的时候
确实也说过挺多次“没问题要问了。”,最终也没有导致笔主被pass掉(可能是前面表现比较好,哈哈,自恋一下)。
我现在回想起来,觉得自己当时做法其实挺不对的。面试本身就是一个双向选择的过程,你对这个问题的回答也会侧
面反映出你对这次面试的上心程度,你的问题是否有价值,也影响了你最终的选择与公司是否选择你。
面试官在技术面试中主要考察的还是你这样个人到底有没有胜任这个工作的能力以及你是否适合公司未来的发展需
要,很多公司还需要你认同它的文化,我觉得你只要不是太笨,应该不会栽在这里。除非你和另外一个人在能力上相
同,但是只能在你们两个人中选一个,那么这个问题才对你能不能拿到offer至关重要。有准备总比没准备好,给面
试官留一个好的影响总归是没错的。
但是,就非技术面试来说,我觉得好好回答这个问题对你最终的结果还是比较重要的。
总的来说不管是技术面试还是非技术面试,如果你想赢得公司的青睐和尊重,我觉得我们都应该重视这个问题。
1.3.2 真诚一点,不要问太 Low 的问题
回答这个问题很重要的一点就是你没有必要放低自己的姿态问一些很虚或者故意讨好面试官的问题,也不要把自己从
面经上学到的东西照搬下来使用。面试官也不是傻子,特别是那种特别有经验的面试官,你是真心诚意的问问题,还
是从别处照搬问题来讨好面试官,人家可能一听就听出来了。总的来说,还是要真诚。除此之外,不要问太Low的问
题,会显得你整个人格局比较小或者说你根本没有准备(侧面反映你对这家公司不伤心,既然你不上心,为什么要要
你呢)。举例几个比较 Low 的问题,大家看看自己有没有问过其中的问题:
贵公司的主要业务是什么?(面试之前自己不知道提前网上查一下吗?)
贵公司的男女比例如何?(考虑脱单?记住你是来工作的!)
贵公司一年搞几次外出旅游?(你是来工作的,这些娱乐活动先别放在心上!)
…
1.3.3 有哪些有价值的问题值得问?
针对这个问题。笔主专门找了几个专门做HR工作的小哥哥小姐姐们询问并且查阅了挺多前辈们的回答,然后结合自
己的实际经历,我概括了下面几个比较适合问的问题。
面对HR或者其他Level比较低的面试官时 - 能不能谈谈你作为一个公司老员工对公司的感受? (这个问题比较容易回答,不会让面试官陷入无话可说的尴尬
境地。另外,从面试官的回答中你可以加深对这个公司的了解,让你更加清楚这个公司到底是不是你想的那样
或者说你是否能适应这个公司的文化。除此之外,这样的问题在某种程度上还可以拉进你与面试官的距离。) 2. 能不能问一下,你当时因为什么原因选择加入这家公司的呢或者说这家公司有哪些地方吸引你?有什么地方你觉
得还不太好或者可以继续完善吗? (类似第一个问题,都是问面试官个人对于公司的看法,) - 我觉得我这次表现的不是太好,你有什么建议或者评价给我吗?(这个是我常问的。我觉得说自己表现不好只是
这个语境需要这样来说,这样可以显的你比较谦虚好学上进。) 4. 接下来我会有一段空档期,有什么值得注意或者建议学习的吗? (体现出你对工作比较上心,自助学习意识比
较强。) - 这个岗位为什么还在招人? (岗位真实性和价值咨询) 6. 大概什么时候能给我回复呢? (终面的时候,如果面试官没有说的话,可以问一下) 7. …
面对部门领导 - 部门的主要人员分配以及对应的主要工作能简单介绍一下吗?
- 未来如果我要加入这个团队,你对我的期望是什么? (部门领导一般情况下是你的直属上级了,你以后和他打
交道的机会应该是最多的。你问这个问题,会让他感觉你是一个对他的部门比较上心,比较有团体意识,并且
愿意倾听的候选人。) - 公司对新入职的员工的培养机制是什么样的呢? (正规的公司一般都有培养机制,提前问一下是对你自己的负
责也会显的你比较上心) - 以您来看,这个岗位未来在公司内部的发展如何? (在我看来,问这个问题也是对你自己的负责吧,谁不想发展
前景更好的岗位呢?) 5. 团队现在面临的最大挑战是什么? (这样的问题不会暴露你对公司的不了解,并且也能让你对未来工作的挑战或
困难有一个提前的预期。)
面对Level比较高的(比如总裁,老板) 1. 贵公司的发展目标和方向是什么? (看下公司的发展是否满足自己的期望) - 与同行业的竞争者相比,贵公司的核心竞争优势在什么地方? (充分了解自己的优势和劣势)
- 公司现在面临的最大挑战是什么?
1.3.4 来个补充,顺便送个祝福给大家
薪酬待遇和相关福利问题一般在终面的时候(最好不要在前面几面的时候就问到这个问题),面试官会提出来或者在
面试完之后以邮件的形式告知你。一般来说,如果面试官很愿意为你回答问题,对你的问题也比较上心的话,那他肯
定是觉得你就是他们要招的人。
大家在面试的时候,可以根据自己对于公司或者岗位的了解程度,对上面提到的问题进行适当修饰或者修改。上面提
到的一些问题只是给没有经验的朋友一个参考,如果你还有其他比较好的问题的话,那当然也更好啦!
金三银四。过了二月就到了面试高峰期或者说是黄金期。几份惊喜几份愁,愿各位能始终不忘初心!每个人都有每个
人的难处。引用一句《阿甘正传》里面的台词:“生活就像一盒巧克力,你永远不知道下一块是什么味道“。
1.4 面试官问你的优点是什么,应该如何回答?
回答这样的问题,最好能够结合你要应聘的职位来做针对性回答。一般面试官问这个问题的时候,很可能会只让你说
几个你觉得最能体现你能力的优点,为了避免自己在面试过程中不知道该说自己的那些优点,你可以在面试之前好好
准备一下。 面试的时候最好可以说几个你要应聘的职位所做的事情需要的优点或者说你要应聘的公司比较看重的优
点(企业文化)。
1.5 面试官问你的缺点是什么,应该如何回答?
缺点肯定不能是目标岗位需要的关键能力!!!
总之,记住一点,面试官问你这个问题的话,你可以说一些不影响你这个职位工作需要的一些缺点。比如你面试后端
工程师,面试官问你的缺点是什么的话,你可以这样说:自己比较内向,平时不太爱与人交流,但是考虑到以后可能
要和客户沟通,自己正在努力改。
1.6 七个大部分程序员在面试前很关心的问题
身边的朋友或者公众号的粉丝很多人都向我询问过:“我是双非/三本/专科学校的,我有机会进入大厂吗?”、“非计
算机专业的学生能学好吗?”、“如何学习Java?”、“Java学习该学那些东西?”、“我该如何准备Java面试?”…这些方
面的问题。我会根据自己的一点经验对大部分人关心的这些问题进行答疑解惑。现在又刚好赶上考研结束,这篇文章
也算是给考研结束准备往Java后端方向发展的朋友们指名一条学习之路。道理懂了如果没有实际行动,那这篇文章对
你或许没有任何意义。
Question1:我是双非/三本/专科学校的,我有机会进入大厂吗?
我自己也是非985非211学校的,结合自己的经历以及一些朋友的经历,我觉得让我回答这个问题再好不过。
首先,我觉得学校歧视很正常,真的太正常了,如果要抱怨的话,你只能抱怨自己没有进入名校。但是,千万不
要动不动说自己学校差,动不动拿自己学校当做自己进不了大厂的借口,学历只是筛选简历的很多标准中的一个而
已,如果你够优秀,简历够丰富,你也一样可以和名校同学一起同台竞争。
企业HR肯定是更喜欢高学历的人,毕竟985,211优秀人才比例肯定比普通学校高很多,HR团队肯定会优先在这
些学校里选。这就好比相亲,你是愿意在很多优秀的人中选一个优秀的,还是愿意在很多普通的人中选一个优秀的
呢? 双非本科甚至是二本、三本甚至是专科的同学也有很多进入大厂的,不过比率相比于名校的低很多而
已。从大厂招聘的结果上看,高学历人才的数量占据大头,那些成功进入BAT、美团,京东,网易等大厂的双非本科
甚至是二本、三本甚至是专科的同学往往是因为具备丰富的项目经历或者在某个含金量比较高的竞赛比如ACM中取得
了不错的成绩。一部分学历不突出但能力出众的面试者能够进入大厂并不是说明学历不重要,而是学历的软肋能够通
过其他的优势来弥补。 所以,如果你的学校不够好而你自己又想去大厂的话,建议你可以从这几点来做:①尽量在
面试前最好有一个可以拿的出手的项目;②有实习条件的话,尽早出去实习,实习经历也会是你的简历的一个亮点
(有能力在大厂实习最佳!);③参加一些含金量比较高的比赛,拿不拿得到名次没关系,重在锻炼。
Question2:非计算机专业的学生能学好Java后台吗?我能进大厂吗?
当然可以!现在非科班的程序员很多,很大一部分原因是互联网行业的工资比较高。我们学校外面的培训班里面
90%都是非科班,我觉得他们很多人学的都还不错。另外,我的一个朋友本科是机械专业,大一开始自学安卓,技术
贼溜,在我看来他比大部分本科是计算机的同学学的还要好。参考Question1的回答,即使你是非科班程序员,如果
你想进入大厂的话,你也可以通过自己的其他优势来弥补。
我觉得我们不应该因为自己的专业给自己划界限或者贴标签,说实话,很多科班的同学可能并不如你,你以为科
班的同学就会认真听讲吗?还不是几乎全靠自己课下自学!不过如果你是非科班的话,你想要学好,那么注定就要舍
弃自己本专业的一些学习时间,这是无可厚非的。
建议非科班的同学,首先要打好计算机基础知识基础:①计算机网络、②操作系统、③数据机构与算法,我个人
觉得这3个对你最重要。这些东西就像是内功,对你以后的长远发展非常有用。当然,如果你想要进大厂的话,这些
知识也是一定会被问到的。另外,“一定学好数据机构与算法!一定学好数据机构与算法!一定学好数据机构与算
法!”,重要的东西说3遍。
Question3: 我没有实习经历的话找工作是不是特别艰难?
没有实习经历没关系,只要你有拿得出手的项目或者大赛经历的话,你依然有可能拿到大厂的 offer 。笔主当时
找工作的时候就没有实习经历以及大赛获奖经历,单纯就是凭借自己的项目经验撑起了整个面试。
如果你既没有实习经历,又没有拿得出手的项目或者大赛经历的话,我觉得在简历关,除非你有其他特别的亮
点,不然,你应该就会被刷。
Question4: 我该如何准备面试呢?面试的注意事项有哪些呢?
下面是我总结的一些准备面试的Tips以及面试必备的注意事项: - 准备一份自己的自我介绍,面试的时候根据面试对象适当进行修改(突出重点,突出自己的优势在哪里,切忌
流水账); - 注意随身带上自己的成绩单和简历复印件; (有的公司在面试前都会让你交一份成绩单和简历当做面试中的参
考。) - 如果需要笔试就提前刷一些笔试题,大部分在线笔试的类型是选择题+编程题,有的还会有简答题。(平时空闲
时间多的可以刷一下笔试题目(牛客网上有很多),但是不要只刷面试题,不动手code,程序员不是为了考试
而存在的。)另外,注意抓重点,因为题目太多了,但是有很多题目几乎次次遇到,像这样的题目一定要搞
定。 - 提前准备技术面试。 搞清楚自己面试中可能涉及哪些知识点、那些知识点是重点。面试中哪些问题会被经常问
到、自己改如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方
式的学习很难坚持下去!) 5. 面试之前做好定向复习。 也就是专门针对你要面试的公司来复习。比如你在面试之前可以在网上找找有没有你
要面试的公司的面经。 - 准备好自己的项目介绍。 如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你
可以从下面几个方向来考虑:①对项目整体设计的一个感受(面试官可能会让你画系统的架构图;②在这个项
目中你负责了什么、做了什么、担任了什么角色;③ 从这个项目中你学会了那些东西,使用到了那些技术,学
会了那些新技术的使用;④项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开
发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能
比如:用redis做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。 - 面试之后记得复盘。 面试遭遇失败是很正常的事情,所以善于总结自己的失败原因才是最重要的。如果失败,
不要灰心;如果通过,切勿狂喜。
一些还算不错的 Java面试/学习相关的仓库,相信对大家准备面试一定有帮助:盘点一下Github上开源的Java面试/
学习相关的仓库,看完弄懂薪资至少增加10k
Question5: 我该自学还是报培训班呢?
我本人更加赞同自学(你要知道去了公司可没人手把手教你了,而且几乎所有的公司都对培训班出生的有偏见。
为什么有偏见,你学个东西还要去培训班,说明什么,同等水平下,你的自学能力以及自律能力一定是比不上自学的
人的)。但是如果,你连每天在寝室坚持学上8个小时以上都坚持不了,或者总是容易半途而废的话,我还是推荐你
去培训班。观望身边同学去培训班的,大多是非计算机专业或者是没有自律能力以及自学能力非常差的人。
另外,如果自律能力不行,你也可以通过结伴学习、参加老师的项目等方式来督促自己学习。
总结:去不去培训班主要还是看自己,如果自己能坚持自学就自学,坚持不下来就去培训班。
Question6: 没有项目经历/博客/Github开源项目怎么办?
从现在开始做!
网上有很多非常不错的项目视频,你就跟着一步一步做,不光要做,还要改进,改善。另外,如果你的老师有相
关 Java 后台项目的话,你也可以主动申请参与进来。
如果有自己的博客,也算是简历上的一个亮点。建议可以在掘金、Segmentfault、CSDN等技术交流社区写博
客,当然,你也可以自己搭建一个博客(采用 Hexo+Githu Pages 搭建非常简单)。写一些什么?学习笔记、实战内
容、读书笔记等等都可以。
多用 Github,用好 Github,上传自己不错的项目,写好 readme 文档,在其他技术社区做好宣传。相信你也会
收获一个不错的开源项目!
Question7: 大厂到底青睐什么样的应届生?
从阿里、腾讯等大厂招聘官网对于Java后端方向/后端方向的应届实习生的要求,我们大概可以总结归纳出下面
这 4 点能给简历增加很多分数:
参加过竞赛(含金量超高的是ACM);
对数据结构与算法非常熟练;
参与过实际项目(比如学校网站);
参与过某个知名的开源项目或者自己的某个开源项目很不错;
除了我上面说的这三点,在面试Java工程师的时候,下面几点也提升你的个人竞争力:
熟悉Python、Shell、Perl等脚本语言;
熟悉 Java 优化,JVM调优;
熟悉 SOA 模式;
熟悉自己所用框架的底层知识比如Spring;
了解分布式一些常见的理论;
具备高并发开发经验;大数据开发经验等等。
二 Java
2.1 Java 基础知识
2.1.1 重载和重写的区别
重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以
不同,发生在编译时。
重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,
访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。
2.1.2 String 和 StringBuffer、StringBuilder 的区别是什么?String 为什
么是不可变的?
可变性
简单的来说:String 类中使用 final 关键字字符数组保存字符串, private final char value[] ,所以 String
对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder
中也是使用字符数组保存字符串 char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。
StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自
行查阅源码。
AbstractStringBuilder.java
线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与
StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共
方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对
方法进行加同步锁,所以是非线程安全的。
性能
abstract class AbstractStringBuilder implements Appendable, CharSequence { char[] value; int count; AbstractStringBuilder() { }AbstractStringBuilder(int capacity) { value = new char[capacity]; }
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。
StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用
StirngBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结: - 操作少量的数据 = String
- 单线程操作字符串缓冲区下操作大量数据 = StringBuilder
- 多线程操作字符串缓冲区下操作大量数据 = StringBuffer
2.1.3 自动装箱与拆箱
装箱:将基本类型用它们对应的引用类型包装起来;
拆箱:将包装类型转换为基本数据类型;
2.1.4 == 与 equals
== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型比较的是
值,引用数据类型比较的是内存地址)
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“”比较这两个对象。
情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相
等,则返回 true (即,认为这两个对象相等)。
举个例子:
说明:
String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的
equals 方法比较的是对象的值。
当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有
就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
public class test1 { public static void main(String[] args) { String a = new String(“ab”); // a 为一个引用 String b = new String(“ab”); // b为另一个引用,对象的内容一样 String aa = “ab”; // 放在常量池中 String bb = “ab”; // 从常量池中查找 if (aa == bb) // true System.out.println("aabb"); if (a == b) // false,非同一对象 System.out.println(“a==b”); if (a.equals(b)) // true System.out.println(“aEQb”); if (42 == 42.0) { // true System.out.println(“true”); } } }
2.1.5 关于 final 关键字的一些总结
final关键字主要用在三个地方:变量、方法、类。 - 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的
变量,则在对其初始化之后便不能再让其指向另一个对象。 - 当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。
- 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。
在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的
任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地
指定为fianl。
2.1.6 Object类的常见方法总结
Object类是一个特殊的类,是所有类的父类。它主要提供了以下11个方法:
2.1.7 Java 中的异常处理
public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了 final关键字修饰,故不允许子类重写。 public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的 HashMap。 public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户 比较字符串的值是否相等。 protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回 当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生 CloneNotSupportedException异常。 public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方 法。public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视 器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。 public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒 在此对象监视器上等待的所有线程,而不是一个线程。 public final native void wait(long timeout) throws InterruptedException//native方法,并且不能 重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。 public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数, 这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。 public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等 待,没有超时时间这个概念 protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作
Java异常类层次结构图
在 Java 中,所有的异常都有一个共同的祖先java.lang包中的 Throwable类。Throwable: 有两个重要的子类:
Exception(异常) 和 Error(错误) ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无
关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当
JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一
般会选择线程终止。
这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual
MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和
处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错
误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。
Exception(异常):是程序本身可以处理的异常。Exception 类有一个重要的子类 RuntimeException。
RuntimeException 异常由Java虚拟机抛出。NullPointerException(要访问的变量没有引用任何对象时,抛出该
异常)、ArithmeticException(算术运算异常,一个整数除以0时,抛出该异常)和
ArrayIndexOutOfBoundsException (下标越界异常)。
注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。
Throwable类常用方法
public string getMessage():返回异常发生时的详细信息
public string toString():返回异常发生时的简要描述
public string getLocalizedMessage():返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可
以声称本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同
public void printStackTrace():在控制台上打印Throwable对象封装的异常信息
异常处理总结
try 块:用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。
catch 块:用于处理try捕获到的异常。
finally 块:无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return语句
时,finally语句块将在方法返回之前被执行。
在以下4种特殊情况下,finally块不会被执行: - 在finally语句块中发生了异常。
- 在前面的代码中用了System.exit()退出程序。
- 程序所在的线程死亡。
- 关闭CPU。
2.1.8 获取用键盘输入常用的的两种方法
方法1:通过 Scanner
方法2:通过 BufferedReader
2.1.9 接口和抽象类的区别是什么 - 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),抽象类可以
有非抽象的方法 - 接口中的实例变量默认是 final 类型的,而抽象类中则不一定
- 一个类可以实现多个接口,但最多只能实现一个抽象类
- 一个类实现接口的话要实现接口的所有方法,而抽象类不一定
- 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计层面来说,抽象是对类的抽
象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
备注:在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现
两个接口,接口中定义了一样的默认方法,必须重写,不然会报错。(详见
issue:https://github.com/Snailclimb/JavaGuide/issues/146)
2.2 Java 集合框架
2.2.1 Arraylist 与 LinkedList 异同 - 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
- 底层数据结构: Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向链表数据结构(JDK1.6之
前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别:); 详细可阅读JDK1.7-LinkedList
循环链表优化
Scanner input = new Scanner(System.in); String s = input.nextLine(); input.close(); BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); String s = input.readLine(); - 插入和删除是否受元素位置的影响: ① ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素
位置的影响。 比如:执行 add(E e) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种
情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话( add(int index, E element) )时
间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向
前移一位的操作。 ② LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是
近似 O(1)而数组为近似 O(n)。 - 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通
过元素的序号快速获取元素对象(对应于 get(int index) 方法)。 - 内存空间占用: ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空
间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数
据)。
补充内容:RandomAccess接口
public interface RandomAccess { }
查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过是一个
标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。
在binarySearch()方法中,它要判断传入的list 是否RamdomAccess的实例,如果是,调用
indexedBinarySearch()方法,如果不是,那么调用iteratorBinarySearch()方法
public static int binarySearch(List<? extends Comparable<? super T>> list, T key) { if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD) return Collections.indexedBinarySearch(list, key); elsereturn Collections.iteratorBinarySearch(list, key); }
ArrayList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。为什么呢?我觉得还是和底层数据结构有关!
ArrayList 底层是数组,而 LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随
机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。,
ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。 RandomAccess 接口只是标识,并不
是说 ArrayList 实现 RandomAccess 接口才具有快速随机访问功能的!
下面再总结一下 list 的遍历方式选择:
实现了RandomAccess接口的list,优先选择普通for循环 ,其次foreach,
未实现RandomAccess接口的list, 优先选择iterator遍历(foreach遍历底层也是通过iterator实现的),大
size的数据,千万不要使用普通for循环
补充:数据结构基础之双向链表
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从
双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表,如
下图所示,同时下图也是LinkedList 底层使用的是双向循环链表数据结构。
2.2.2 ArrayList 与 Vector 区别
Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要
在同步操作上耗费大量的时间。
Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。
2.2.3 HashMap的底层实现
JDK1.8之前
JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经
过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的
长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的
话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的
hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
JDK 1.8 HashMap 的 hash 方法源码:
JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。
对比一下 JDK1.7的 HashMap 的 hash 方法源码.
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希
冲突,则将冲突的值加到链表中即可。
static final int hash(Object key) { int h; // key.hashCode():返回散列值也就是hashcode // ^ :按位异或 // >>>:无符号右移,忽略符号位,空位都以0补齐 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
JDK1.8之后
相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转
化为红黑树,以减少搜索时间。
TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺
陷,因为二叉查找树在某些情况下会退化成一个线性结构。
推荐阅读:
《Java 8系列之重新认识HashMap》 :https://zhuanlan.zhihu.com/p/21673805
2.2.4 HashMap 和 Hashtable 的区别 - 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过
synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!); - 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在
代码中使用它; - 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键
所对应的值为 null。。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。 4. 初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为
11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来
的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充
为2的幂次方大小(HashMap 中的 tableSizeFor() 方法保证,下面给出了源代码)。也就是说 HashMap 总
是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。 - 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为
8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
HasMap 中带有初始容量的构造函数:
下面这个方法保证了 HashMap 总是使用2的幂作为哈希表的大小。
2.2.5 HashMap 的长度为什么是2的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的
范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应
用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之
前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算
方法是“ (n - 1) & hash ”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其
除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采
用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
2.2.6 HashMap 多线程操作导致死循环问题
在多线程下,进行 put 操作会导致 HashMap 死循环,原因在于 HashMap 的扩容 resize()方法。由于扩容是新建一
个数组,复制原数据到数组。由于数组下标挂有链表,所以需要复制链表,但是多线程操作有可能导致环形链表。复
制链表过程如下:
以下模拟2个线程同时扩容。假设,当前 HashMap 的空间为2(临界值为1),hashcode 分别为 0 和 1,在散列地
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }/*** Returns a power of two size for the given target capacity. */ static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
址 0 处有元素 A 和 B,这时候要添加元素 C,C 经过 hash 运算,得到散列地址为 1,这时候由于超过了临界值,空
间不够,需要调用 resize 方法进行扩容,那么在多线程条件下,会出现条件竞争,模拟过程如下:
线程一:读取到当前的 HashMap 情况,在准备扩容时,线程二介入
线程二:读取 HashMap,进行扩容
线程一:继续执行
这个过程为,先将 A 复制到新的 hash 表中,然后接着复制 B 到链头(A 的前边:B.next=A),本来 B.next=null,
到此也就结束了(跟线程二一样的过程),但是,由于线程二扩容的原因,将 B.next=A,所以,这里继续复制A,让
A.next=B,由此,环形链表出现:B.next=A; A.next=B
注意:jdk1.8已经解决了死循环的问题。
2.2.7 HashSet 和 HashMap 区别
如果你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常
少,因为除了 clone() 方法、writeObject()方法、readObject()方法是 HashSet 自己不得不实现之外,其他方法都是
直接调用 HashMap 中的方法。)
2.2.8 ConcurrentHashMap 和 Hashtable 的区别
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟
HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类
似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了
分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁
竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑
树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很
多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,
但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,
效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用
put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
两者的对比图:
图片来源:http://www.cnblogs.com/chengxiao/p/6842045.html
HashTable:
JDK1.7的ConcurrentHashMap:
JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):
2.2.9 ConcurrentHashMap线程安全的具体实现方式/底层具体实现
JDK1.7(上面有示意图)
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的
数据也能被其他线程访问。
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。
Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数
据。
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结
构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个
HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。
JDK1.8 (上面有示意图)
ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8
的结构类似,数组+链表/红黑二叉树。
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
2.2.10 集合框架底层数据结构总结
Collection - List
Arraylist: Object数组
Vector: Object数组
LinkedList: 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环) 详细可阅读JDK1.7-LinkedList循环链表优
化 - Set
HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素
LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类
似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)
Map
HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希
冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默
认为8)时,将链表转化为红黑树,以减少搜索时间
LinkedHashMap: LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和
链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以
保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:
《LinkedHashMap 源码详细分析(JDK1.8)》
HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
TreeMap: 红黑树(自平衡的排序二叉树)
2.3 Java多线程
关于 Java多线程,在面试的时候,问的比较多的就是①悲观锁和乐观锁( 具体可以看我的这篇文章:面试必备之乐
观锁与悲观锁)、②synchronized和lock区别以及volatile和synchronized的区别,③可重入锁与非可重入锁的区
别、④多线程是解决什么问题的、⑤线程池解决什么问题、⑥线程池的原理、⑦线程池使用时的注意事项、⑧AQS原
理、⑨ReentranLock源码,设计原理,整体过程 等等问题。
static class Segment<K,V> extends ReentrantLock implements Serializable { }
面试官在多线程这一部分很可能会问你有没有在项目中实际使用多线程的经历。所以,如果你在你的项目中有实
际使用Java多线程的经历 的话,会为你加分不少哦!
一 面试中关于 synchronized 关键字的 5 连击
1.1 说一说自己对于 synchronized 关键字的了解
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者
代码块在任意时刻只能有一个线程执行。
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操
作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,
都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要
相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后
Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对
锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的
开销。
1.2 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗
synchronized关键字最主要的三种使用方式:
修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。也就是给当前类加锁,会作
用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态
资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实
例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允
许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态
synchronized 方法占用的锁是当前实例对象锁。
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 和 synchronized 方
法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和
synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态
方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串常量
池具有缓冲功能!
下面我已一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。
面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理
呗!”
双重校验锁实现对象单例(线程安全)
public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { }public static Singleton getUniqueInstance() { //先判断对象是否已经实例过,没有实例化过才进入加锁代码
另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分
为三步执行: - 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出先问题,但是在
多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用
getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被
初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
1.3 讲一下 synchronized 关键字的底层原理
synchronized 关键字底层原理属于 JVM 层面。
① synchronized 同步语句块的情况
通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行 javap -c -s -v -l SynchronizedDemo.class 。 if (uniqueInstance == null) { //类对象加锁 synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } }return uniqueInstance; } }public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println(“synchronized 代码块”); } } }
从上面我们可以看出:
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同
步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图
获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取
锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设
为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当
前线程就要阻塞等待,直到锁被另外一个线程释放为止。
② synchronized 修饰方法的的情况
public class SynchronizedDemo2 { public synchronized void method() { System.out.println(“synchronized 方法”); } }
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是
ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来
辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
1.4 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优
化吗
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减
少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐
渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
关于这几种优化的详细信息可以查看:synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和
ReenTrantLock 的对比
1.5 谈谈 synchronized和ReenTrantLock 的区别
① 两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时
这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死
锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API
synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多
优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就
是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看
它是如何实现的。
③ ReenTrantLock 比 synchronized 增加了一些高级功能
相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;
③可实现选择性通知(锁可以绑定多个条件)
ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也
就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等
待的线程先获得锁。 ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的
ReentrantLock(boolean fair) 构造方法来制定是否是公平的。
synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也
可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很
好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视
器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵
活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合
Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而
synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果
执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的
signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。
④ 性能已不是选择标准
二 面试中关于线程池的 4 连击
2.1 讲一下Java内存模型
在 JDK1.2 之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前
的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就
可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数
据的不一致。
要解决这个问题,就需要把变量声明为 volatile,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行
读取。
说白了, volatile 关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。
2.2 说说 synchronized 关键字和 volatile 关键字的区别
synchronized关键字和volatile关键字比较
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关
键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进
行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行
效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访
问资源的同步性。
三 面试中关于 线程池的 2 连击
3.1 为什么要用线程池?
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务
的数量。
这里借用《Java并发编程的艺术》提到的来说一下使用线程池的好处:
降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,
使用线程池可以进行统一的分配,调优和监控。
3.2 实现Runnable接口和Callable接口的区别
如果想让线程池执行任务的话需要实现的Runnable接口或Callable接口。 Runnable接口或Callable接口实现类都可
以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。两者的区别在于 Runnable 接口不会返回结果但
是 Callable 接口可以返回结果。
备注: 工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。
( Executors.callable(Runnable task) 或 Executors.callable(Runnable task,Object resule) )。
3.3 执行execute()方法和submit()方法的区别是什么呢?
- execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
2)submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断
任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用
get(long timeout,TimeUnit unit) 方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行
完。
3.4 如何创建线程池
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这
样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors 返回线程池对象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积
大量的请求,从而导致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能
会创建大量线程,从而导致OOM。
方式一:通过构造方法实现
方式二:通过Executor 框架的工具类Executors来实现 我们可以创建三种类型的ThreadPoolExecutor:
FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的
任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线
程空闲时,便处理在任务队列中的任务。
SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会
被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但
若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新
的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
对应Executors工具类中的方法如图所示:
四 面试中关于 Atomic 原子类的 4 连击
4.1 介绍一下Atomic 原子类
Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割
的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不
会被其他线程干扰。
所以,所谓原子类说简单点就是具有原子/原子操作特征的类。
并发包 java.util.concurrent 的原子类都存放在 java.util.concurrent.atomic 下,如下图所示。
4.2 JUC 包中的原子类是哪4类?
基本类型
使用原子的方式更新基本类型
AtomicInteger:整形原子类
AtomicLong:长整型原子类
AtomicBoolean :布尔型原子类
数组类型
使用原子的方式更新数组里的某个元素
AtomicIntegerArray:整形数组原子类
AtomicLongArray:长整形数组原子类
AtomicReferenceArray :引用类型数组原子类
引用类型
AtomicReference:引用类型原子类
AtomicStampedRerence:原子更新引用类型里的字段原子类
AtomicMarkableReference :原子更新带有标记位的引用类型
对象的属性修改类型
AtomicIntegerFieldUpdater:原子更新整形字段的更新器
AtomicLongFieldUpdater:原子更新长整形字段的更新器
AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原
子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
4.3 讲讲 AtomicInteger 的使用
AtomicInteger 类常用方法
AtomicInteger 类的使用示例
使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。
4.4 能不能给我简单介绍一下 AtomicInteger 类的原理
AtomicInteger 线程安全原理简单分析
AtomicInteger 类的部分源码:
public final int get() //获取当前的值 public final int getAndSet(int newValue)//获取当前的值,并设置新的值 public final int getAndIncrement()//获取当前的值,并自增 public final int getAndDecrement() //获取当前的值,并自减 public final int getAndAdd(int delta) //获取当前的值,并加上预期的值 boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输 入值(update) public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程 在之后的一小段时间内还是可以读到旧的值。 class AtomicIntegerTest { private AtomicInteger count = new AtomicInteger(); //使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全。 public void increment() { count.incrementAndGet(); }public int getCount() { return count.get(); } }
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免
synchronized 的高开销,执行效率大为提升。
CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法
是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变
量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
关于 Atomic 原子类这部分更多内容可以查看我的这篇文章:并发编程面试必备:JUC 中的 Atomic 原子类总结
五 AQS
5.1 AQS 介绍
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的
ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是
基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
5.2 AQS 原理分析
// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用) private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try {valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField(“value”)); } catch (Exception ex) { throw new Error(ex); } }private volatile int value;
AQS 原理这部分参考了部分博客,在5.2节末尾放了链接。
在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于AQS原理的理解”。下面给大家一个示
例供大家参加,面试不是背题,大家一定要假如自己的思想,即使加入不了自己的思想也要保证自己能够通俗
的讲出来而不是背出来。
下面大部分内容其实在AQS类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。
5.2.1 AQS 原理概览
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设
置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制
AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结
点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁
的分配。
看个AQS(AbstractQueuedSynchronizer)原理图:
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该
同步状态进行原子操作实现对其值的修改。
状态信息通过procted类型的getState,setState,compareAndSetState进行操作
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
5.2.2 AQS 对资源的共享方式
AQS定义两种资源共享方式
Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、
CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某
一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方
式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
5.2.3 AQS底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应
用):
- 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源
state的获取和释放) - 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:
默认情况下,每个方法都抛出 UnsupportedOperationException 。 这些方法的实现必须是内部线程安全的,并且
通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类
使用。
//返回同步状态的当前值 protected final int getState() { return state; }// 设置同步状态的值 protected final void setState(int newState) { state = newState; }//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成 功,且有剩余资源。 tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将
state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才
有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但
要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个
子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子
线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现 tryAcquire-tryRelease 、 tryAcquireShared-tryReleaseShared 中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,
如 ReentrantReadWriteLock 。
推荐两篇 AQS 原理和相关源码分析的文章:
http://www.cnblogs.com/waterystone/p/4920797.html
https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html
5.3 AQS 组件总结
Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问
某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
CountDownLatch (倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这
个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,
但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier
的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫
同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用
await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
关于AQS这部分的更多内容可以查看我的这篇文章:并发编程面试必备:AQS 原理以及 AQS 同步组件总结
2.4 Java虚拟机
关于Java虚拟机,在面试的时候一般会问的大多就是①Java内存区域、②虚拟机垃圾算法、③虚拟机垃圾收集
器、④JVM内存管理、⑤JVM调优、⑥Java类加载机制这些问题了。推荐书籍《深入理解Java虚拟机:JVM高级特性
与最佳实践(第二版》、《实战Java虚拟机》。
Java内存区域
面试常见问题:
介绍下 Java 内存区域(运行时数据区)
Java 对象的创建过程(五步,建议能默写出来并且要知道每一步虚拟机做了什么)
对象的访问定位的两种方式(句柄和直接指针两种方式)
拓展问题:
String类和常量池
8种基本类型的包装类和常量池
JVM垃圾回收就是这么简单
面试常见问题:
如何判断对象是否死亡(两种方法)。
简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好
处)。
如何判断一个常量是废弃常量
如何判断一个类是无用的类
垃圾收集有哪些算法,各自的特点?
HotSpot为什么要分为新生代和老年代?
常见的垃圾回收器有那些?
介绍一下CMS,G1收集器。
Minor Gc和Full GC 有什么不同呢?
2.5 设计模式
设计模式比较常见的就是让你手写一个单例模式(注意单例模式的几种不同的实现方法)或者让你说一下某个常见的
设计模式在你的项目中是如何使用的,另外面试官还有可能问你抽象工厂和工厂方法模式的区别、工厂模式的思想这
样的问题。
建议把代理模式、观察者模式、(抽象)工厂模式好好看一下,这三个设计模式也很重要。
三 计算机网络常见面试点总结
3.1 TCP、UDP 协议的区别
UDP 在传送数据之前不需要先建立连接,远地主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可
靠交付,但在某些情况下 UDP 确是一种最有效的工作方式(一般用于即时通信),比如: QQ 语音、 QQ 视频 、直
播等等
TCP 提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。 TCP 不提供广播或多播
服务。由于 TCP 要提供可靠的,面向连接的运输服务(TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立
连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资
源),这一难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增
大很多,还要占用许多处理机资源。TCP 一般用于文件传输、发送和接收邮件、远程登录等场景。
3.2 在浏览器中输入url地址 ->> 显示主页的过程
百度好像最喜欢问这个问题。
打开一个网页,整个过程会使用哪些协议
图片来源:《图解HTTP》
总体来说分为以下几个过程: 1. DNS解析 - TCP连接
- 发送HTTP请求
- 服务器处理请求并返回HTTP报文
- 浏览器解析渲染页面
- 连接结束
具体可以参考下面这篇文章:
https://segmentfault.com/a/1190000006879700
3.3 各种协议与HTTP协议之间的关系
一般面试官会通过这样的问题来考察你对计算机网络知识体系的理解。
图片来源:《图解HTTP》
3.4 HTTP长连接、短连接
在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中
断连接。当客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源(如JavaScript文件、图像
文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话。
而从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:
在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客
户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时
间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。
HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。
—— 《HTTP长连接、短连接究竟是什么?》
3.5 TCP 三次握手和四次挥手(面试常客)
为了准确无误地把数据送达目标处,TCP协议采用了三次握手策略。
漫画图解:
图片来源:《图解HTTP》 Connection:keep-alive
简单示意图:
客户端–发送带有 SYN 标志的数据包–一次握手–服务端
服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端
客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端
为什么要三次握手?
三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是
双方确认自己与对方的发送与接收是正常的。
第一次握手:Client 什么都不能确认;Server 确认了对方发送正常
第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己接收正常,对方发
送正常
第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,
对方发送接收正常
所以三次握手就能确认双发收发功能都正常,缺一不可。
为什么要传回 SYN
接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。
SYN 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先
发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以
ACK(Acknowledgement[汉译:确认字符 ,在数据通信传输中,接收站发给发送站的一种传输控制字符。它表
示确认发来的数据已经接受无误。 ])消息响应。这样在客户机和服务器之间才能建立起可靠的TCP连接,数据
才可以在客户机和服务器之间传递。
传了 SYN,为啥还要传 ACK
双方通信无误必须是两者互相发送信息都无误。传了 SYN,证明发送方到接收方的通道没有问题,但是接收方到发送
方的通道还需要 ACK 信号来进行验证。
断开一个 TCP 连接则需要“四次挥手”:
客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送
服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加1 。和 SYN 一样,一个 FIN 将占用一个序号
服务器-关闭与客户端的连接,发送一个FIN给客户端
客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加1
为什么要四次挥手
任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送
的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。
举个例子:A 和 B 打电话,通话即将结束后,A 说“我没啥要说的了”,B回答“我知道了”,但是 B 可能还会有要说的
话,A 不能要求 B 跟着自己的节奏结束通话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道
了”,这样通话才算结束。
上面讲的比较概括,推荐一篇讲的比较细致的文章:
https://blog.csdn.net/qzcsu/article/details/72861891
四 Linux
4.1 简单介绍一下 Linux 文件系统?
Linux文件系统简介
在Linux操作系统中,所有被操作系统管理的资源,例如网络接口卡、磁盘驱动器、打印机、输入输出设备、普通文
件或是目录都被看作是一个文件。
也就是说在LINUX系统中有一个重要的概念:一切都是文件。其实这是UNIX哲学的一个体现,而Linux是重写UNIX而
来,所以这个概念也就传承了下来。在UNIX系统中,把一切资源都看作是文件,包括硬件设备。UNIX系统把每个硬
件都看成是一个文件,通常称为设备文件,这样用户就可以用读写文件的方式实现对硬件的访问。
文件类型与目录结构
Linux支持5种文件类型 :
Linux的目录结构如下:
Linux文件系统的结构层次鲜明,就像一棵倒立的树,最顶层是其根目录:
常见目录说明:
/bin: 存放二进制可执行文件(ls,cat,mkdir等),常用命令一般都在这里;
/etc: 存放系统管理和配置文件;
/home: 存放所有用户文件的根目录,是用户主目录的基点,比如用户user的主目录就是/home/user,可以
用~user表示;
/usr : 用于存放系统应用程序;
/opt: 额外安装的可选应用程序包所放置的位置。一般情况下,我们可以把tomcat等都安装到这里;
/proc: 虚拟文件系统目录,是系统内存的映射。可直接访问这个目录来获取系统信息;
/root: 超级用户(系统管理员)的主目录(特权阶级o);
/sbin: 存放二进制可执行文件,只有root才能访问。这里存放的是系统管理员使用的系统级别的管理命令和程
序。如ifconfig等;
/dev: 用于存放设备文件;
/mnt: 系统管理员安装临时文件系统的安装点,系统提供这个目录是让用户临时挂载其他的文件系统;
/boot: 存放用于系统引导时使用的各种文件;
/lib : 存放着和系统运行相关的库文件 ;
/tmp: 用于存放各种临时文件,是公用的临时文件存储点;
/var: 用于存放运行时需要改变数据的文件,也是某些大文件的溢出区,比方说各种服务的日志文件(系统启
动日志等。)等;
/lost+found: 这个目录平时是空的,系统非正常关机而留下“无家可归”的文件(windows下叫什么.chk)就在
这里。
4.2 一些常见的 Linux 命令了解吗?
目录切换命令
cd usr : 切换到该目录下usr目录
cd …(或cd…/) : 切换到上一层目录
cd / : 切换到系统根目录
cd ~ : 切换到用户主目录
cd - : 切换到上一个所在目录
目录的操作命令(增删改查)
- mkdir 目录名称 : 增加目录
- ls或者ll (ll是ls -l的缩写,ll命令以看到该目录下的所有目录和文件的详细信息):查看目录信息
- find 目录 参数 : 寻找目录(查)
- mv 目录名称 新目录名称 : 修改目录的名称(改)
注意:mv的语法不仅可以对目录进行重命名而且也可以对各种文件,压缩包等进行 重命名的操作。mv命令用
来对文件或目录重新命名,或者将文件从一个目录移到另一个目录中。后面会介绍到mv命令的另一个用法。 - mv 目录名称 目录的新位置 : 移动目录的位置—剪切(改)
注意:mv语法不仅可以对目录进行剪切操作,对文件和压缩包等都可执行剪切操作。另外mv与cp的结果不
同,mv好像文件“搬家”,文件个数并未增加。而cp对文件进行复制,文件个数增加了。 - cp -r 目录名称 目录拷贝的目标位置 : 拷贝目录(改),-r代表递归拷贝
注意:cp命令不仅可以拷贝目录还可以拷贝文件,压缩包等,拷贝文件和压缩包时不 用写-r递归 - rm [-rf] 目录 : 删除目录(删)
注意:rm不仅可以删除目录,也可以删除其他文件或压缩包,为了增强大家的记忆, 无论删除任何目录或文
件,都直接使用 rm -rf 目录/文件/压缩包
文件的操作命令(增删改查) - touch 文件名称 : 文件的创建(增)
- cat/more/less/tail 文件名称 文件的查看(查)
cat : 只能显示最后一屏内容
more : 可以显示百分比,回车可以向下一行, 空格可以向下一页,q可以退出查看
less : 可以使用键盘上的PgUp和PgDn向上 和向下翻页,q结束查看
tail-10 : 查看文件的后10行,Ctrl+C结束
注意:命令 tail -f 文件 可以对某个文件进行动态监控,例如tomcat的日志文件, 会随着程序的运行,日志会变
化,可以使用tail -f catalina-2016-11-11.log 监控 文 件的变化 - vim 文件 : 修改文件的内容(改)
vim编辑器是Linux中的强大组件,是vi编辑器的加强版,vim编辑器的命令和快捷方式有很多,但此处不一一阐
述,大家也无需研究的很透彻,使用vim编辑修改文件的方式基本会使用就可以了。
在实际开发中,使用vim编辑器主要作用就是修改配置文件,下面是一般步骤:
vim 文件------>进入文件----->命令模式------>按i进入编辑模式----->编辑文件 ------->按Esc进入底行模式----->输 入:wq/q! (输入wq代表写入内容并退出,即保存;输入q!代表强制退出不保存。) - rm -rf 文件 : 删除文件(删)
同目录删除:熟记 rm -rf 文件 即可
压缩文件的操作命令
1)打包并压缩文件:
Linux中的打包文件一般是以.tar结尾的,压缩的命令一般是以.gz结尾的。
而一般情况下打包和压缩是一起进行的,打包并压缩后的文件的后缀名一般.tar.gz。 命令: tar -zcvf 打包压缩后的 文件名 要打包压缩的文件 其中:
z:调用gzip压缩命令进行压缩
c:打包文件
v:显示运行过程
f:指定文件名
比如:加入test目录下有三个文件分别是 :aaa.txt bbb.txt ccc.txt,如果我们要打包test目录并指定压缩后的压缩包名
称为test.tar.gz可以使用命令: tar -zcvf test.tar.gz aaa.txt bbb.txt ccc.txt 或: tar -zcvf test.tar.gz /test/ 2)解压压缩包:
命令:tar [-xvf] 压缩文件
其中:x:代表解压
示例:
1 将/test下的test.tar.gz解压到当前目录下可以使用命令: tar -xvf test.tar.gz
2 将/test下的test.tar.gz解压到根目录/usr下: tar -xvf xxx.tar.gz -C /usr (- C代表指定解压的位置)
其他常用命令
pwd : 显示当前所在位置
grep 要搜索的字符串 要搜索的文件 --color : 搜索命令,–color代表高亮显示
ps -ef / ps aux : 这两个命令都是查看当前系统正在运行进程,两者的区别是展示格式不同。如果想要查看
特定的进程可以使用这样的格式: ps aux|grep redis (查看包括redis字符串的进程)
注意:如果直接用ps((Process Status))命令,会显示所有进程的状态,通常结合grep命令查看某进程的
状态。
kill -9 进程的pid : 杀死进程(-9 表示强制终止。)
先用ps查找进程,然后用kill杀掉
网络通信命令:
查看当前系统的网卡信息:ifconfig
查看与某台机器的连接情况:ping
查看当前系统的端口使用:netstat -an
shutdown : shutdown -h now : 指定现在立即关机; shutdown +5 “System will shutdown after 5 minutes” :指定5分钟后关机,同时送出警告信息给登入用户。
reboot : reboot : 重开机。 reboot -w : 做个重开机的模拟(只有纪录并不会真的重开机)。
五 MySQL
5.1 说说自己对于 MySQL 常见的两种存储引擎:MyISAM与
InnoDB的理解
关于二者的对比与总结: 1. count运算上的区别:因为MyISAM缓存有表meta-data(行数等),因此在做COUNT(*)时对于一个结构很好
的查询是不需要消耗多少资源的。而对于InnoDB来说,则没有这种缓存。 - 是否支持事务和崩溃后的安全恢复: MyISAM 强调的是性能,每次查询具有原子性,其执行数度比InnoDB类型
更快,但是不提供事务支持。但是InnoDB 提供事务支持事务,外部键等高级数据库功能。 具有事务
(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID
compliant))型表。 - 是否支持外键: MyISAM不支持,而InnoDB支持。
MyISAM更适合读密集的表,而InnoDB更适合写密集的的表。 在数据库做主从分离的情况下,经常选择MyISAM作
为主库的存储引擎。 一般来说,如果需要事务支持,并且有较高的并发读取频率(MyISAM的表锁的粒度太大,所以
当该表写并发量较高时,要等待的查询就会很多了),InnoDB是不错的选择。如果你的数据量很大(MyISAM支持压
缩特性可以减少磁盘的空间占用),而且不需要支持事务时,MyISAM是最好的选择。
5.2 数据库索引了解吗?
系列思维导图源文件(数据库+架构)以及思维导图制作软件—XMind8 破解安装,公众号(JavaGuide)后台
回复:“思维导图” 免费领取!(下面的图片不是很清楚,原图非常清晰,另外提供给大家源文件也是为了大家
根据自己需要进行修改)
下面是我补充的一些内容
5.2.1 为什么索引能提高查询速度?
先从 MySQL 的基本存储结构说起
MySQL的基本存储结构是页(记录都存在页里边):
各个数据页可以组成一个双向链表
每个数据页中的记录又可以组成一个单向链表
每个数据页都会为存储在它里边儿的记录生成一个页目录,在通过主键查找某条记录的时候可以在页目录
中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录
以其他列(非主键)作为搜索条件:只能从最小记录开始依次遍历单链表中的每条记录。
所以说,如果我们写select * from user where indexname = 'xxx’这样没有进行任何优化的sql语句,默认会这样
做: - 定位到记录所在的页:需要遍历双向链表,找到所在的页
- 从所在的页内中查找相应的记录:由于不是根据主键查询,只能遍历所在页的单链表了
很明显,在数据量很大的情况下这样查找会很慢!这样的时间复杂度为O(n)。
使用索引之后
索引做了些什么可以让我们查询加快速度呢?其实就是将无序的数据变成有序(相对):
要找到id为8的记录简要步骤:
很明显的是:没有用索引我们是需要遍历双向链表来定位对应的页,现在通过 “目录” 就可以很快地定位到对应的页
上了!(二分查找,时间复杂度近似为O(logn))
其实底层结构就是B+树,B+树作为树的一种实现,能够让我们很快地查找出对应的记录。
以下内容整理自:《Java工程师修炼之道》
5.2.2 最左前缀原则
MySQL中的索引可以以一定顺序引用多列,这种索引叫作联合索引。如User表的name和city加联合索引就是
(name,city)o而最左前缀原则指的是,如果查询的时候查询条件精确匹配索引的左边连续一列或几列,则此列就可以
被用到。如下:
这里需要注意的是,查询的时候如果两个条件都用上了,但是顺序不同,如 city= xx and name =xx ,那么现在
的查询引擎会自动优化为匹配联合索引的顺序,这样是能够命中索引的.
由于最左前缀原则,在创建联合索引时,索引字段的顺序需要考虑字段值去重之后的个数,较多的放前面。
ORDERBY子句也遵循此规则。
注意避免冗余索引
冗余索引指的是索引的功能相同,能够命中 就肯定能命中 ,那么 就是冗余索引如(name,city )和(name )这两
个索引就是冗余索引,能够命中后者的查询肯定是能够命中前者的 在大多数情况下,都应该尽量扩展已有的索引而
不是创建新索引。
MySQLS.7 版本后,可以通过查询 sys 库的 schemal_r dundant_indexes 表来查看冗余索引
5.2.3 Mysql如何为表字段添加索引???
1.添加PRIMARY KEY(主键索引)
2.添加UNIQUE(唯一索引)
3.添加INDEX(普通索引)
4.添加FULLTEXT(全文索引)
5.添加多列索引
select * from user where name=xx and city=xx ; //可以命中索引 select * from user where name=xx ; // 可以命中索引 select * from user where city=xx; // 无法命中索引 ALTER TABLEtable_name
ADD PRIMARY KEY (column
) ALTER TABLEtable_name
ADD UNIQUE (column
) ALTER TABLEtable_name
ADD INDEX index_name (column
) ALTER TABLEtable_name
ADD FULLTEXT (column
) ALTER TABLEtable_name
ADD INDEX index_name (column1
,column2
,column3
)
5.3 当MySQL单表记录数过大时,数据库的CRUD性能会明显下
降,一些常见的优化措施如下:
当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下: - 限定数据的范围: 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时
候,我们可以控制在一个月的范围内。; - 读/写分离: 经典的数据库拆分方案,主库负责写,从库负责读;
- 垂直分区: 根据数据库里面数据表的相关性进行拆分。 例如,用户表中既有用户的登录信息又有用户的基本信
息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。简单来说垂直拆分是指数据表列的拆分,
把一张列比较多的表拆分为多张表。 如下图所示,这样来说大家应该就更容易理解了。
垂直拆分的优点: 可以使得行数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简
化表的结构,易于维护。垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过
在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂; - 水平分区: 保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达
到了分布式的目的。 水平拆分可以支撑非常大的数据量。 水平拆分是指数据表行的拆分,表的行数超过200万
行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多
个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。
水平拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的
数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 水平拆分最好分库 。水平拆分能
够 支持非常大的数据量存储,应用端改造也少,但 分片事务难以解决 ,跨界点Join性能较差,逻辑复杂。
《Java工程师修炼之道》的作者推荐 尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂
度 ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择
客户端分片架构,这样可以减少一次和中间件的网络I/O。
下面补充一下数据库分片的两种常见方案:
客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的 ShardingJDBC 、阿里的TDDL是两种比较常用的实现。
中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat
、360的Atlas、网易的DDB等等都是这种架构的实现。
5.4 事务隔离级别(图文详解)
什么是事务?
事务是逻辑上的一组操作,要么都执行,要么都不执行。
事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就
是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩
溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要
失败。
事物的特性(ACID) - 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
- 一致性: 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
- 隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
- 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影
响。
并发事务带来的问题
在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对统一数据进行操
作)。并发虽然是必须的,但可能会导致以下的问题。
脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这
时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个
事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事
务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢
隔离级别 脏读 不可重复读 幻影读
READ-UNCOMMITTED √ √ √
READ-COMMITTED × √ √
REPEATABLE-READ × × √
SERIALIZABLE × × ×
失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最
终结果A=19,事务1的修改被丢失。
不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务
也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的
数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发
事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就
好像发生了幻觉一样,所以称为幻读。
不可重复度和幻读区别:
不可重复读的重点是修改,幻读的重点在于新增或者删除。
例1(同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 ):事务1中的A先生读取自己的工资为 1000的操
作还没完成,事务2中的B先生就修改了A的工资为2000,导 致A再读自己的工资时工资变为 2000;这就是不可重复
读。
例2(同样的条件, 第1次和第2次读出来的记录数不一样 ):假某工资单表中工资大于3000的有4人,事务1读取了所
有工资大于3000的人,共查到4条记录,这时事务2 又插入了一条工资大于3000的记录,事务1再次读取时查到的记
录就变为了5条,这样就导致了幻读。
事务隔离级别
SQL 标准定义了四个隔离级别:
READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻
读或不可重复读
READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读
仍有可能发生
REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修
改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务
之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。我们可以通过 SELECT @@tx_isolation; 命令来查看
这里需要注意的是:与 SQL 标准不同的地方在于InnoDB 存储引擎在 REPEATABLE-READ(可重读)事务隔离级别
下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以
说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要
求,即达到了 SQL标准的SERIALIZABLE(可串行化)隔离级别。
因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内
容):,但是你要知道的是InnoDB 存储引擎默认使用 REPEATABLE-READ(可重读)并不会有任何性能损失。
InnoDB 存储引擎在 分布式事务 的情况下一般会用到SERIALIZABLE(可串行化)隔离级别。
实际情况演示
在下面我会使用 2 个命令行mysql ,模拟多线程(多事务)对同一份数据的脏读问题。
MySQL 命令行的默认配置中事务都是自动提交的,即执行SQL语句后就会马上执行 COMMIT 操作。如果要显式地开
启一个事务需要使用命令: START TARNSACTION 。
我们可以通过下面的命令来设置隔离级别。
我们再来看一下我们在下面实际操作中使用到的一些并发控制语句: START TARNSACTION | BEGIN :显式地开启一个事务。
COMMIT :提交事务,使得对数据库做的所有修改成为永久性。
ROLLBACK 回滚会结束用户的事务,并撤销正在进行的所有未提交的修改。
脏读(读未提交) mysql> SELECT @@tx_isolation; ±----------------+ | @@tx_isolation | ±----------------+ | REPEATABLE-READ | ±----------------+ SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE]
避免脏读(读已提交)
不可重复读
还是刚才上面的读已提交的图,虽然避免了读未提交,但是却出现了,一个事务还没有结束,就发生了 不可重复读
问题。
可重复读
防止幻读(可重复读)
一个事务对数据库进行操作,这种操作的范围是数据库的全部行,然后第二个事务也在对这个数据库操作,这种操作
可以是插入一行记录或删除一行记录,那么第一个是事务就会觉得自己出现了幻觉,怎么还有没有处理的记录呢? 或 者 怎么多处理了一行记录呢?
幻读和不可重复读有些相似之处 ,但是不可重复读的重点是修改,幻读的重点在于新增或者删除。
参考
《MySQL技术内幕:InnoDB存储引擎》
https://dev.mysql.com/doc/refman/5.7/en/
Mysql 锁:灵魂七拷问
Innodb 中的事务隔离级别和锁的关系
六 Redis
6.1 redis 简介
简单来说 redis 就是一个数据库,不过与传统数据库不同的是 redis 的数据是存在内存中的,所以存写速度非常快,
因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业
务场景。除此之外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。
6.2 为什么要用 redis /为什么要用缓存
主要从“高性能”和“高并发”这两点来看待这个问题。
高性能:
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数
缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当
快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
高并发:
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中
去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
6.3 为什么要用 redis 而不用 map/guava 做缓存?
下面的内容来自 segmentfault 一位网友的提问,地址:https://segmentfault.com/q/1010000009106416
缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是
轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓
存不具有一致性。
使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致
性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。
6.4 redis 和 memcached 的区别
对于 redis 和 memcached 我总结了下面四点。现在公司一般都是用 redis 来实现缓存,而且 redis 自身也越来越强
大了! - redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供
list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。 2. Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而
Memecache把数据全部存在内存之中。 - 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前
是原生支持 cluster 模式的. 4. Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。
来自网络上的一张图,这里分享给大家!
6.5 redis 常见数据结构以及使用场景分析 - String
常用命令: set,get,decr,incr,mget 等。
String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 常规key-value缓存应用;
常规计数:微博数,粉丝数等。
2.Hash
常用命令: hget,hset,hgetall 等。
Hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅
仅修改这个对象中的某个字段的值。 比如我们可以Hash数据结构来存储用户信息,商品信息等等。比如下面我就用
hash 类型存放了我本人的一些信息:
3.List
常用命令: lpush,rpush,lpop,rpop,lrange等 key=JavaUser293847 value={ “id”: 1, “name”: “SnailClimb”, “age”: 22, “location”: “Wuhan, Hubei” }
list 就是链表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表,
消息列表等功能都可以用Redis的 list 结构来实现。
Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功
能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。
4.Set
常用命令: sadd,spop,smembers,sunion 等
set 对外提供的功能与list类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。
当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在
一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。
比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常
方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,具体命令如下:
5.Sorted Set
常用命令: zadd,zrange,zrem,zcard等 和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维
度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。
6.6 redis 设置过期时间
Redis中有个设置时间过期的功能,即对存储在 redis 数据库中的值可以设置一个过期时间。作为一个缓存数据库,
这是非常实用的。如我们一般项目中的 token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统
的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。
我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的
时间。
如果假设你设置了一批 key 只能存活1个小时,那么接下来1小时后,redis是怎么对这批key进行删除的?
定期删除+惰性删除。
通过名字大概就能猜出这两个删除方式的意思了。
定期删除:redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删
除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所
有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!
惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期
key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这
就是所谓的惰性删除,也是够懒的哈!
sinterstore key1 key2 key3 将交集存在key1内
但是仅仅通过设置过期时间还是有问题的。我们想一下:如果定期删除漏掉了很多过期 key,然后你也没及时去查,
也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了。怎么解决这个问题
呢?
redis 内存淘汰机制。
6.7 redis 内存淘汰机制(MySQL里有2000w数据,Redis中只存
20w的数据,如何保证Redis中的数据都是热点数据?)
redis 配置文件 redis.conf 中有相关注释,我这里就不贴了,大家可以自行查阅或者通过这个网址查看:
http://download.redis.io/redis-stable/redis.conf
redis 提供 6种数据淘汰策略: - volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的). 5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使
用吧!
备注: 关于 redis 设置过期时间以及内存淘汰机制,我这里只是简单的总结一下,后面会专门写一篇文章来总结!
6.8 redis 持久化机制(怎么保证 redis 挂掉之后再重启数据可以
进行恢复)
很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机
器、机器故障之后回复数据),或者是为了防止系统故障而将数据备份到一个远程位置。
Redis不同于Memcached的很重一点就是,Redis支持持久化,而且支持两种不同的持久化操作。Redis的一种持久
化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file,AOF).这两种方法各有千
秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合自己的持久化方法。
快照(snapshotting)持久化(RDB)
Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行
备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性
能),还可以将快照留在原地以便重启服务器的时候使用。
快照持久化是Redis默认采用的持久化方式,在redis.conf配置文件中默认有此下配置:
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令 创建快照。 save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创 建快照。 save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创 建快照。
AOF(append-only file)持久化
与快照持久化相比,AOF持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下Redis没有开启
AOF(append only file)方式的持久化,可以通过appendonly参数开启:
开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件。AOF文件的
保存位置和RDB文件的位置相同,都是通过dir参数设置的,默认的文件名是appendonly.aof。 在Redis的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec选项 ,让Redis每秒同步一次AOF文件,Redis性能
几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操
作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
Redis 4.0 对于持久化机制的优化
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。
如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB
和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是
AOF 格式,可读性较差。
补充内容:AOF 重写
AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小。
AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任伺读
入、分析或者写入操作。
在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新AOF文件期
间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容
追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的
AOF文件,以此来完成AOF文件重写操作
更多内容可以查看我的这篇文章:
https://github.com/Snailclimb/JavaGuide/blob/master/数据存储/Redis/Redis持久化.md
6.9 redis 事务
Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然
后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令
请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
appendonly yes appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度 appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘 appendfsync no #让操作系统决定何时进行同步
在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的可靠性和安全性。在 Redis 中,事务总是具有原子性
(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下时,事务
也具有持久性(Durability)。
6.10 缓存雪崩和缓存穿透问题解决方案
缓存雪崩
简介:缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩
掉。
解决办法(中华石杉老师在他的视频中提到过,视频地址在最后一个问题中有提到):
事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
事后:利用 redis 持久化机制保存的数据尽快恢复缓存
缓存穿透
简介:一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量
请求而崩掉。
解决办法: 有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈
希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压
力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存
在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
参考:
https://blog.csdn.net/zeb_perfect/article/details/54135506enter link description here
6.11 如何解决 Redis 的并发竞争 Key 问题
所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺
序不同,这样也就导致了结果的不同!
推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问
题,不要使用分布式锁,这样会影响性能)
基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的
与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有
序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁
无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
在实践中,当然是从以可靠性为主。所以首推Zookeeper。
参考:
https://www.jianshu.com/p/8bddd381de06
6.12 如何保证缓存与数据库双写时的数据一致性?
你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如
何解决一致性问题?
一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的
情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致
的情况
串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
参考:
Java工程师面试突击第1季(可能是史上最好的Java面试突击课程)-中华石杉老师。视频地址见下面!
链接: https://pan.baidu.com/s/18pp6g1xKVGCfUATf_nMrOA
密码:5i58
参考
redis设计与实现(第二版) 七 Spring
Spring一般是不可避免的,如果你的简历上注明了你会Spring Boot或者Spring Cloud的话,那么面试官也可
能会同时问你这两个技术,比如他可能会问你springboot和spring的区别。 所以,一定要谨慎对待写在简历上的东
西,一定要对简历上的东西非常熟悉。
另外,AOP实现原理、动态代理和静态代理、Spring IOC的初始化过程、IOC原理、自己怎么实现一个IOC容
器? 这些东西都是经常会被问到的。
7.1 Spring Bean 的作用域
7.2 Spring 事务中的隔离级别
TransactionDefinition 接口中定义了五个表示隔离级别的常量:
TransactionDefinition.ISOLATION_DEFAULT: 使用后端数据库默认的隔离级别,Mysql 默认采用的
REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别. TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 最低的隔离级别,允许读取尚未提交的数据变
更,可能会导致脏读、幻读或不可重复读
TransactionDefinition.ISOLATION_READ_COMMITTED: 允许读取并发事务已经提交的数据,可以阻止脏
读,但是幻读或不可重复读仍有可能发生
TransactionDefinition.ISOLATION_REPEATABLE_READ: 对同一字段的多次读取结果都是一致的,除非数据
是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
TransactionDefinition.ISOLATION_SERIALIZABLE: 最高的隔离级别,完全服从ACID的隔离级别。所有的事
务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻
读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
7.3 Spring 事务中的事务传播行为
支持当前事务的情况:
TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事
务,则创建一个新的事务。
TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事
务,则以非事务的方式继续运行。
TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有
事务,则抛出异常。(mandatory:强制性)
不支持当前事务的情况:
TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当
前事务挂起。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把
当前事务挂起。
TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。
其他情况:
TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套
事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
7.4 AOP
AOP思想的实现一般都是基于 代理模式 ,在JAVA中一般采用JDK动态代理模式,但是我们都知道,JDK动态代理模式
只能代理接口而不能代理类。因此,Spring AOP 会这样子来进行切换,因为Spring AOP 同时支持 CGLIB、
ASPECTJ、JDK动态代理。
如果目标对象的实现类实现了接口,Spring AOP 将会采用 JDK 动态代理来生成 AOP 代理类;
如果目标对象的实现类没有实现接口,Spring AOP 将会采用 CGLIB 来生成 AOP 代理类——不过这个选择过程
对开发者完全透明、开发者也无需关心。
这部分内容可以查看下面这几篇文章:
https://www.jianshu.com/p/fe8d1e8bd63e
http://www.cnblogs.com/puyangsky/p/6218925.html
https://juejin.im/post/5a55af9e518825734d14813f
7.5 IOC
Spring IOC的初始化过程:
IOC源码阅读
https://javadoop.com/post/spring-ioc
八 消息队列
“RabbitMQ?”“Kafka?”“RocketMQ?”…在日常学习与开发过程中,我们常常听到消息队列这个关键词。我也在
我的多篇文章中提到了这个概念。可能你是熟练使用消息队列的老手,又或者你是不懂消息队列的新手,不论你了不
了解消息队列,本文都将带你搞懂消息队列的一些基本理论。如果你是老手,你可能从本文学到你之前不曾注意的一
些关于消息队列的重要概念,如果你是新手,相信本文将是你打开消息队列大门的一板砖。
8.1 什么是消息队列
我们可以把消息队列比作是一个存放消息的容器,当我们需要使用消息的时候可以取出消息供自己使用。消息队
列是分布式系统中重要的组件,使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。目前
使用较多的消息队列有ActiveMQ,RabbitMQ,Kafka,RocketMQ,我们后面会一一对比这些消息队列。
另外,我们知道队列 Queue 是一种先进先出的数据结构,所以消费消息时也是按照顺序来消费的。比如生产者
发送消息1,2,3…对于消费者就会按照1,2,3…的顺序来消费。但是偶尔也会出现消息被消费的顺序不对的情况,比如某
个消息消费失败又或者一个 queue 多个consumer 也会导致消息被消费的顺序不对,我们一定要保证消息被消费的
顺序正确。
除了上面说的消息消费顺序的问题,使用消息队列,我们还要考虑如何保证消息不被重复消费?如何保证消息的
可靠性传输(如何处理消息丢失的问题)?…等等问题。所以说使用消息队列也不是十全十美的,使用它也会让系
统可用性降低、复杂度提高,另外需要我们保障一致性等问题。
8.2 为什么要用消息队列
我觉得使用消息队列主要有两点好处:1.通过异步处理提高系统性能(削峰、减少响应所需时间);2.降低系统耦
合性。如果在面试的时候你被面试官问到这个问题的话,一般情况是你在你的简历上涉及到消息队列这方面的内容,
这个时候推荐你结合你自己的项目来回答。
《大型网站技术架构》第四章和第七章均有提到消息队列对应用性能及扩展性的提升。
(1) 通过异步处理提高系统性能(削峰、减少响应所需时间)
如上图,在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧
增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即 返回,再由消息队列
的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数
据库有更好的伸缩性),因此响应速度得到大幅改善。
通过以上分析我们可以得出消息队列具有很好的削峰作用的功能——即通过异步处理,将短时间高并发产生的事
务消息存储在消息队列中,从而削平高峰期的并发事务。 举例:在电子商务一些秒杀、促销活动中,合理使用消息
队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示:
因为用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中
可能失败。因此使用消息队列进行异步处理之后,需要适当修改业务流程进行配合,比如用户在提交订单之后,订单
数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚
至出库后,再通过电子邮件或短信通知用户订单成功,以免交易纠纷。这就类似我们平时手机订火车票和电影票。
(2) 降低系统耦合性
我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展
性无疑更好一些。
我们最常见的事件驱动架构类似生产者消费者模式,在大型网站中通常用利用消息队列实现事件驱动结构。如下
图所示:
消息队列使利用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅
消息。 从上图可以看到消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合,消息发送者将消息发送
至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该
消息从何而来。对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现
网站业务的可扩展性设计。
消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接
受者订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。
另外为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,
等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务
器集群中的其他服务器发布消息。
备注: 不要认为消息队列只能利用发布-订阅模式工作,只不过在解耦这个特定业务环境下是使用发布-订阅模式的。
除了发布-订阅模式,还有点对点订阅模式(一个消息只有一个消费者),我们比较常用的是发布-订阅模式。 另外,
这两种消息模型是 JMS 提供的,AMQP 协议还提供了 5 种消息模型。
8.3 使用消息队列带来的一些问题
系统可用性降低: 系统可用性在某种程度上降低,为什么这样说呢?在加入MQ之前,你不用考虑消息丢失或
者说MQ挂掉等等的情况,但是,引入MQ之后你就需要去考虑了!
系统复杂性提高: 加入MQ之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺
序性等等问题!
一致性问题: 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万
一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了!
8.4 JMS VS AMQP
8.4.1 JMS
JMS 简介
JMS(JAVA Message Service,java消息服务)是java的消息服务,JMS的客户端之间可以通过JMS服务进行异步的
消息传输。JMS(JAVA Message Service,Java消息服务)API是一个消息服务的标准或者说是规范,允许应用程序
组件基于JavaEE平台创建、发送、接收和读取消息。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。
ActiveMQ 就是基于 JMS 规范实现的。
JMS两种消息模型
①点到点(P2P)模型
使用队列
(Queue)作为消息通信载体;满足生产者与消费者模式,一条消息只能被一个消费者使用,未被消费的消息在队
列中保留直到被消费或超时。比如:我们生产者发送100条消息的话,两个消费者来消费一般情况下两个消费者会按
照消息发送的顺序各自消费一半(也就是你一个我一个的消费。)
② 发布/订阅(Pub/Sub)模型
发布订阅模型(Pub/Sub) 使用主题
(Topic)作为消息通信载体,类似于广播模式;发布者发布一条消息,该消息通过主题传递给所有的订阅者,在一
条消息广播之后才订阅的用户则是收不到该条消息的。
JMS 五种不同的消息正文格式
JMS定义了五种不同的消息正文格式,以及调用的消息类型,允许你发送并接收以一些不同形式的数据,提供现
有消息格式的一些级别的兼容性。
StreamMessage – Java原始值的数据流
MapMessage–一套名称-值对
TextMessage–一个字符串对象
ObjectMessage–一个序列化的 Java对象
BytesMessage–一个字节的数据流
8.4.2 AMQP
AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准 高级消息队列协议(二
进制应用层协议),是应用层协议的一个开放标准,为面向消息的中间件设计,兼容 JMS。基于此协议的客户端与消
息中间件可传递消息,并不受客户端/中间件同产品,不同的开发语言等条件的限制。
RabbitMQ 就是基于 AMQP 协议实现的。
8.4.3 JMS vs AMQP
对比方向
JMS AMQP
定义
Java API 协议
跨语言 否 是 跨平台 否 是 支持消息类型
提供两种消息模
型:①Peer-2-
Peer;②Pub/sub
提供了五种消息模型:①direct exchange;②fanout exchange;③topic
change;④headers exchange;⑤system exchange。本质来讲,后四种和JMS的
pub/sub模型没有太大差别,仅是在路由机制上做了更详细的划分;
支持消息类型
支持多种消息类
型 ,我们在上面
提到过
byte[](二进制)
总结:
AMQP 为消息定义了线路层(wire-level protocol)的协议,而JMS所定义的是API规范。在 Java 体系中,多个
client均可以通过JMS进行交互,不需要应用修改代码,但是其对跨平台的支持较差。而AMQP天然具有跨平
台、跨语言特性。
JMS 支持TextMessage、MapMessage 等复杂的消息类型;而 AMQP 仅支持 byte[] 消息类型(复杂的类型可
序列化后发送)。
由于Exchange 提供的路由算法,AMQP可以提供多样化的路由方式来传递消息到消息队列,而 JMS 仅支持 队 列 和 主题/订阅 方式两种。
8.5 常见的消息队列对比
对比方向
概要
吞吐量
万级的 ActiveMQ 和 RabbitMQ 的吞吐量(ActiveMQ 的性能最差)要比 十万级甚至是百万级的
RocketMQ 和 Kafka 低一个数量级。
可用性
都可以实现高可用。ActiveMQ 和 RabbitMQ 都是基于主从架构实现高可用性。RocketMQ 基于分布式
架构。 kafka 也是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用
时效性
RabbitMQ 基于erlang开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。其他三个都是
ms 级。
功能支持
除了 Kafka,其他三个功能都较为完备。 Kafka 功能较为简单,主要支持简单的MQ功能,在大数据领域
的实时计算以及日志采集被大规模使用,是事实上的标准
消息丢失
ActiveMQ 和 RabbitMQ 丢失的可能性非常低, RocketMQ 和 Kafka 理论上不会丢失。
总结:
ActiveMQ 的社区算是比较成熟,但是较目前来说,ActiveMQ 的性能比较差,而且版本迭代很慢,不推荐使
用。
RabbitMQ 在吞吐量方面虽然稍逊于 Kafka 和 RocketMQ ,但是由于它基于 erlang 开发,所以并发能力很强,
性能极其好,延时很低,达到微秒级。但是也因为 RabbitMQ 基于 erlang 开发,所以国内很少有公司有实力做
erlang源码级别的研究和定制。如果业务场景对并发量要求不是太高(十万级、百万级),那这四种消息队列
中,RabbitMQ 一定是你的首选。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,
绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。
RocketMQ 阿里出品,Java 系开源项目,源代码我们可以直接阅读,然后可以定制自己公司的MQ,并且
RocketMQ 有阿里巴巴的实际业务场景的实战考验。RocketMQ 社区活跃度相对较为一般,不过也还可以,文
档相对来说简单一些,然后接口这块不是按照标准 JMS 规范走的有些系统要迁移需要修改大量代码。还有就是
阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用
RocketMQ 挺好的
kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用
性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。
kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及
日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。
参考:《Java工程师面试突击第1季-中华石杉老师》
九 Dubbo
本文是作者根据官方文档以及自己平时的使用情况,对 Dubbo 所做的一个总结。如果不懂 Dubbo 的使用的话,可
以参考我的这篇文章《超详细,新手都能看懂 !使用SpringBoot+Dubbo 搭建一个简单的分布式服务》
Dubbo 官网:http://dubbo.apache.org/zh-cn/index.html
Dubbo 中文文档: http://dubbo.apache.org/zh-cn/index.html
一 重要的概念
1.1 什么是 Dubbo?
Apache Dubbo (incubating) |ˈdʌbəʊ| 是一款高性能、轻量级的开源Java RPC 框架,它提供了三大核心能力:面向
接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。简单来说 Dubbo 是一个分布式服务框架,
致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。
Dubbo 目前已经有接近 23k 的 Star ,Dubbo的Github 地址:https://github.com/apache/incubator-dubbo 。 另
外,在开源中国举行的2018年度最受欢迎中国开源软件这个活动的评选中,Dubbo 更是凭借其超高人气仅次于
vue.js 和 ECharts 获得第三名的好成绩。
Dubbo 是由阿里开源,后来加入了 Apache 。正式由于 Dubbo 的出现,才使得越来越多的公司开始使用以及接受分
布式架构。
我们上面说了 Dubbo 实际上是 RPC 框架,那么什么是 RPC呢?
1.2 什么是 RPC?RPC原理是什么?
什么是 RPC?
RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解
底层网络技术的协议。比如两个不同的服务A,B部署在两台不同的机器上,那么服务 A 如果想要调用服务 B 中的某个
方法该怎么办呢?使用 HTTP请求 当然可以,但是可能会比较慢而且一些优化做的并不好。 RPC 的出现就是为了解
决这个问题。
RPC原理是什么?
我这里这是简单的提一下。详细内容可以查看下面这篇文章:
http://www.importnew.com/22003.html - 服务消费方(client)调用以本地调用方式调用服务;
- client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
- client stub找到服务地址,并将消息发送到服务端;
- server stub收到消息后进行解码;
- server stub根据解码结果调用本地的服务;
- 本地服务执行并将结果返回给server stub; 7. server stub将返回结果打包成消息并发送至消费方;
- client stub接收到消息,并进行解码;
- 服务消费方得到最终结果。
下面再贴一个网上的时序图:
说了这么多,我们为什么要用 Dubbo 呢?
1.3 为什么要用 Dubbo?
Dubbo 的诞生和 SOA 分布式架构的流行有着莫大的关系。SOA 面向服务的架构(Service Oriented
Architecture),也就是把工程按照业务逻辑拆分成服务层、表现层两个工程。服务层中包含业务逻辑,只需要对外
提供服务即可。表现层只需要处理和页面的交互,业务逻辑都是调用服务层的服务来实现。SOA架构中有两个主要角
色:服务提供者(Provider)和服务使用者(Consumer)。
如果你要开发分布式程序,你也可以直接基于 HTTP 接口进行通信,但是为什么要用 Dubbo呢?
我觉得主要可以从 Dubbo 提供的下面四点特性来说为什么要用 Dubbo: 1. 负载均衡——同一个服务部署在不同的机器时该调用那一台机器上的服务 - 服务调用链路生成——随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用
要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo 可以为我们解决服务之间互相是如
何调用的。 - 服务访问压力以及时长统计、资源调度和治理——基于访问压力实时管理集群容量,提高集群利用率。
- 服务降级——某个服务挂掉之后调用备用服务
另外,Dubbo 除了能够应用在分布式系统中,也可以应用在现在比较火的微服务系统中。不过,由于 Spring Cloud
在微服务中应用更加广泛,所以,我觉得一般我们提 Dubbo 的话,大部分是分布式系统的情况。
我们刚刚提到了分布式这个概念,下面再给大家介绍一下什么是分布式?为什么要分布式?
1.4 什么是分布式?
分布式或者说 SOA 分布式重要的就是面向服务,说简单的分布式就是我们把整个系统拆分成不同的服务然后将这些
服务放在不同的服务器上减轻单体服务的压力提高并发量和性能。比如电商系统可以简单地拆分成订单系统、商品系
统、登录系统等等,拆分之后的每个服务可以部署在不同的机器上,如果某一个服务的访问量比较大的话也可以将这
个服务同时部署在多台机器上。
1.5 为什么要分布式?
从开发角度来讲单体应用的代码都集中在一起,而分布式系统的代码根据业务被拆分。所以,每个团队可以负责一个
服务的开发,这样提升了开发效率。另外,代码根据业务拆分之后更加便于维护和扩展。
另外,我觉得将系统拆分成分布式之后不光便于系统扩展和维护,更能提高整个系统的性能。你想一想嘛?把整个系
统拆分成不同的服务/系统,然后每个服务/系统 单独部署在一台服务器上,是不是很大程度上提高了系统性能呢?
二 Dubbo 的架构
2.1 Dubbo 的架构图解
上述节点简单说明:
Provider: 暴露服务的服务提供方
Consumer: 调用远程服务的服务消费方
Registry: 服务注册与发现的注册中心
Monitor: 统计服务的调用次数和调用时间的监控中心
Container: 服务运行容器
调用关系说明: - 服务容器负责启动,加载,运行服务提供者。
- 服务提供者在启动时,向注册中心注册自己提供的服务。
- 服务消费者在启动时,向注册中心订阅自己所需的服务。
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一
台调用。 - 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
重要知识点总结:
注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注
册中心不转发请求,压力较小
监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并
以报表展示
注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外
注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者
注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表
注册中心和监控中心都是可选的,服务消费者可以直连服务提供者
服务提供者无状态,任意一台宕掉后,不影响使用
服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
2.2 Dubbo 工作原理
图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复
用,其中,Service 和 Config 层为 API,其它各层均为 SPI。
各层说明:
第一层:service层,接口层,给服务提供者和消费者来实现的
第二层:config层,配置层,主要是对dubbo进行各种配置的
第三层:proxy层,服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton
第四层:registry层,服务注册层,负责服务的注册与发现
第五层:cluster层,集群层,封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务
第六层:monitor层,监控层,对rpc接口的调用次数和调用时间进行监控
第七层:protocol层,远程调用层,封装rpc调用
第八层:exchange层,信息交换层,封装请求响应模式,同步转异步
第九层:transport层,网络传输层,抽象mina和netty为统一接口
第十层:serialize层,数据序列化层。网络传输需要。
三 Dubbo 的负载均衡策略
3.1 先来解释一下什么是负载均衡
先来个官方的解释。
维基百科对负载均衡的定义:负载均衡改善了跨多个计算资源(例如计算机,计算机集群,网络链接,中央处
理单元或磁盘驱动的的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间,并避免
任何单个资源的过载。使用具有负载平衡而不是单个组件的多个组件可以通过冗余提高可靠性和可用性。负载
平衡通常涉及专用软件或硬件
上面讲的大家可能不太好理解,再用通俗的话给大家说一下。
比如我们的系统中的某个服务的访问量特别大,我们将这个服务部署在了多台服务器上,当客户端发起请求的时候,
多台服务器都可以处理这个请求。那么,如何正确选择处理该请求的服务器就很关键。假如,你就要一台服务器来处
理该服务的请求,那该服务部署在多台服务器的意义就不复存在了。负载均衡就是为了避免单个服务器响应同一请
求,容易造成服务器宕机、崩溃等问题,我们从负载均衡的这四个字就能明显感受到它的意义。
3.2 再来看看 Dubbo 提供的负载均衡策略
在集群负载均衡时,Dubbo 提供了多种均衡策略,默认为 random 随机调用。可以自行扩展负载均衡策略,参见:
负载均衡扩展。
备注:下面的图片来自于:尚硅谷2018Dubbo 视频。
3.2.1 Random LoadBalance(默认,基于权重的随机负载均衡机制)
随机,按权重设置随机概率。
在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整
提供者权重。
3.2.2 RoundRobin LoadBalance(不推荐,基于权重的轮询负载均衡机制)
轮循,按公约后的权重设置轮循比率。
存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久
之,所有请求都卡在调到第二台上。
3.2.3 LeastActive LoadBalance
最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
3.2.4 ConsistentHash LoadBalance
一致性 Hash,相同参数的请求总是发到同一提供者。(如果你需要的不是随机负载均衡,是要一类请求都到一
个节点,那就走这个一致性hash策略。)
当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
算法参见:http://en.wikipedia.org/wiki/Consistent_hashing
缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key=“hash.arguments” value=“0,1” />
缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key=“hash.nodes” value=“320” />
3.3 配置方式
xml 配置方式
服务端服务级别
客户端服务级别
<dubbo:service interface="…" loadbalance=“roundrobin” />
服务端方法级别
客户端方法级别
注解配置方式:
消费方基于基于注解的服务级别配置方式:
四 zookeeper宕机与dubbo直连的情况
zookeeper宕机与dubbo直连的情况在面试中可能会被经常问到,所以要引起重视。
在实际生产中,假如zookeeper注册中心宕掉,一段时间内服务消费方还是能够调用提供方的服务的,实际上它使用
的本地缓存进行通讯,这只是dubbo健壮性的一种提现。
dubbo的健壮性表现: - 监控中心宕掉不影响使用,只是丢失部分采样数据
- 数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
- 注册中心对等集群,任意一台宕掉后,将自动切换到另一台
- 注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
- 服务提供者无状态,任意一台宕掉后,不影响使用
- 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
我们前面提到过:注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中
心交互,注册中心不转发请求,压力较小。所以,我们可以完全可以绕过注册中心——采用 dubbo 直连 ,即在服务
消费方配置服务提供方的位置信息。
xml配置方式:
注解方式:
<dubbo:reference interface="…" loadbalance=“roundrobin” /> <dubbo:service interface="…"> <dubbo:method name="…" loadbalance=“roundrobin”/> </dubbo:service> <dubbo:reference interface="…"> <dubbo:method name="…" loadbalance=“roundrobin”/> </dubbo:reference> @Reference(loadbalance = “roundrobin”) HelloService helloService; <dubbo:reference id=“userService” interface=“com.zang.gmall.service.UserService” url=“dubbo://localhost:20880” /> @Reference(url = “127.0.0.1:20880”) HelloService helloService;
十 数据结构
数据结构比较常问的就是:二叉树、红黑树(很可能让你手绘一个红黑树出来哦!)、二叉查找树(BST)、平
衡二叉树(Self-balancing binary search tree)、B-树,B+树与B*树的优缺点比较、 LSM 树这些知识点。
数据结构很重要,而且学起来也相对要难一些。建议学习数据结构一定要循序渐进的来,一步一个脚印的走好。
一定要搞懂原理,最好自己能用代码实现一遍。
Queue
什么是队列
队列是数据结构中比较重要的一种类型,它支持 FIFO,尾部添加、头部删除(先进队列的元素先出队列),跟我们
生活中的排队类似。
队列的种类
单队列(单队列就是常见的队列, 每次添加元素时,都是添加到队尾,存在“假溢出”的问题也就是明明有位置却
不能添加的情况)
循环队列(避免了“假溢出”的问题)
Java 集合框架中的队列 Queue
Java 集合中的 Queue 继承自 Collection 接口 ,Deque, LinkedList, PriorityQueue, BlockingQueue 等类都实现了
它。 Queue 用来存放 等待处理元素 的集合,这种场景一般用于缓冲、并发访问。 除了继承 Collection 接口的一些
方法,Queue 还添加了额外的 添加、删除、查询操作。
推荐文章
Java 集合深入理解(9):Queue 队列
Set
什么是 Set
Set 继承于 Collection 接口,是一个不允许出现重复元素,并且无序的集合,主要 HashSet 和 TreeSet 两大实现
类。
在判断重复元素的时候,Set 集合会调用 hashCode()和 equal()方法来实现。
补充:有序集合与无序集合说明
有序集合:集合里的元素可以根据 key 或 index 访问 (List、Map)
无序集合:集合里的元素只能遍历。(Set)
HashSet 和 TreeSet 底层数据结构
HashSet 是哈希表结构,主要利用 HashMap 的 key 来存储元素,计算插入元素的 hashCode 来获取元素在集合中
的位置;
TreeSet 是红黑树结构,每一个元素都是树中的一个节点,插入的元素都会进行排序;
推荐文章
Java集合–Set(基础)
List
什么是List
在 List 中,用户可以精确控制列表中每个元素的插入位置,另外用户可以通过整数索引(列表中的位置)访问元素,
并搜索列表中的元素。 与 Set 不同,List 通常允许重复的元素。 另外 List 是有序集合而 Set 是无序集合。
List的常见实现类
ArrayList 是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。
LinkedList 是一个双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList随机访问效率低,但随机
插入、随机删除效率高。
Vector 是矢量队列,和ArrayList一样,它也是一个动态数组,由数组实现。但是ArrayList是非线程安全的,而
Vector是线程安全的。
Stack 是栈,它继承于Vector。它的特性是:先进后出(FILO, First In Last Out)。相关阅读:java数据结构与算法之
栈(Stack)设计与实现
ArrayList 和 LinkedList 源码学习
ArrayList 源码学习
LinkedList 源码学习
推荐阅读
java 数据结构与算法之顺序表与链表深入分析
Map
集合框架源码学习之 HashMap(JDK1.8)
ConcurrentHashMap 实现原理及源码分析
树
1 二叉树
二叉树(百度百科)
(1)完全二叉树——若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有
叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。
(2)满二叉树——除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。
(3)平衡二叉树——平衡二叉树又被称为AVL树(区别于AVL算法),它是一棵二叉排序树,且具有以下性质:它
是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
2 完全二叉树
完全二叉树(百度百科)
完全二叉树:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二
叉树
3 满二叉树
满二叉树(百度百科,国内外的定义不同)
国内教程定义:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,
如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
堆
数据结构之堆的定义
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结
点的值都小于或等于其左右孩子结点的值,称为小顶堆
4 二叉查找树(BST)
浅谈算法和数据结构: 七 二叉查找树
二叉查找树的特点: - 若任意节点的左子树不空,则左子树上所有结点的 值均小于它的根结点的值;
- 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 任意节点的左、右子树也分别为二叉查找树。
- 没有键值相等的节点(no duplicate nodes)。
5 平衡二叉树(Self-balancing binary search tree)
平衡二叉树(百度百科,平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等)
6 红黑树
红黑树特点: 1. 每个节点非红即黑; - 根节点总是黑色的;
- 每个叶子节点都是黑色的空节点(NIL节点);
- 如果节点是红色的,则它的子节点必须是黑色的(反之不一定);
- 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)
红黑树的应用:
TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。
为什么要用红黑树
简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
详细了解可以查看 漫画:什么是红黑树?(也介绍到了二叉查找树,非常推荐)
推荐文章:
漫画:什么是红黑树?(也介绍到了二叉查找树,非常推荐)
寻找红黑树的操作手册(文章排版以及思路真的不错)
红黑树深入剖析及Java实现(美团点评技术团队)
7 B-,B+,B树
二叉树学习笔记之B树、B+树、B树 《B-树,B+树,B树详解》
《B-树,B+树与B树的优缺点比较》
B-树(或B树)是一种平衡的多路查找(又称排序)树,在文件系统中有所应用。主要用作文件的索引。其中的B
就表示平衡(Balance) - B+ 树的叶子节点链表结构相比于 B- 树便于扫库,和范围检索。
- B+树支持range-query(区间查询)非常方便,而B树不支持。这是数据库选用B+树的最主要原因。
- B树 是B+树的变体,B树分配新结点的概率比B+树要低,空间使用率更高;
8 LSM 树
[HBase] LSM树 VS B+树
B+树最大的性能问题是会产生大量的随机IO
为了克服B+树的弱点,HBase引入了LSM树的概念,即Log-Structured Merge-Trees。
LSM树由来、设计思想以及应用到HBase的索引
图
BFS及DFS
《使用BFS及DFS遍历树和图的思路及实现》
十一 算法
常见的加密算法、排序算法都需要自己提前了解一下,排序算法最好自己能够独立手写出来。
我觉得面试中最刺激、最有压力或者说最有挑战的一个环节就是手撕算法了。面试中大部分算法题目都是来自于
Leetcode、剑指offer上面,建议大家可以每天挤出一点时间刷一下算法题。
推荐两个刷题必备网站:
LeetCode:
LeetCode(中国)官网
如何高效地使用 LeetCode
牛客网:
牛客网首页
十二 实际场景题
我觉得实际场景题就是对你的知识运用能力以及思维能力的考察。建议大家在平时养成多思考问题的习惯,这样
面试的时候碰到这样的问题就不至于慌了。另外,如果自己实在不会就给面试官委婉的说一下,面试官可能会给你提
醒一下。切忌不懂装懂,乱答一气。 面试官可能会问你类似这样的问题:①假设你要做一个银行app,有
可能碰到多个人同时向一个账户打钱的情况,有可能碰到什么问题,如何解决(锁)②你是怎么保证你的代码质量和
正确性的?③下单过程中是下订单减库存还是付款减库存,分析一下两者的优劣;④同时给10万个人发工资,怎么样
设计并发方案,能确保在1分钟内全部发完。⑤如果让你设计xxx系统的话,你会如何设计。
十三 BATJ真实面试题
美团基础篇 - System.out.println(3|9)输出什么?
正确答案:11.
考察知识点:&和&&;|和||
&和&&:
共同点:两者都可做逻辑运算符。它们都表示运算符的两边都是true时,结果为true;
不同点: &也是位运算符。& 表示在运算时两边都会计算,然后再判断;&&表示先运算符号左边的东西,然后判断是
否为true,是true就继续运算右边的然后判断并输出,是false就停下来直接输出不会再运行后面的东西。
|和||:
共同点:两者都可做逻辑运算符。它们都表示运算符的两边任意一边为true,结果为true,两边都不是true,结果就
为false;
不同点:|也是位运算符。| 表示两边都会运算,然后再判断结果;|| 表示先运算符号左边的东西,然后判断是否为
true,是true就停下来直接输出不会再运行后面的东西,是false就继续运算右边的然后判断并输出。
回到本题:
3 | 9=0011(二进制) | 1001(二进制)=1011(二进制)=11(十进制) - 说一下转发(Forward)和重定向(Redirect)的区别
转发是服务器行为,重定向是客户端行为。
转发(Forword) 通过RequestDispatcher对象的 forward(HttpServletRequest request,HttpServletResponse response) 方法实现的。 RequestDispatcher 可以通过 HttpServletRequest 的 getRequestDispatcher() 方法获得。例如下面的代码就是跳转到 login_success.jsp 页面。
重定向(Redirect) 是利用服务器返回的状态吗来实现的。客户端浏览器请求服务器的时候,服务器会返回一个状
态码。服务器通过HttpServletRequestResponse的setStatus(int status)方法设置状态码。如果服务器返回301或者
302,则浏览器会到新的网址重新请求该资源。
request.getRequestDispatcher(“login_success.jsp”).forward(request, response); - 从地址栏显示来说: forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,
然后把这些内容再发给浏览器.浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址. redirect是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址.所以地址栏显示的是新的URL. - 从数据共享来说: forward:转发页面和转发到的页面可以共享request里面的数据. redirect:不能共享数据. 3. 从运用地方来说: forward:一般用于用户登陆的时候,根据角色转发到相应的模块. redirect:一般用于用户注销登
陆时返回主页面和跳转到其它的网站等 - 从效率来说: forward:高. redirect:低.
- 在浏览器中输入url地址到显示主页的过程,整个过程会使用哪些
协议
图片来源:《图解HTTP》:
总体来说分为以下几个过程: 1. DNS解析 - TCP连接
- 发送HTTP请求
- 服务器处理请求并返回HTTP报文
- 浏览器解析渲染页面
- 连接结束
具体可以参考下面这篇文章:
https://segmentfault.com/a/1190000006879700 - TCP 三次握手和四次挥手
为了准确无误地把数据送达目标处,TCP协议采用了三次握手策略。
漫画图解:
图片来源:《图解HTTP》
简单示意图:
客户端–发送带有 SYN 标志的数据包–一次握手–服务端
服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端
客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端
为什么要三次握手
三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是
双方确认自己与对方的发送与接收是正常的。
第一次握手:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常。
第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己接收正常,对方发
送正常
第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,
对方发送接收正常
所以三次握手就能确认双发收发功能都正常,缺一不可。
为什么要传回 SYN
接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。
SYN 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先
发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以
ACK(Acknowledgement[汉译:确认字符 ,在数据通信传输中,接收站发给发送站的一种传输控制字符。它表
示确认发来的数据已经接受无误。 ])消息响应。这样在客户机和服务器之间才能建立起可靠的TCP连接,数据
才可以在客户机和服务器之间传递。
传了 SYN,为啥还要传 ACK
双方通信无误必须是两者互相发送信息都无误。传了 SYN,证明发送方(主动关闭方)到接收方(被动关闭方)的通
道没有问题,但是接收方到发送方的通道还需要 ACK 信号来进行验证。
断开一个 TCP 连接则需要“四次挥手”:
客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送
服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加1 。和 SYN 一样,一个 FIN 将占用一个序号
服务器-关闭与客户端的连接,发送一个FIN给客户端
客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加1
为什么要四次挥手
任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送
的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。
举个例子:A 和 B 打电话,通话即将结束后,A 说“我没啥要说的了”,B回答“我知道了”,但是 B 可能还会有要说的
话,A 不能要求 B 跟着自己的节奏结束通话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道
了”,这样通话才算结束。
上面讲的比较概括,推荐一篇讲的比较细致的文章:https://blog.csdn.net/qzcsu/article/details/72861891 - IP地址与MAC地址的区别
参考:https://blog.csdn.net/guoweimelon/article/details/50858597
IP地址是指互联网协议地址(Internet Protocol Address)IP Address的缩写。IP地址是IP协议提供的一种统一的地
址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。
MAC 地址又称为物理地址、硬件地址,用来定义网络设备的位置。网卡的物理地址通常是由网卡生产厂家写入网卡
的,具有全球唯一性。MAC地址用于在网络中唯一标示一个网卡,一台电脑会有一或多个网卡,每个网卡都需要有一
个唯一的MAC地址。 - HTTP请求,响应报文格式
HTTP请求报文主要由请求行、请求头部、请求正文3部分组成
HTTP响应报文主要由状态行、响应头部、响应正文3部分组成
详细内容可以参考:https://blog.csdn.net/a19881029/article/details/14002273 - 为什么要使用索引?索引这么多优点,为什么不对表中的每一个列
创建一个索引呢?索引是如何提高查询速度的?说一下使用索引的注
意事项?Mysql索引主要使用的两种数据结构?什么是覆盖索引?
为什么要使用索引? - 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
- 可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。
- 帮助服务器避免排序和临时表
- 将随机IO变为顺序IO
- 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
索引这么多优点,为什么不对表中的每一个列创建一个索引呢? - 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
- 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索
引,那么需要的空间就会更大。 - 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
索引是如何提高查询速度的?
将无序的数据变成相对有序的数据(就像查目录一样)
说一下使用索引的注意事项 - 避免 where 子句中对字段施加函数,这会造成无法命中索引。
- 在使用InnoDB时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键。
- 将打算加索引的列设置为 NOT NULL ,否则将导致引擎放弃使用索引而进行全表扫描
- 删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 MySQL 5.7 可以通过查询 sys 库的
chema_unused_indexes 视图来查询哪些索引从未被使用 - 在使用 limit offset 查询缓慢时,可以借助索引来提高性能
Mysql索引主要使用的哪两种数据结构?
哈希索引:对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可
以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。
BTree索引:Mysql的BTree索引使用的是B树中的B+Tree。但对于主要的两种存储引擎(MyISAM和InnoDB)
的实现方式是不同的。
更多关于索引的内容可以查看我的这篇文章:【思维导图-索引篇】搞定数据库索引就是这么简单
什么是覆盖索引?
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称 之为“覆盖索引”。我们知道在InnoDB存储引
擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就
会比较慢。覆盖索引就是把要查询出的列和索引是对应的,不做回表操作! - 进程与线程的区别是什么?进程间的几种通信方式说一下?线程间
的几种通信方式知道不?
进程与线程的区别是什么?
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同
的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工
作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。另外,也正是因为共享资源,所以线程中执
行时一般都要进行同步和互斥。总的来说,进程和线程的主要差别在于它们是不同的操作系统资源管理方式。
进程间的几种通信方式说一下? - 管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有血缘关系的进程间使用。
进程的血缘关系通常指父子进程关系。管道分为pipe(无名管道)和fifo(命名管道)两种,有名管道也是半双
工的通信方式,但是它允许无亲缘关系进程间通信。 - 信号量(semophore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它通常作为一种锁
机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同
线程之间的同步手段。 - 消息队列(message queue):消息队列是由消息组成的链表,存放在内核中 并由消息队列标识符标识。消
息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。消息队列与管道通信
相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件
接收特定类型的消息。 - 信号(signal):信号是一种比较复杂的通信方式,用于通知接收进程某一事件已经发生。
- 共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进
程创建,但多个进程都可以访问,共享内存是最快的IPC方式,它是针对其他进程间的通信方式运行效率低而专
门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。 - 套接字(socket):socket,即套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)
系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网
络连接计算机上的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来。
线程间的几种通信方式知道不?
1、锁机制
互斥锁:提供了以排它方式阻止数据结构被并发修改的方法。
读写锁:允许多个线程同时读共享数据,而对写操作互斥。
条件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件测试是在互斥锁的保护下进行
的。条件变量始终与互斥锁一起使用。
2、信号量机制:包括无名线程信号量与有名线程信号量
3、信号机制:类似于进程间的信号处理。
线程间通信的主要目的是用于线程同步,所以线程没有象进程通信中用于数据交换的通信机制。 - 为什么要用单例模式?手写几种线程安全的单例模式?
简单来说使用单例模式可以带来下面几个好处:
对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统
开销;
由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。
懒汉式(双重检查加锁版本) public class Singleton { //volatile保证,当uniqueInstance变量被初始化成Singleton实例时,多个线程可以正确处理 uniqueInstance变量 private volatile static Singleton uniqueInstance; private Singleton() { }public static Singleton getInstance() { //检查实例,如果不存在,就进入同步代码块 if (uniqueInstance == null) { //只有第一次才彻底执行这里的代码 synchronized(Singleton.class) { //进入同步代码块后,再检查一次,如果仍是null,才创建实例 if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } }return uniqueInstance;
} }
静态内部类方式
静态内部实现的单例是懒加载的且线程安全。
只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance(只有第一次使用
这个单例的实例的时候才加载,同时不会有线程安全问题)。
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); }private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } } - 简单介绍一下bean;知道Spring的bean的作用域与生命周期
吗? 在 Spring 中,那些组成应用程序的主体及由 Spring IOC 容器所管理的对象,被称之为 bean。简单地讲,bean 就
是由 IOC 容器初始化、装配及管理的对象,除此之外,bean 就与应用程序中的其他对象没有什么区别了。而 bean
的定义以及 bean 相互间的依赖关系将通过配置元数据来描述。
Spring中的bean默认都是单例的,这些单例Bean在多线程程序下如何保证线程安全呢? 例如对于Web应用来说,
Web容器对于每个用户请求都创建一个单独的Sevlet线程来处理请求,引入Spring框架之后,每个Action都是单例
的,那么对于Spring托管的单例Service Bean,如何保证其安全呢? Spring的单例是基于BeanFactory也就是Spring
容器的,单例Bean在此容器内只有一个,Java的单例是基于 JVM,每个 JVM 内只有一个实例。
Spring的bean的生命周期以及更多内容可以查看:一文轻松搞懂Spring中bean的作用域与生命周期 - Spring 中的事务传播行为了解吗?TransactionDefinition 接
口中哪五个表示隔离级别的常量?
事务传播行为
事务传播行为(为了解决业务层方法之间互相调用的事务问题): 当事务方法被另一个事务方法调用时,必须指定
事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。在
TransactionDefinition定义中包括了如下几个表示传播行为的常量:
支持当前事务的情况:
TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,
则创建一个新的事务。
TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,
则以非事务的方式继续运行。
TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事
务,则抛出异常。(mandatory:强制性)
不支持当前事务的情况:
TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前
事务挂起。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当
前事务挂起。
TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。
其他情况:
TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事
务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
隔离级别
TransactionDefinition 接口中定义了五个表示隔离级别的常量:
TransactionDefinition.ISOLATION_DEFAULT: 使用后端数据库默认的隔离级别,Mysql 默认采用的
REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别. TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 最低的隔离级别,允许读取尚未提交的数据变
更,可能会导致脏读、幻读或不可重复读
TransactionDefinition.ISOLATION_READ_COMMITTED: 允许读取并发事务已经提交的数据,可以阻止脏
读,但是幻读或不可重复读仍有可能发生
TransactionDefinition.ISOLATION_REPEATABLE_READ: 对同一字段的多次读取结果都是一致的,除非数据
是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
TransactionDefinition.ISOLATION_SERIALIZABLE: 最高的隔离级别,完全服从ACID的隔离级别。所有的事
务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻
读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 - SpringMVC 原理了解吗?
客户端发送请求-> 前端控制器 DispatcherServlet 接受客户端请求 -> 找到处理器映射 HandlerMapping 解析请求对
应的 Handler-> HandlerAdapter 会根据 Handler 来调用真正的处理器开处理请求,并处理相应的业务逻辑 -> 处理
器返回一个模型视图 ModelAndView -> 视图解析器进行解析 -> 返回一个视图对象->前端控制器 DispatcherServlet
渲染数据(Model)->将得到视图对象返回给用户
关于 SpringMVC 原理更多内容可以查看我的这篇文章:SpringMVC 工作原理详解 - Spring AOP IOC 实现原理
过了秋招挺长一段时间了,说实话我自己也忘了如何简要概括 Spring AOP IOC 实现原理,就在网上找了一个较为简
洁的答案,下面分享给各位。
IOC: 控制反转也叫依赖注入。IOC利用java反射机制,AOP利用代理模式。IOC 概念看似很抽象,但是很容易理解。
说简单点就是将对象交给容器管理,你只需要在spring配置文件中配置对应的bean以及设置相关的属性,让spring
容器来生成类的实例对象以及管理对象。在spring容器启动的时候,spring会把你在配置文件中配置的bean都初始
化好,然后在你需要调用的时候,就把它已经初始化好的那些bean分配给你需要调用这些bean的类。
AOP: 面向切面编程。(Aspect-Oriented Programming) 。AOP可以说是对OOP的补充和完善。OOP引入封装、
继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。实现AOP的技术,主要分为两大类:
一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入
的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码,属于静态代理。
美团进阶篇
1 消息队列MQ的套路
消息队列/消息中间件应该是Java程序员必备的一个技能了,如果你之前没接触过消息队列的话,建议先去百度一下
某某消息队列入门,然后花2个小时就差不多可以学会任何一种消息队列的使用了。如果说仅仅学会使用是万万不够
的,在实际生产环境还要考虑消息丢失等等情况。关于消息队列面试相关的问题,推荐大家也可以看一下视频《Java
工程师面试突击第1季-中华石杉老师》,如果大家没有资源的话,可以在我的公众号“Java面试通关手册”后台回复关
键字“1”即可!
1.1 介绍一下消息队列MQ的应用场景/使用消息队列的好处
面试官一般会先问你这个问题,预热一下,看你知道消息队列不,一般在第一面的时候面试官可能只会问消息队列
MQ的应用场景/使用消息队列的好处、使用消息队列会带来什么问题、消息队列的技术选型这几个问题,不会太深究
下去,在后面的第二轮/第三轮技术面试中可能会深入问一下。
《大型网站技术架构》第四章和第七章均有提到消息队列对应用性能及扩展性的提升。
1)通过异步处理提高系统性能
如上图,在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使
得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即 返回,再由消息队列的消费
者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有
更好的伸缩性),因此响应速度得到大幅改善。
通过以上分析我们可以得出消息队列具有很好的削峰作用的功能——即通过异步处理,将短时间高并发产生的事务消
息存储在消息队列中,从而削平高峰期的并发事务。 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列
可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示:
因为用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能
失败。因此使用消息队列进行异步处理之后,需要适当修改业务流程进行配合,比如用户在提交订单之后,订单数据
写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出
库后,再通过电子邮件或短信通知用户订单成功,以免交易纠纷。这就类似我们平时手机订火车票和电影票。
2)降低系统耦合性
我们知道模块分布式部署以后聚合方式通常有两种:1.分布式消息队列和2.分布式服务。
先来简单说一下分布式服务:
目前使用比较多的用来构建SOA(Service Oriented Architecture面向服务体系结构)的分布式服务框架是阿里巴
巴开源的Dubbo.如果想深入了解Dubbo的可以看我写的关于Dubbo的这一篇文章:《高性能优秀的服务框架-
dubbo介绍》:https://juejin.im/post/5acadeb1f265da2375072f9c
再来谈我们的分布式消息队列:
我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无
疑更好一些。
我们最常见的事件驱动架构类似生产者消费者模式,在大型网站中通常用利用消息队列实现事件驱动结构。如下图所
示:
消息队列使利用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消
息。 从上图可以看到消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合,消息发送者将消息发送至
分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消
息从何而来。对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网
站业务的可扩展性设计。
消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者
订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。
另外为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消
息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集
群中的其他服务器发布消息。
备注: 不要认为消息队列只能利用发布-订阅模式工作,只不过在解耦这个特定业务环境下是使用发布-订阅模式的,
比如在我们的ActiveMQ消息队列中还有点对点工作模式,具体的会在后面的文章给大家详细介绍,这一篇文章主要
还是让大家对消息队列有一个更透彻的了解。
这个问题一般会在上一个问题问完之后,紧接着被问到。“使用消息队列会带来什么问题?”这个问题要引起重
视,一般我们都会考虑使用消息队列会带来的好处而忽略它带来的问题!
1.2 那么使用消息队列会带来什么问题?考虑过这些问题吗?
系统可用性降低: 系统可用性在某种程度上降低,为什么这样说呢?在加入MQ之前,你不用考虑消息丢失或
者说MQ挂掉等等的情况,但是,引入MQ之后你就需要去考虑了!
系统复杂性提高: 加入MQ之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺
序性等等问题!
一致性问题: 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万
一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了!
了解下面这个问题是为了我们更好的进行技术选型!该部分摘自:《Java工程师面试突击第1季-中华石杉老
师》,如果大家没有资源的话,可以在我的公众号“Java面试通关手册”后台回复关键字“1”即可!
1.3 介绍一下你知道哪几种消息队列,该如何选择呢?
特性 ActiveMQ RabbitMQ RocketMQ Kafaka
单机
吞吐
量
万级,吞
吐量比
RocketMQ
和Kafka要
低了一个
数量级
万级,吞吐量比RocketMQ和
Kafka要低了一个数量级
10万级,RocketMQ也是可
以支撑高吞吐的一种MQ
10万级别,这是
kafka最大的优
点,就是吞吐量
高。一般配合大
数据类的系统来
进行实时数据计
算、日志采集等
场景
topic
数量
对吞
吐量
的影
响
topic可以达到几百,几千个
的级别,吞吐量会有较小幅
度的下降这是RocketMQ的
一大优势,在同等机器下,
可以支撑大量的topic
topic从几十个到
几百个的时候,
吞吐量会大幅度
下降。所以在同
等机器下,kafka
尽量保证topic数
量不要过多。如
果要支撑大规模
topic,需要增加
更多的机器资源
可用
性
高,基于
主从架构
实现高可
用性
高,基于主从架构实现高可用
性
非常高,分布式架构
非常高,kafka是
分布式的,一个
数据多个副本,
少数机器宕机,
不会丢失数据,
不会导致不可用
消息
可靠
性
有较低的
概率丢失
数据
经过参数优化配置,可以做
到0丢失
经过参数优化配
置,消息可以做
到0丢失
时效
性
ms级
微秒级,这是rabbitmq的一大
特点,延迟是最低的 ms级 延迟在ms级以内
功能
支持
MQ领域的
功能极其
完备
基于erlang开发,所以并发能
力很强,性能极其好,延时很
低
MQ功能较为完善,还是分
布式的,扩展性好
功能较为简单,
主要支持简单的
MQ功能,在大
数据领域的实时
计算以及日志采
集被大规模使
用,是事实上的
标准
特性 ActiveMQ RabbitMQ RocketMQ Kafaka
优劣
势总
结
非常成
熟,功能
强大,在
业内大量
的公司以
及项目中
都有应
用。偶尔
会有较低
概率丢失
消息,而
且现在社
区以及国
内应用都
越来越
少,官方
社区现在
对
ActiveMQ
5.x维护越
来越少,
几个月才
发布一个
版本而且
确实主要
是基于解
耦和异步
来用的,
较少在大
规模吞吐
的场景中
使用
erlang语言开发,性能极其
好,延时很低;吞吐量到万
级,MQ功能比较完备而且开
源提供的管理界面非常棒,用
起来很好用。社区相对比较活
跃,几乎每个月都发布几个版
本分在国内一些互联网公司近
几年用rabbitmq也比较多一些
但是问题也是显而易见的,
RabbitMQ确实吞吐量会低一
些,这是因为他做的实现机制
比较重。而且erlang开发,国
内有几个公司有实力做erlang
源码级别的研究和定制?如果
说你没这个实力的话,确实偶
尔会有一些问题,你很难去看
懂源码,你公司对这个东西的
掌控很弱,基本职能依赖于开
源社区的快速维护和修复
bug。而且rabbitmq集群动态
扩展会很麻烦,不过这个我觉
得还好。其实主要是erlang语
言本身带来的问题。很难读源
码,很难定制和掌控。
接口简单易用,而且毕竟在
阿里大规模应用过,有阿里
品牌保障。日处理消息上百
亿之多,可以做到大规模吞
吐,性能也非常好,分布式
扩展也很方便,社区维护还
可以,可靠性和可用性都是
ok的,还可以支撑大规模的
topic数量,支持复杂MQ业
务场景。而且一个很大的优
势在于,阿里出品都是java
系的,我们可以自己阅读源
码,定制自己公司的MQ,
可以掌控。社区活跃度相对
较为一般,不过也还可以,
文档相对来说简单一些,然
后接口这块不是按照标准
JMS规范走的有些系统要迁
移需要修改大量代码。还有
就是阿里出台的技术,你得
做好这个技术万一被抛弃,
社区黄掉的风险,那如果你
们公司有技术实力我觉得用
RocketMQ挺好的
kafka的特点其实
很明显,就是仅
仅提供较少的核
心功能,但是提
供超高的吞吐
量,ms级的延
迟,极高的可用
性以及可靠性,
而且分布式可以
任意扩展。同时
kafka最好是支撑
较少的topic数量
即可,保证其超
高吞吐量。而且
kafka唯一的一点
劣势是有可能消
息重复消费,那
么对数据准确性
会造成极其轻微
的影响,在大数
据领域中以及日
志采集中,这点
轻微影响可以忽
略这个特性天然
适合大数据实时
计算以及日志收
集。
这部分内容,我这里不给出答案,大家可以自行根据自己学习的消息队列查阅相关内容,我可能会在后面的文
章中介绍到这部分内容。另外,下面这些问题在视频《Java工程师面试突击第1季-中华石杉老师》中都有提
到,如果大家没有资源的话,可以在我的公众号“Java面试通关手册”后台回复关键字“1”即可!
1.4 关于消息队列其他一些常见的问题展望 - 引入消息队列之后如何保证高可用性
- 如何保证消息不被重复消费呢?
- 如何保证消息的可靠性传输(如何处理消息丢失的问题)?
- 我该怎么保证从消息队列里拿到的数据按顺序执行?
- 如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,
说说怎么解决? - 如果让你来开发一个消息队列中间件,你会怎么设计架构?
2 谈谈 InnoDB 和 MyIsam 两者的区别
2.1 两者的对比 - count运算上的区别: 因为MyISAM缓存有表meta-data(行数等),因此在做COUNT(*)时对于一个结构很好
的查询是不需要消耗多少资源的。而对于InnoDB来说,则没有这种缓存 - 是否支持事务和崩溃后的安全恢复: MyISAM 强调的是性能,每次查询具有原子性,其执行数度比InnoDB类型
更快,但是不提供事务支持。但是InnoDB 提供事务支持事务,外部键等高级数据库功能。 具有事务
(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID
compliant))型表。 - 是否支持外键: MyISAM不支持,而InnoDB支持。
2.2 关于两者的总结
MyISAM更适合读密集的表,而InnoDB更适合写密集的的表。 在数据库做主从分离的情况下,经常选择MyISAM作
为主库的存储引擎。
一般来说,如果需要事务支持,并且有较高的并发读取频率(MyISAM的表锁的粒度太大,所以当该表写并发量较高
时,要等待的查询就会很多了),InnoDB是不错的选择。如果你的数据量很大(MyISAM支持压缩特性可以减少磁盘
的空间占用),而且不需要支持事务时,MyISAM是最好的选择。
3 聊聊 Java 中的集合吧!
3.1 Arraylist 与 LinkedList 有什么不同?(注意加上从数据结构分析的内容) - 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
- 底层数据结构: Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向链表数据结构(注意双向
链表和双向循环链表的区别:); - 插入和删除是否受元素位置的影响: ① ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素
位置的影响。 比如:执行 add(E e) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种
情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话( add(int index, E element) )时
间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向
前移一位的操作。 ② LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是
近似 O(1)而数组为近似 O(n)。 - 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通
过元素的序号快速获取元素对象(对应于 get(int index) 方法)。 - 内存空间占用: ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空
间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数
据)。
补充内容:RandomAccess接口
查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过是一个
标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。
public interface RandomAccess { }
在binarySearch()方法中,它要判断传入的list 是否RamdomAccess的实例,如果是,调用
indexedBinarySearch()方法,如果不是,那么调用iteratorBinarySearch()方法
ArraysList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。为什么呢?我觉得还是和底层数据结构有关!
ArraysList 底层是数组,而 LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速
随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访
问。,ArraysList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。 RandomAccess 接口只是标
识,并不是说 ArraysList 实现 RandomAccess 接口才具有快速随机访问功能的!
下面再总结一下 list 的遍历方式选择:
实现了RandomAccess接口的list,优先选择普通for循环 ,其次foreach,
未实现RandomAccess接口的ist, 优先选择iterator遍历(foreach遍历底层也是通过iterator实现的),大size
的数据,千万不要使用普通for循环
Java 中的集合这类问题几乎是面试必问的,问到这类问题的时候,HashMap 又是几乎必问的问题,所以大家
一定要引起重视!
3.2 HashMap的底层实现
1)JDK1.8之前
JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经
过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的时数组的
长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的
话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的
hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
JDK 1.8 HashMap 的 hash 方法源码:
JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。
public static int binarySearch(List<? extends Comparable<? super T>> list, T key) { if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD) return Collections.indexedBinarySearch(list, key); elsereturn Collections.iteratorBinarySearch(list, key); } static final int hash(Object key) { int h; // key.hashCode():返回散列值也就是hashcode // ^ :按位异或 // >>>:无符号右移,忽略符号位,空位都以0补齐 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
对比一下 JDK1.7的 HashMap 的 hash 方法源码.
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希
冲突,则将冲突的值加到链表中即可。
2)JDK1.8之后
相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转
化为红黑树,以减少搜索时间。
static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为
二叉查找树在某些情况下会退化成一个线性结构。
问完 HashMap 的底层原理之后,面试官可能就会紧接着问你 HashMap 底层数据结构相关的问题!
3.3 既然谈到了红黑树,你给我手绘一个出来吧,然后简单讲一下自己对于红黑
树的理解
红黑树特点: 1. 每个节点非红即黑; - 根节点总是黑色的;
- 每个叶子节点都是黑色的空节点(NIL节点);
- 如果节点是红色的,则它的子节点必须是黑色的(反之不一定);
- 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)
红黑树的应用:
TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。
为什么要用红黑树
简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
3.4 红黑树这么优秀,为何不直接使用红黑树得了?
说一下自己对于这个问题的看法:我们知道红黑树属于(自)平衡二叉树,但是为了保持“平衡”是需要付出代价的,
红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,这费事啊。你说说我们引入红黑树就是
为了查找数据快,如果链表长度很短的话,根本不需要引入红黑树的,你引入之后还要付出代价维持它的平衡。但是
链表过长就不一样了。至于为什么选 8 这个值呢?通过概率统计所得,这个值是综合查询成本和新增元素成本得出的
最好的一个值。
3.5 HashMap 和 Hashtable 的区别/HashSet 和 HashMap 区别
HashMap 和 Hashtable 的区别 - 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过
synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!); - 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在
代码中使用它; - 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键
所对应的值为 null。。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。 4. 初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为
11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来
的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充
为2的幂次方大小(HashMap 中的 tableSizeFor() 方法保证,下面给出了源代码)。也就是说 HashMap 总
是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。 - 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为
8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
HashSet 和 HashMap 区别
如果你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常
少,因为除了 clone() 方法、writeObject()方法、readObject()方法是 HashSet 自己不得不实现之外,其他方法都是
直接调用 HashMap 中的方法。)
美团终结篇 - Object类有哪些方法?
这个问题,面试中经常出现。我觉得不论是出于应付面试还是说更好地掌握Java这门编程语言,大家都要掌握!
1.1 Object类的常见方法总结
Object类是一个特殊的类,是所有类的父类。它主要提供了以下11个方法:
public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了 final关键字修饰,故不允许子类重写。
问完上面这个问题之后,面试官很可能紧接着就会问你“hashCode与equals”相关的问题。
1.2 hashCode与equals
面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?”
1.2.1 hashCode()介绍
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在
哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函
数。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将
对象的 内存地址 转换为整数之后返回。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!
(可以快速找到所需要的对象)
1.2.2 为什么要有hashCode
我们以“HashSet如何检查重复”为例子来说明为什么要有hashCode: public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的 HashMap。 public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户 比较字符串的值是否相等。 protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回 当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生 CloneNotSupportedException异常。 public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方 法。public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视 器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。 public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒 在此对象监视器上等待的所有线程,而不是一个线程。 public final native void wait(long timeout) throws InterruptedException//native方法,并且不能 重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。 public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数, 这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。 public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等 待,没有超时时间这个概念 protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作 public native int hashCode();
当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他已经加
入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相
同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,
HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head fist
java》第二版)。这样我们就大大减少了equals的次数,相应就大大提高了执行速度。
1.2.3 hashCode()与equals()的相关规定 - 如果两个对象相等,则hashcode一定也是相同的
- 两个对象相等,对两个对象分别调用equals方法都返回true
- 两个对象有相同的hashcode值,它们也不一定是相等的
- 因此,equals方法被覆盖过,则hashCode方法也必须被覆盖
- hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何
都不会相等(即使这两个对象指向相同的数据)
1.2.4 为什么两个对象有相同的hashcode值,它们也不一定是相等的?
在这里解释一位小伙伴的问题。以下内容摘自《Head Fisrt Java》。
因为hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这
也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode)。
我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断
是否真的相同。也就是说 hashcode 只是用来缩小查找成本。
与equals 的对比也是比较常问的基础问题之一!
1.3 与equals
== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型比较的是
值,引用数据类型比较的是内存地址)
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
情况1:类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“”比较这两个对象。
情况2:类覆盖了equals()方法。一般,我们都覆盖equals()方法来两个对象的内容相等;若它们的内容相等,
则返回true(即,认为这两个对象相等)。
举个例子:
public class test1 { public static void main(String[] args) { String a = new String(“ab”); // a 为一个引用 String b = new String(“ab”); // b为另一个引用,对象的内容一样 String aa = “ab”; // 放在常量池中 String bb = “ab”; // 从常量池中查找 if (aa == bb) // true System.out.println("aabb"); if (a == b) // false,非同一对象 System.out.println(“a==b”); if (a.equals(b)) // true System.out.println(“aEQb”); if (42 == 42.0) { // true System.out.println(“true”);
说明:
String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址,而String的equals方
法比较的是对象的值。
当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就
把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。
在【备战春招/秋招系列5】美团面经总结进阶篇 (附详解答案) 这篇文章中,我们已经提到了一下关于
HashMap 在面试中常见的问题:HashMap 的底层实现、简单讲一下自己对于红黑树的理解、红黑树这么优
秀,为何不直接使用红黑树得了、HashMap 和 Hashtable 的区别/HashSet 和 HashMap 区别。HashMap 和
ConcurrentHashMap 这俩兄弟在一般只要面试中问到集合相关的问题就一定会被问到,所以各位务必引起重
视!
2 ConcurrentHashMap 相关问题
2.1 ConcurrentHashMap 和 Hashtable 的区别
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟
HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类
似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了
分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁
竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经
摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用
synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且
线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容
旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步
方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用
put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
两者的对比图:
图片来源:http://www.cnblogs.com/chengxiao/p/6842045.html
HashTable:
} } }
JDK1.7的ConcurrentHashMap:
JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):
2.2 ConcurrentHashMap线程安全的具体实现方式/底层具体实现
JDK1.7(上面有示意图)
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的
数据也能被其他线程访问。
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。
Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数
据。
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结
构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个
HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。
JDK1.8(上面有示意图)
ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8
的结构类似,数组+链表/红黑二叉树。
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
3 谈谈 synchronized 和 ReenTrantLock 的区别
① 两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时
这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死
锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API
synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多
优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就
是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看
它是如何实现的。
③ ReenTrantLock 比 synchronized 增加了一些高级功能
相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;
③可实现选择性通知(锁可以绑定多个条件)
ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也
就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等
待的线程先获得锁。 ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的
ReentrantLock(boolean fair) 构造方法来制定是否是公平的。
synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也
可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很
好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视
器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵
活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合
Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而
synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果
执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的
signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。
④ 两者的性能已经相差无几
static class Segment<K,V> extends ReentrantLock implements Serializable { }
在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具体表示为:synchronized 关键字吞吐量岁线
程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了,
synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM
团队对 synchronized 关键字做了很多优化。JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了。
所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的!JDK1.6之后,性能已经不是选择synchronized和
ReenTrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在
synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和
ReenTrantLock一样,在很多地方都是用到了CAS操作。
4 线程池了解吗?
4.1 为什么要用线程池?
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务
的数量。
这里借用《Java并发编程的艺术》提到的来说一下使用线程池的好处:
降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,
使用线程池可以进行统一的分配,调优和监控。
4.2 Java 提供了哪几种线程池?他们各自的使用场景是什么?
Java 主要提供了下面4种线程池
FixedThreadPool: 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的
任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线
程空闲时,便处理在任务队列中的任务。
SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会
被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但
若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新
的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
ScheduledThreadPoolExecutor: 主要用来在给定的延迟后运行任务,或者定期执行任务。
ScheduledThreadPoolExecutor又分为:ScheduledThreadPoolExecutor(包含多个线程)和
SingleThreadScheduledExecutor (只包含一个线程)两种。
各种线程池的适用场景介绍
FixedThreadPool: 适用于为了满足资源管理需求,而需要限制当前线程数量的应用场景。它适用于负载比较
重的服务器;
SingleThreadExecutor: 适用于需要保证顺序地执行各个任务并且在任意时间点,不会有多个线程是活动的
应用场景。
CachedThreadPool: 适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器;
ScheduledThreadPoolExecutor: 适用于需要多个后台执行周期任务,同时为了满足资源管理需求而需要限
制后台线程的数量的应用场景,
SingleThreadScheduledExecutor: 适用于需要单个后台线程执行周期任务,同时保证顺序地执行各个任务
的应用场景。
4.3 创建的线程池的方式
(1) 使用 Executors 创建
我们上面刚刚提到了 Java 提供的几种线程池,通过 Executors 工具类我们可以很轻松的创建我们上面说的几种线程
池。但是实际上我们一般都不是直接使用Java提供好的线程池,另外在《阿里巴巴Java开发手册》中强制线程池不允
许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数 的方式,这样的处理方式让写的同学更加明确
线程池的运行规则,规避资源耗尽的风险。
(2) ThreadPoolExecutor的构造函数创建
我们可以自己直接调用 ThreadPoolExecutor 的构造函数来自己创建线程池。在创建的同时,给 BlockQueue 指定容
量就可以了。示例如下:
这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出
java.util.concurrent.RejectedExecutionException,这是因为当前线程池使用的队列是有边界队列,队列已经满了
便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好。
(3) 使用开源类库
Hollis 大佬之前在他的文章中也提到了:“除了自己定义ThreadPoolExecutor外。还有其他方法。这个时候第一时间
就应该想到开源类库,如apache和guava等。”他推荐使用guava提供的ThreadFactoryBuilder来创建线程池。下面
是参考他的代码示例:
Executors 返回线程池对象的弊端如下: FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请 求,从而导致OOM。 CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量 线程,从而导致OOM。 private static ExecutorService executor = new ThreadPoolExecutor(13, 13, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(13)); public class ExecutorsDemo { private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder() .setNameFormat(“demo-pool-%d”).build(); private static ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy()); public static void main(String[] args) { for (int i = 0; i < Integer.MAX_VALUE; i++) { pool.execute(new SubThread()); } }
通过上述方式创建线程时,不仅可以避免OOM的问题,还可以自定义线程名称,更加方便的出错的时候溯源。
5 Nginx
5.1 简单介绍一下Nginx
Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。 Nginx 主要提供反向代
理、负载均衡、动静分离(静态资源服务)等服务。下面我简单地介绍一下这些名词。
反向代理
谈到反向代理,就不得不提一下正向代理。无论是正向代理,还是反向代理,说到底,就是代理模式的衍生版本罢了
正向代理:某些情况下,代理我们用户去访问服务器,需要用户手动的设置代理服务器的ip和端口号。正向代
理比较常见的一个例子就是 VPN了。
反向代理: 是用来代理服务器的,代理我们要访问的目标服务器。代理服务器接受请求,然后将请求转发给内
部网络的服务器,并将从服务器上得到的结果返回给客户端,此时代理服务器对外就表现为一个服务器。
通过下面两幅图,大家应该更好理解(图源:http://blog.720ui.com/2016/nginx_action_05_proxy/):
}
所以,简单的理解,就是正向代理是为客户端做代理,代替客户端去访问服务器,而反向代理是为服务器做代理,代
替服务器接受客户端请求。
负载均衡
在高并发情况下需要使用,其原理就是将并发请求分摊到多个服务器执行,减轻每台服务器的压力,多台服务器(集 群)共同完成工作任务,从而提高了数据的吞吐量。
Nginx支持的weight轮询(默认)、ip_hash、fair、url_hash这四种负载均衡调度算法,感兴趣的可以自行查阅。
负载均衡相比于反向代理更侧重的时将请求分担到多台服务器上去,所以谈论负载均衡只有在提供某服务的服务器大
于两台时才有意义。
动静分离
动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以
后,我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路。
5.2 为什么要用 Nginx?
这部分内容参考极客时间—Nginx核心知识100讲的内容。
如果面试官问你这个问题,就一定想看你知道 Nginx 服务器的一些优点吗。
Nginx 有以下5个优点: - 高并发、高性能(这是其他web服务器不具有的)
- 可扩展性好(模块化设计,第三方插件生态圈丰富)
- 高可靠性(可以在服务器行持续不间断的运行数年)
- 热部署(这个功能对于 Nginx 来说特别重要,热部署指可以在不停止 Nginx服务的情况下升级 Nginx)
- BSD许可证(意味着我们可以将源代码下载下来进行修改然后使用自己的版本)
5.3 Nginx 的四个主要组成部分了解吗?
这部分内容参考极客时间—Nginx核心知识100讲的内容。
Nginx 二进制可执行文件:由各模块源码编译出一个文件
Nginx.conf 配置文件:控制Nginx 行为
acess.log 访问日志: 记录每一条HTTP请求信息
error.log 错误日志:定位问题
写在最后
最后,再强调几点: - 一定要谨慎对待写在简历上的东西,一定要对简历上的东西非常熟悉。因为一般情况下,面试官都是会根据你
的简历来问的; 2. 能有一个上得了台面的项目也非常重要,这很可能是面试官会大量发问的地方,所以在面试
之前好好回顾一下自己所做的项目; - 和面试官聊基础知识比如设计模式的使用、多线程的使用等等,可以结合具体的项目场景或者是自己在平时是
如何使用的; - 注意自己开源的Github项目,面试官可能会挖你的Github项目提问;
- 建议提前了解一下自己想要面试的公司的价值观,判断一下自己究竟是否适合这个公司。
另外,我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、
丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!
本文档由 SnailClimb 整理,文章大部分内容来源于本人的开源项目 JavaGuide,你可以把这个文档看做JavaGuide
的精简版,适合面试前的突击。更多精彩内容,欢迎关注我的公众号:JavaGuide。如需转载对应的文章,请附上下
面一段内容:
本文转载自 JavaGuide 地址:https://github.com/Snailclimb/JavaGuide 作者:SnailClimb