Offer来了Java面试核心知识点精讲原理篇.pdf
http://www.100md.com
2020年11月16日
![]() |
| 第1页 |
![]() |
| 第8页 |
![]() |
| 第17页 |
![]() |
| 第22页 |
![]() |
| 第49页 |
![]() |
| 第122页 |
参见附件(8254KB,124页)。
Offer来了:Java面试核心知识点精讲(原理篇)是对Java程序员面试必备知识点的总结,详细讲解了JVM原理、多线程、数据结构和算法、分布式缓存、设计模式等面试必备知识点,在讲解时不拖泥带水,力求精简。

编辑推荐
适读人群 :中级Java、高级Java、Java架构师
本书在手,Java程序员笑傲“江湖”!
面试在即,Java知识点很凌乱?
别急,本书精选重要知识点为你精细讲解:
JVM原理、Java基础;
并发编程、数据结构和算法;
网络与负载均衡;;
数据库与分布式事务
分布式缓存原理及应用;
设计模式原理及实现。
除了原理讲解,还有Java实现!
面试时的原理+动手实现脑海已就位,整装待发!
互联网寒冬怕什么!
另外,作者亲授掌握本书知识点技巧:
----3周:细读本书,详细理解书中的知识点、代码和架构图。
----2天:对着本书目录回忆知识点,及时查漏补缺。
----3小时:复习本书,以充分掌握本书知识点。
我想,你应该会更快!加油!
内容简介
本书是对Java程序员面试必备知识点的总结,详细讲解了JVM原理、多线程、数据结构和算法、分布式缓存、设计模式等面试必备知识点,在讲解时不拖泥带水,力求精简。
本书总计9章,第1章讲解JVM原理,涉及JVM运行机制、JVM内存模型、常用垃圾回收算法和JVM类加载机制等内容;第2章讲解Java基础知识,涉及集合、异常分类及处理、反射、注解、内部类、泛型和序列化等内容;第3章讲解Java并发编程知识,涉及Java多线程的工作原理及应用、Java线程池的工作原理及应用,以及锁、进程调度算法等内容;第4章讲解数据结构知识,涉及栈、队列、链表、散列表、二叉树、红黑树、图和位图等内容;第5章讲解Java中的常用算法,涉及二分查找、冒泡排序、插入排序、快速排序、希尔排序、归并排序、桶排序、基数排序等算法;第6章讲解网络与负载均衡原理,涉及TCP/IP、HTTP、常用负载均衡算法和LVS原理等内容;第7章讲解数据库及分布式事务原理,涉及数据库存储引擎、数据库并发操作和锁、数据库分布式事务等内容;第8章讲解分布式缓存的原理及应用,涉及分布式缓存介绍、Ehcache原理及应用、Redis原理及应用、分布式缓存设计的核心问题等内容;第9章讲解设计模式,涉及常见的23种经典设计模式。
本书可作为Java程序员的技术面试参考用书,也可作为Java程序员、技术经理和架构师的日常技术参考用书。
作者简介
王磊,现任国内某知名互联网公司大数据技术架构师,有十余年丰富的物联网及大数据研发和技术架构经验,对物联网及大数据的原理和技术实现有深刻的理解。长期从事海外项目的研发和交付工作,对异地多活数据中心的建设及高可用、高并发系统的设计有丰富的实战经验。
章节架构
第1章讲解JVM原理,涉及JVM运行机制、JVM内存模型、常用垃圾回收算法和JVM类加载机制等内容。
第2章讲解Java基础知识,涉及集合、异常分类及处理、反射、注解、内部类、泛型和序列化等内容。
第3章讲解Java并发编程知识,涉及Java多线程的工作原理及应用、Java线程池的工作原理及应用,以及锁、进程调度算法等内容。
第4章讲解数据结构知识,涉及栈、队列、链表、散列表、二叉树、红黑树、图和位图等内容。
第5章讲解Java中的常用算法,涉及二分查找、冒泡排序、插入排序、快速排序、希尔排序、归并排序、桶排序、基数排序等算法。
第6章讲解网络与负载均衡原理,涉及TCP/IP、HTTP、常用负载均衡算法和LVS原理等内容。
第7章讲解数据库及分布式事务原理,涉及数据库存储引擎、数据库并发操作和锁、数据库分布式事务等内容。
第8章讲解分布式缓存的原理及应用,涉及分布式缓存介绍、Ehcache原理及应用、Redis原理及应用、分布式缓存设计的核心问题等内容。
第9章讲解设计模式,涉及常见的23种经典设计模式。
Offer来了Java面试核心知识点精讲原理篇截图



时间 版本 说明
2019-2-27 v 1.0 初版发布
2019-3-2 v 2.0 对于第一版进行了大幅度更新,除了修改了一些小错误之外,还增加了一些内容。
2019-4-18 v3.0 修复错误,完善内容,增加了少部分内容。
必看
本文档由 SnailClimb 整理,文章大部分内容来源于本人的开源项目 JavaGuide,你可以把这个文档看做JavaGuide
的精简版,适合面试前的突击。更多精彩内容,欢迎关注我的公众号:JavaGuide。如需转载对应的文章,请附上下
面一段内容:
本文转载自JavaGuide,地址:https:github.comSnailclimbJavaGuide,作者:SnailClimb
历史更新记录
建议阅读本文档的方式
本文档提供详细的目录,建议大家使用电脑阅读。如果大家用手机阅读的话,可以下载一个不错的PDF阅读器,比如
很多人常用的福昕PDF阅读器。
本文档提供详细的目录,大家可以根据自己的实际需要选择自己薄弱的知识章节阅读。
前言
不论是校招还是社招都避免不了各种面试、笔试,如何去准备这些东西就显得格外重要。不论是笔试还是面试都是有
章可循的,我这个“有章可循”说的意思只是说应对技术面试是可以提前准备。
运筹帷幄之后,决胜千里之外!不打毫无准备的仗,我觉得大家可以先从下面几个方面来准备面试:1. 自我介绍。(你可千万这样介绍:“我叫某某,性别,来自哪里,学校是那个,自己爱干什么”,记住:多说点简
历上没有的,多说点自己哪里比别人强!)
2. 自己面试中可能涉及哪些知识点、那些知识点是重点。
3. 面试中哪些问题会被经常问到、面试中自己改如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多
少?能记住多久?第二:背题的方式的学习很难坚持下去!)
4. 自己的简历该如何写。
“80%的o?er掌握在20%的人手中” 这句话也不是不无道理的。决定你面试能否成功的因素中实力固然占有很大一部
分比例,但是如果你的心态或者说运气不好的话,依然无法拿到满意的 o?er。运气暂且不谈,就拿心态来说,千万
不要因为面试失败而气馁或者说怀疑自己的能力,面试失败之后多总结一下失败的原因,后面你就会发现自己会越来
越强大。
另外,大家要明确的很重要的几点是:
1. 写在简历上的东西一定要慎重,这可能是面试官大量提问的地方;
2. 大部分应届生找工作的硬伤是没有工作经验或实习经历;
3. 将自己的项目经历完美的展示出来非常重要。
笔主能力有限,如果有不对的地方或者和你想法不同的地方,敬请雅正、不舍赐教。
一 面试前的准备
1.1 如何准备一场面试
不论是校招还是社招都避免不了各种面试、笔试,如何去准备这些东西就显得格外重要。不论是笔试还是面试都是有
章可循的,我这个“有章可循”说的意思只是说应对技术面试是可以提前准备。 我其实特别不喜欢那种临近考试就提前
背啊记啊各种题的行为,非常反对!我觉得这种方法特别极端,而且在稍有一点经验的面试官面前是根本没有用的。
建议大家还是一步一个脚印踏踏实实地走。
1.1.1 如何获取大厂面试机会?
在讲如何获取大厂面试机会之前,先来给大家科普对比一下两个校招非常常见的概念——春招和秋招。
1. 招聘人数 :秋招多于春招 ;
2. 招聘时间 : 秋招一般7月左右开始,大概一直持续到10月底。但是大厂(如BAT)都会早开始早结束,所以一
定要把握好时间。春招最佳时间为3月,次佳时间为4月,进入5月基本就不会再有春招了(金三银四)。
3. 应聘难度 :秋招略大于春招;
4. 招聘公司: 秋招数量多,而春招数量较少,一般为秋招的补充。
综上,一般来说,秋招的含金量明显是高于春招的。
下面我就说一下我自己知道的一些方法,不过应该也涵盖了大部分获取面试机会的方法。
1. 关注大厂官网,随时投递简历(走流程的网申);
2. 线下参加宣讲会,直接投递简历;
3. 找到师兄师姐认识的人,帮忙内推(能够让你避开网申简历筛选,笔试筛选,还是挺不错的,不过也还是需要
你的简历够棒);
4. 博客发文被看中Github优秀开源项目作者,大厂内部人员邀请你面试;
5. 求职类网站投递简历(不是太推荐,适合海投);除了这些方法,我也遇到过这样的经历:有些大公司的一些部门可能暂时没招够人,然后如果你的亲戚或者朋友刚好
在这个公司,而你正好又在寻求o?er,那么面试机会基本上是有了,而且这种面试的难度好像一般还普遍比其他正
规面试低很多。
1.1.2 面试必知
下面几点概括起来就是:了解自己的能力、要应聘的公司、自己要应聘的岗位,提前做好自己我介绍以及项目介绍等
等方面的功课,确保你能在面试过程中简短清晰的回答出来(可以用Star法则来组织自己的语言)。
1) 准备自己的自我介绍
从HR面、技术面到高管面部门主管面,面试官一般会让你先自我介绍一下,所以好好准备自己的自我介绍真的非常
重要。网上一般建议的是准备好两份自我介绍:一份对hr说的,主要讲能突出自己的经历,会的编程技术一语带过;
另一份对技术面试官说的,主要讲自己会的技术细节,项目经验,经历那些就一语带过。
我这里简单分享一下我自己的自我介绍的一个简单的模板吧:
面试官,您好!我叫某某。大学时间我主要利用课外时间学习某某。在校期间参与过一个某某系统的开发,另
外,自己学习过程中也写过很多系统比如某某系统。在学习之余,我比较喜欢通过博客整理分享自己所学知
识。我现在是某某社区的认证作者,写过某某很不错的文章。另外,我获得过某某奖,我的Github上开源的某个
项目已经有多少Star了。
2) 关于着装
穿西装、打领带、小皮鞋?NO!NO!NO!这是互联网公司面试又不是去走红毯,所以你只需要穿的简单大方就
好,不需要太正式。
3) 随身带上自己的成绩单和简历
有的公司在面试前都会让你交一份成绩单和简历当做面试中的参考。
4) 如果需要笔试就提前刷一些笔试题
平时空闲时间多的可以刷一下笔试题目(牛客网上有很多)。但是不要只刷面试题,不动手code,程序员不是为了
考试而存在的。
5) 花时间一些逻辑题
面试中发现有些公司都有逻辑题测试环节,并且都把逻辑笔试成绩作为很重要的一个参考。
6) 准备好自己的项目介绍
如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑:
1. 对项目整体设计的一个感受(面试官可能会让你画系统的架构图)
2. 在这个项目中你负责了什么、做了什么、担任了什么角色
3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用
4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个
棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用redis做缓存提高
访问速度和并发量、使用消息队列削峰和降流等等。
7) 提前了解公司以及要应聘的岗位 面试之前一定要提前对要应聘的公司以及岗位有所了解,这一点对于喜欢海投的同学来说要格外注意。如果你去一个
公司面试连公司的主要业务或者主要产品都不了解的话,那么面试官打心里肯定会觉得你并没有很重视他们公司,所
以他们为什么要重视你呢?你也要提前了解你所要应聘岗位对你的专业能力或者其他能力的要求,比如有的岗位就是
需要英语水平比较高,需要你通过六级或者托福雅思,假如你不满足的话,那就没必要再去投递简历面试了。
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 内存淘汰机制等等。
所以,首先,你要明确的一点是:你不会的东西就不要写在简历上。另外,你要考虑你该如何才能让你的亮点在简历
中凸显出来,比如:你在某某项目做了什么事情解决了什么问题(只要有项目就一定有要解决的问题)、你的某一个
项目里使用了什么技术后整体性能和并发量提升了很多等等。
面试和工作是两回事,聪明的人会把面试官往自己擅长的领域领,其他人则被面试官牵着鼻子走。虽说面试和工作是
两回事,但是你要想要获得自己满意的 o?er ,你自身的实力必须要强。
1.2.2 这3点你必须知道
1. 大部分公司的HR都说我们不看重学历(骗你的!),但是如果你的学校不出众的话,很难在一堆简历中脱颖而
出,除非你的简历上有特别的亮点,比如:某某大厂的实习经历、获得了某某大赛的奖等等。
2. 大部分应届生找工作的硬伤是没有工作经验或实习经历,所以如果你是应届生就不要错过秋招和春招。一旦错
过,你后面就极大可能会面临社招,这个时候没有工作经验的你可能就会面临各种碰壁,导致找不到一个好的
工作
3. 写在简历上的东西一定要慎重,这是面试官大量提问的地方;
4. 将自己的项目经历完美的展示出来非常重要。
1.2.3 你必须知道的两大法则
①STAR法则(Situation Task Action Result):
Situation: 事情是在什么情况下发生;
Task:: 你是如何明确你的任务的;
Action: 针对这样的情况分析,你采用了什么行动方式;
Result: 结果怎样,在这样的情况下你学习到了什么。
简而言之,STAR法则,就是一种讲述自己故事的方式,或者说,是一个清晰、条理的作文模板。不管是什么,合理
熟练运用此法则,可以轻松的对面试官描述事物的逻辑方式,表现出自己分析阐述问题的清晰性、条理性和逻辑性。
下面这段内容摘自百度百科,我觉得写的非常不错:
STAR法则,500强面试题回答时的技巧法则,备受面试者成功者和500强HR的推崇。 由于这个法则被广泛应用
于面试问题的回答,尽管我们还在写简历阶段,但是,写简历时能把面试的问题就想好,会使自己更加主动和
自信,做到简历,面试关联性,逻辑性强,不至于在一个月后去面试,却把简历里的东西都忘掉了(更何况有
些朋友会稍微夸大简历内容)。在我们写简历时,每个人都要写上自己的工作经历,活动经历,想必每一个同
学,都会起码花上半天甚至更长的时间去搜寻脑海里所有有关的经历,争取找出最好的东西写在简历上。但是
此时,我们要注意了,简历上的任何一个信息点都有可能成为日后面试时的重点提问对象,所以说,不能只管
写上让自己感觉最牛的经历就完事了,要想到今后,在面试中,你所写的经历万一被面试官问到,你真的能回
答得流利,顺畅,且能通过这段经历,证明自己正是适合这个职位的人吗?
②FAB 法则(Feature Advantage Bene?t):
Feature: 是什么;
Advantage: 比别人好在哪些地方;
Bene?t: 如果雇佣你,招聘方会得到什么好处。
简单来说,这个法则主要是让你的面试官知道你的优势、招了你之后对公司有什么帮助。1.2.4 项目经历怎么写?
简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。对于项目经历大家可以考虑
从如下几点来写:
简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。对于项目经历大家可以考虑
从如下几点来写:
1. 对项目整体设计的一个感受
2. 在这个项目中你负责了什么、做了什么、担任了什么角色
3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用
4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个
棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用redis做缓存提高
访问速度和并发量、使用消息队列削峰和降流等等。
1.2.5 专业技能该怎么写?
先问一下你自己会什么,然后看看你意向的公司需要什么。一般HR可能并不太懂技术,所以他在筛选简历的时候可
能就盯着你专业技能的关键词来看。对于公司有要求而你不会的技能,你可以花几天时间学习一下,然后在简历上可
以写上自己了解这个技能。比如你可以这样写:
Dubbo:精通
Spring:精通
Docker:掌握
SOA分布式开发 :掌握
Spring Cloud:了解
1.2.6 开源程序员简历模板分享
分享一个Github上开源的程序员简历模板。包括PHP程序员简历模板、iOS程序员简历模板、Android程序员简历模
板、Web前端程序员简历模板、Java程序员简历模板、CC++程序员简历模板、NodeJS程序员简历模板、架构师简历
模板以及通用程序员简历模板 。 Github地址:https:github.comgeekcompanyResumeSample
如果想学如何用 Markdown 写简历写一份高质量简历,请看这里:https:github.comSnailclimbJava-
Guideblobmaster面试必备手把手教你用Markdown写一份高质量的简历.md
1.2.7 其他的一些关于写简历的小tips
1. 尽量避免主观表述,少一点语义模糊的形容词,尽量要简洁明了,逻辑结构清晰。
2. 注意排版(不需要花花绿绿的),尽量使用Markdown语法。
3. 如果自己有博客或者个人技术栈点的话,写上去会为你加分很多。
4. 如果自己的Github比较活跃的话,写上去也会为你加分很多。
5. 注意简历真实性,一定不要写自己不会的东西,或者带有欺骗性的内容
6. 项目经历建议以时间倒序排序,另外项目经历不在于多,而在于有亮点。
7. 如果内容过多的话,不需要非把内容压缩到一页,保持排版干净整洁就可以了。
8. 简历最后最好能加上:“感谢您花时间阅读我的简历,期待能有机会和您共事。”这句话,显的你会很有礼貌。
1.3 如果面试官问你“你有什么问题问我吗?”时,你该如何回答 我还记得当时我去参加面试的时候,几乎每一场面试,特别是HR面和高管面的时候,面试官总是会在结尾问我:“问了
你这么多问题了,你有什么问题问我吗?”。这个时候很多人内心就会陷入短暂的纠结中:我该问吗?不问的话面试官
会不会对我影响不好?问什么问题?问这个问题会不会让面试官对我的影响不好啊?
1.3.1 这个问题对最终面试结果的影响到底大不大?
就技术面试而言,回答这个问题的时候,只要你不是触碰到你所面试的公司的雷区,那么我觉得这对你能不能拿到最
终o?er来说影响确实是不大的。我说这些并不代表你就可以直接对面试官说:“我没问题了。”,笔主当时面试的时候
确实也说过挺多次“没问题要问了。”,最终也没有导致笔主被pass掉(可能是前面表现比较好,哈哈,自恋一下)。
我现在回想起来,觉得自己当时做法其实挺不对的。面试本身就是一个双向选择的过程,你对这个问题的回答也会侧
面反映出你对这次面试的上心程度,你的问题是否有价值,也影响了你最终的选择与公司是否选择你。
面试官在技术面试中主要考察的还是你这样个人到底有没有胜任这个工作的能力以及你是否适合公司未来的发展需
要,很多公司还需要你认同它的文化,我觉得你只要不是太笨,应该不会栽在这里。除非你和另外一个人在能力上相
同,但是只能在你们两个人中选一个,那么这个问题才对你能不能拿到o?er至关重要。有准备总比没准备好,给面
试官留一个好的影响总归是没错的。
但是,就非技术面试来说,我觉得好好回答这个问题对你最终的结果还是比较重要的。
总的来说不管是技术面试还是非技术面试,如果你想赢得公司的青睐和尊重,我觉得我们都应该重视这个问题。
1.3.2 真诚一点,不要问太 Low 的问题
回答这个问题很重要的一点就是你没有必要放低自己的姿态问一些很虚或者故意讨好面试官的问题,也不要把自己从
面经上学到的东西照搬下来使用。面试官也不是傻子,特别是那种特别有经验的面试官,你是真心诚意的问问题,还
是从别处照搬问题来讨好面试官,人家可能一听就听出来了。总的来说,还是要真诚。除此之外,不要问太Low的问
题,会显得你整个人格局比较小或者说你根本没有准备(侧面反映你对这家公司不伤心,既然你不上心,为什么要要
你呢)。举例几个比较 Low 的问题,大家看看自己有没有问过其中的问题:
贵公司的主要业务是什么?(面试之前自己不知道提前网上查一下吗?)
贵公司的男女比例如何?(考虑脱单?记住你是来工作的!)
贵公司一年搞几次外出旅游?(你是来工作的,这些娱乐活动先别放在心上!)......
1.3.3 有哪些有价值的问题值得问?
针对这个问题。笔主专门找了几个专门做HR工作的小哥哥小姐姐们询问并且查阅了挺多前辈们的回答,然后结合自
己的实际经历,我概括了下面几个比较适合问的问题。
面对HR或者其他Level比较低的面试官时 1. 能不能谈谈你作为一个公司老员工对公司的感受? (这个问题比较容易回答,不会让面试官陷入无话可说的尴尬
境地。另外,从面试官的回答中你可以加深对这个公司的了解,让你更加清楚这个公司到底是不是你想的那样
或者说你是否能适应这个公司的文化。除此之外,这样的问题在某种程度上还可以拉进你与面试官的距离。)
2. 能不能问一下,你当时因为什么原因选择加入这家公司的呢或者说这家公司有哪些地方吸引你?有什么地方你觉
得还不太好或者可以继续完善吗? (类似第一个问题,都是问面试官个人对于公司的看法,)
3. 我觉得我这次表现的不是太好,你有什么建议或者评价给我吗?(这个是我常问的。我觉得说自己表现不好只是
这个语境需要这样来说,这样可以显的你比较谦虚好学上进。)
4. 接下来我会有一段空档期,有什么值得注意或者建议学习的吗? (体现出你对工作比较上心,自助学习意识比
较强。)
5. 这个岗位为什么还在招人? (岗位真实性和价值咨询)
6. 大概什么时候能给我回复呢? (终面的时候,如果面试官没有说的话,可以问一下)
7. ......
面对部门领导
1. 部门的主要人员分配以及对应的主要工作能简单介绍一下吗?
2. 未来如果我要加入这个团队,你对我的期望是什么? (部门领导一般情况下是你的直属上级了,你以后和他打
交道的机会应该是最多的。你问这个问题,会让他感觉你是一个对他的部门比较上心,比较有团体意识,并且
愿意倾听的候选人。)
3. 公司对新入职的员工的培养机制是什么样的呢? (正规的公司一般都有培养机制,提前问一下是对你自己的负
责也会显的你比较上心)
4. 以您来看,这个岗位未来在公司内部的发展如何? (在我看来,问这个问题也是对你自己的负责吧,谁不想发展
前景更好的岗位呢?)
5. 团队现在面临的最大挑战是什么? (这样的问题不会暴露你对公司的不了解,并且也能让你对未来工作的挑战或
困难有一个提前的预期。)
面对Level比较高的(比如总裁,老板)
1. 贵公司的发展目标和方向是什么? (看下公司的发展是否满足自己的期望)
2. 与同行业的竞争者相比,贵公司的核心竞争优势在什么地方? (充分了解自己的优势和劣势)
3. 公司现在面临的最大挑战是什么?
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: 我没有实习经历的话找工作是不是特别艰难?
没有实习经历没关系,只要你有拿得出手的项目或者大赛经历的话,你依然有可能拿到大厂的 o?er 。笔主当时
找工作的时候就没有实习经历以及大赛获奖经历,单纯就是凭借自己的项目经验撑起了整个面试。
如果你既没有实习经历,又没有拿得出手的项目或者大赛经历的话,我觉得在简历关,除非你有其他特别的亮
点,不然,你应该就会被刷。
Question4: 我该如何准备面试呢?面试的注意事项有哪些呢?
下面是我总结的一些准备面试的Tips以及面试必备的注意事项:
1. 准备一份自己的自我介绍,面试的时候根据面试对象适当进行修改(突出重点,突出自己的优势在哪里,切忌
流水账);
2. 注意随身带上自己的成绩单和简历复印件; (有的公司在面试前都会让你交一份成绩单和简历当做面试中的参
考。)
3. 如果需要笔试就提前刷一些笔试题,大部分在线笔试的类型是选择题+编程题,有的还会有简答题。(平时空闲
时间多的可以刷一下笔试题目(牛客网上有很多),但是不要只刷面试题,不动手code,程序员不是为了考试
而存在的。)另外,注意抓重点,因为题目太多了,但是有很多题目几乎次次遇到,像这样的题目一定要搞
定。4. 提前准备技术面试。 搞清楚自己面试中可能涉及哪些知识点、那些知识点是重点。面试中哪些问题会被经常问
到、自己改如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方
式的学习很难坚持下去!)
5. 面试之前做好定向复习。 也就是专门针对你要面试的公司来复习。比如你在面试之前可以在网上找找有没有你
要面试的公司的面经。
6. 准备好自己的项目介绍。 如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你
可以从下面几个方向来考虑:①对项目整体设计的一个感受(面试官可能会让你画系统的架构图;②在这个项
目中你负责了什么、做了什么、担任了什么角色;③ 从这个项目中你学会了那些东西,使用到了那些技术,学
会了那些新技术的使用;④项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开
发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能
比如:用redis做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。
7. 面试之后记得复盘。 面试遭遇失败是很正常的事情,所以善于总结自己的失败原因才是最重要的。如果失败,不要灰心;如果通过,切勿狂喜。
一些还算不错的 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 和 StringBu?er、StringBuilder 的区别是什么?String 为什
么是不可变的?
可变性
简单的来说:String 类中使用 ?nal 关键字字符数组保存字符串,private final char value[],所以 String
对象是不可变的。而StringBuilder 与 StringBu?er 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder
中也是使用字符数组保存字符串char[]value 但是没有用 ?nal 关键字修饰,所以这两种对象都是可变的。
StringBuilder 与 StringBu?er 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自
行查阅源码。
AbstractStringBuilder.java
线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与
StringBu?er 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共
方法。StringBu?er 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对
方法进行加同步锁,所以是非线程安全的。
性能
abstract class AbstractStringBuilder implements Appendable, CharSequence {
?char[] value;
?int count;
?AbstractStringBuilder {
}
?AbstractStringBuilder(int capacity) {
? ? ?value = new char[capacity];
}每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。
StringBu?er 每次都会对 StringBu?er 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用
StirngBuilder 相比使用 StringBu?er 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
1. 操作少量的数据 = String
2. 单线程操作字符串缓冲区下操作大量数据 = StringBuilder
3. 多线程操作字符串缓冲区下操作大量数据 = StringBu?er
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(aa==bb);
? ? ?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 关于 ?nal 关键字的一些总结
nal关键字主要用在三个地方:变量、方法、类。
1. 对于一个?nal变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的
变量,则在对其初始化之后便不能再让其指向另一个对象。
2. 当用?nal修饰一个类时,表明这个类不能被继承。?nal类中的所有成员方法都会被隐式地指定为?nal方法。
3. 使用?nal方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。
在早期的Java实现版本中,会将?nal方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的
任何性能提升(现在的Java版本已经不需要使用?nal方法进行这些优化了)。类中所有的private方法都隐式地
指定为?anl。
2.1.6 Object类的常见方法总结
Object类是一个特殊的类,是所有类的父类。它主要提供了以下11个方法:
2.1.7 Java 中的异常处理
public final native Class> getClassnative方法,用于返回当前运行时对象的Class对象,使用了
final关键字修饰,故不允许子类重写。
public native int hashCode native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的
HashMap。
public boolean equals(Object obj)用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户
比较字符串的值是否相等。
protected native Object clone throws CloneNotSupportedExceptionnaitive方法,用于创建并返回
当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone != x 为true,x.clone.getClass
== x.getClass 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生
CloneNotSupportedException异常。
public String toString返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方
法。
public final native void notifynative方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视
器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notifyAllnative方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒
在此对象监视器上等待的所有线程,而不是一个线程。
public final native void wait(long timeout) throws InterruptedExceptionnative方法,并且不能
重写。暂停线程的执行。注意: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块,则必须跟一个?nally块。catch 块:用于处理try捕获到的异常。
nally 块:无论是否捕获或处理异常,?nally块里的语句都会被执行。当在try块或catch块中遇到return语句
时,?nally语句块将在方法返回之前被执行。
在以下4种特殊情况下,?nally块不会被执行:
1. 在?nally语句块中发生了异常。
2. 在前面的代码中用了System.exit退出程序。
3. 程序所在的线程死亡。
4. 关闭CPU。
2.1.8 获取用键盘输入常用的的两种方法
方法1:通过 Scanner
方法2:通过 Bu?eredReader
2.1.9 接口和抽象类的区别是什么
1. 接口的方法默认是 public,所有方法在接口中不能有实现( Java 8 开始接口方法可以有默认实现),抽象类可以
有非抽象的方法
2. 接口中的实例变量默认是 ?nal 类型的,而抽象类中则不一定
3. 一个类可以实现多个接口,但最多只能实现一个抽象类
4. 一个类实现接口的话要实现接口的所有方法,而抽象类不一定
5. 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计层面来说,抽象是对类的抽
象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
备注:在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现
两个接口,接口中定义了一样的默认方法,必须重写,不然会报错。(详见
issue:https:github.comSnailclimbJavaGuideissues146)
2.2 Java 集合框架
2.2.1 Arraylist 与 LinkedList 异同
1. 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
2. 底层数据结构: 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; 3. 插入和删除是否受元素位置的影响: ① 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)。
4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通
过元素的序号快速获取元素对象(对应于get(int index)方法)。
5. 内存空间占用: ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空
间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数
据)。
补充内容:RandomAccess接口
查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过是一个
标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。
在binarySearch方法中,它要判断传入的list 是否RamdomAccess的实例,如果是,调用
indexedBinarySearch方法,如果不是,那么调用iteratorBinarySearch方法
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 底层使用的是双向循环链表数据结构。
public interface RandomAccess {
}
?public static
?int binarySearch(List extends Comparable super T>> list, T key) {
? ? ?if (list instanceof RandomAccess || list.size
? ? ? ? ?return Collections.indexedBinarySearch(list, key);
? ? ?else
? ? ? ? ?return Collections.iteratorBinarySearch(list, key);
}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.comp21673805
2.2.4 HashMap 和 Hashtable 的区别
1. 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过
synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
2. 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在
代码中使用它;
3. 对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的幂次方。
5. 底层数据结构: 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.comchengxiaop6842045.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
1. List
Arraylist: Object数组
Vector: Object数组
LinkedList: 双向链表( JDK1.6之前为循环链表,JDK1.7取消了循环) 详细可阅读JDK1.7-LinkedList循环链表优
化
2. 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 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; 这段代码其实是分
为三步执行:
1. 为 uniqueInstance 分配内存空间
2. 初始化 uniqueInstance
3. 将 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?nally 语句块来完成),所以我们可以通过查看它的源代码,来看
它是如何实现的。
③ ReenTrantLock 比 synchronized 增加了一些高级功能
相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;
③可实现选择性通知(锁可以绑定多个条件)ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly来实现这个机制。也
就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等
待的线程先获得锁。 ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的
ReentrantLock(boolean fair)构造方法来制定是否是公平的。
synchronized关键字与wait和notifynotifyAll方法相结合可以实现等待通知机制,ReentrantLock类当然也
可以实现,但是需要借助于Condition接口与newCondition 方法。Condition是JDK1.5之后才有的,它具有很
好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视
器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵
活。 在使用notifynotifyAll方法进行通知时,被通知的线程是由 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方法的区别是什么呢?
1) 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 类的 objectFieldO?set 方法
是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueO?set。另外 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(共享):多个线程可同时执行,如SemaphoreCountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某
一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方
式即可,至于具体线程等待队列的维护(如获取资源失败入队唤醒出队等),AQS已经在顶层实现好了。
5.2.3 AQS底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应
用):
1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源
state的获取和释放)
2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:
默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且
通常应该简短而不是阻塞。AQS类中的其他方法都是?nal ,所以无法被其他类使用,只有这几个方法可以被其他类
使用。
返回同步状态的当前值
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.comwaterystonep4920797.html
https:www.cnblogs.comchengxiaoarchive201707247141160.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解析
2. TCP连接
3. 发送HTTP请求
4. 服务器处理请求并返回HTTP报文
5. 浏览器解析渲染页面
6. 连接结束
具体可以参考下面这篇文章:
https:segmentfault.coma1190000006879700
3.3 各种协议与HTTP协议之间的关系
一般面试官会通过这样的问题来考察你对计算机网络知识体系的理解。
图片来源:《图解HTTP》3.4 HTTP长连接、短连接
在HTTP1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中
断连接。当客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源(如JavaScript文件、图像
文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话。
而从HTTP1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:
在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客
户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时
间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。
HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。
—— 《HTTP长连接、短连接究竟是什么?》
3.5 TCP 三次握手和四次挥手(面试常客)
为了准确无误地把数据送达目标处,TCP协议采用了三次握手策略。
漫画图解:
图片来源:《图解HTTP》
Connection:keep-alive简单示意图:
客户端–发送带有 SYN 标志的数据包–一次握手–服务端
服务端–发送带有 SYNACK 标志的数据包–二次握手–客户端
客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端
为什么要三次握手?
三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是
双方确认自己与对方的发送与接收是正常的。
第一次握手:Client 什么都不能确认;Server 确认了对方发送正常
第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己接收正常,对方发
送正常
第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送接收正常
所以三次握手就能确认双发收发功能都正常,缺一不可。
为什么要传回 SYN
接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。
SYN 是 TCPIP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 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.netqzcsuarticledetails72861891
四 Linux 4.1 简单介绍一下 Linux 文件系统?
Linux文件系统简介
在Linux操作系统中,所有被操作系统管理的资源,例如网络接口卡、磁盘驱动器、打印机、输入输出设备、普通文
件或是目录都被看作是一个文件。
也就是说在LINUX系统中有一个重要的概念:一切都是文件。其实这是UNIX哲学的一个体现,而Linux是重写UNIX而
来,所以这个概念也就传承了下来。在UNIX系统中,把一切资源都看作是文件,包括硬件设备。UNIX系统把每个硬
件都看成是一个文件,通常称为设备文件,这样用户就可以用读写文件的方式实现对硬件的访问。
文件类型与目录结构
Linux支持5种文件类型 :
Linux的目录结构如下:Linux文件系统的结构层次鲜明,就像一棵倒立的树,最顶层是其根目录:
常见目录说明:
bin: 存放二进制可执行文件(ls,cat,mkdir等),常用命令一般都在这里;
etc: 存放系统管理和配置文件;
home: 存放所有用户文件的根目录,是用户主目录的基点,比如用户user的主目录就是homeuser,可以
用~user表示;
usr : 用于存放系统应用程序;
opt: 额外安装的可选应用程序包所放置的位置。一般情况下,我们可以把tomcat等都安装到这里;
proc: 虚拟文件系统目录,是系统内存的映射。可直接访问这个目录来获取系统信息;
root: 超级用户(系统管理员)的主目录(特权阶级^o^);
sbin: 存放二进制可执行文件,只有root才能访问。这里存放的是系统管理员使用的系统级别的管理命令和程
序。如ifcon?g等;
dev: 用于存放设备文件;
mnt: 系统管理员安装临时文件系统的安装点,系统提供这个目录是让用户临时挂载其他的文件系统;
boot: 存放用于系统引导时使用的各种文件;
lib : 存放着和系统运行相关的库文件 ;
tmp: 用于存放各种临时文件,是公用的临时文件存储点;
var: 用于存放运行时需要改变数据的文件,也是某些大文件的溢出区,比方说各种服务的日志文件(系统启
动日志等。)等;
lost+found: 这个目录平时是空的,系统非正常关机而留下“无家可归”的文件(windows下叫什么.chk)就在
这里。
4.2 一些常见的 Linux 命令了解吗?
目录切换命令
cd usr: 切换到该目录下usr目录
cd ..(或cd..): 切换到上一层目录
cd : 切换到系统根目录
cd ~: 切换到用户主目录
cd -: 切换到上一个所在目录
目录的操作命令(增删改查)
1. mkdir 目录名称: 增加目录2. ls或者ll(ll是ls -l的缩写,ll命令以看到该目录下的所有目录和文件的详细信息):查看目录信息
3. find 目录 参数: 寻找目录(查)
4. mv 目录名称 新目录名称: 修改目录的名称(改)
注意:mv的语法不仅可以对目录进行重命名而且也可以对各种文件,压缩包等进行 重命名的操作。mv命令用
来对文件或目录重新命名,或者将文件从一个目录移到另一个目录中。后面会介绍到mv命令的另一个用法。
5. mv 目录名称 目录的新位置: 移动目录的位置---剪切(改)
注意:mv语法不仅可以对目录进行剪切操作,对文件和压缩包等都可执行剪切操作。另外mv与cp的结果不
同,mv好像文件“搬家”,文件个数并未增加。而cp对文件进行复制,文件个数增加了。
6. cp -r 目录名称 目录拷贝的目标位置: 拷贝目录(改),-r代表递归拷贝
注意:cp命令不仅可以拷贝目录还可以拷贝文件,压缩包等,拷贝文件和压缩包时不 用写-r递归
7. rm [-rf] 目录 : 删除目录(删)
注意:rm不仅可以删除目录,也可以删除其他文件或压缩包,为了增强大家的记忆, 无论删除任何目录或文
件,都直接使用rm -rf 目录文件压缩包
文件的操作命令(增删改查)
1. touch 文件名称 : 文件的创建(增)
2. catmorelesstail 文件名称 文件的查看(查)
cat: 只能显示最后一屏内容
more: 可以显示百分比,回车可以向下一行, 空格可以向下一页,q可以退出查看
less: 可以使用键盘上的PgUp和PgDn向上 和向下翻页,q结束查看
tail-10 : 查看文件的后10行,Ctrl+C结束
注意:命令 tail -f 文件 可以对某个文件进行动态监控,例如tomcat的日志文件, 会随着程序的运行,日志会变
化,可以使用tail -f catalina-2016-11-11.log 监控 文 件的变化
3. vim 文件: 修改文件的内容(改)
vim编辑器是Linux中的强大组件,是vi编辑器的加强版,vim编辑器的命令和快捷方式有很多,但此处不一一阐
述,大家也无需研究的很透彻,使用vim编辑修改文件的方式基本会使用就可以了。
在实际开发中,使用vim编辑器主要作用就是修改配置文件,下面是一般步骤:
vim 文件------>进入文件----->命令模式------>按i进入编辑模式----->编辑文件 ------->按Esc进入底行模式----->输
入:wqq! (输入wq代表写入内容并退出,即保存;输入q!代表强制退出不保存。)
4. 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杀掉
网络通信命令:
查看当前系统的网卡信息:ifcon?g
查看与某台机器的连接情况: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来说,则没有这种缓存。
2. 是否支持事务和崩溃后的安全恢复: MyISAM 强调的是性能,每次查询具有原子性,其执行数度比InnoDB类型
更快,但是不提供事务支持。但是InnoDB 提供事务支持事务,外部键等高级数据库功能。 具有事务
(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID
compliant))型表。
3. 是否支持外键: 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语句,默认会这样
做:1. 定位到记录所在的页:需要遍历双向链表,找到所在的页
2. 从所在的页内中查找相应的记录:由于不是根据主键查询,只能遍历所在页的单链表了
很明显,在数据量很大的情况下这样查找会很慢!这样的时间复杂度为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 TABLE `table_name` ADD PRIMARY KEY ( `column` )
ALTER TABLE `table_name` ADD UNIQUE ( `column` )
ALTER TABLE `table_name` ADD INDEX index_name ( `column` )
ALTER TABLE `table_name` ADD FULLTEXT ( `column`)
ALTER TABLE `table_name` ADD INDEX index_name ( `column1`, `column2`, `column3` )5.3 当MySQL单表记录数过大时,数据库的CRUD性能会明显下
降,一些常见的优化措施如下:
当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下:
1. 限定数据的范围: 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时
候,我们可以控制在一个月的范围内。;
2. 读写分离: 经典的数据库拆分方案,主库负责写,从库负责读;
3. 垂直分区: 根据数据库里面数据表的相关性进行拆分。 例如,用户表中既有用户的登录信息又有用户的基本信
息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。 如下图所示,这样来说大家应该就更容易理解了。
垂直拆分的优点: 可以使得行数据变小,在查询时减少读取的Block数,减少IO次数。此外,垂直分区可以简
化表的结构,易于维护。垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过
在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂;
4. 水平分区: 保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达
到了分布式的目的。 水平拆分可以支撑非常大的数据量。 水平拆分是指数据表行的拆分,表的行数超过200万
行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多
个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。
水平拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的
数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 水平拆分最好分库 。水平拆分能
够 支持非常大的数据量存储,应用端改造也少,但 分片事务难以解决 ,跨界点Join性能较差,逻辑复杂。
《Java工程师修炼之道》的作者推荐 尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂
度 ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择
客户端分片架构,这样可以减少一次和中间件的网络IO。
下面补充一下数据库分片的两种常见方案:客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的 Sharding-
JDBC 、阿里的TDDL是两种比较常用的实现。
中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat
、360的Atlas、网易的DDB等等都是这种架构的实现。
5.4 事务隔离级别(图文详解)
什么是事务?
事务是逻辑上的一组操作,要么都执行,要么都不执行。
事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就
是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩
溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要
失败。
事物的特性(ACID)
1. 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
2. 一致性: 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
3. 隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
4. 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影
响。
并发事务带来的问题
在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对统一数据进行操
作)。并发虽然是必须的,但可能会导致以下的问题。
脏读(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.comdocrefman5.7en
Mysql 锁:灵魂七拷问
Innodb 中的事务隔离级别和锁的关系
六 Redis
6.1 redis 简介
简单来说 redis 就是一个数据库,不过与传统数据库不同的是 redis 的数据是存在内存中的,所以存写速度非常快,因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业
务场景。除此之外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。
6.2 为什么要用 redis 为什么要用缓存
主要从“高性能”和“高并发”这两点来看待这个问题。
高性能:
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数
缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当
快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!高并发:
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中
去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。6.3 为什么要用 redis 而不用 mapguava 做缓存?
下面的内容来自 segmentfault 一位网友的提问,地址:https:segmentfault.comq1010000009106416
缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是
轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓
存不具有一致性。
使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致
性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。
6.4 redis 和 memcached 的区别
对于 redis 和 memcached 我总结了下面四点。现在公司一般都是用 redis 来实现缓存,而且 redis 自身也越来越强
大了!
1. redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的kv类型的数据,同时还提供
list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
2. Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而
Memecache把数据全部存在内存之中。
3. 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前
是原生支持 cluster 模式的.
4. Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。
来自网络上的一张图,这里分享给大家!6.5 redis 常见数据结构以及使用场景分析
1. String
常用命令: set,get,decr,incr,mget 等。
String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 常规key-value缓存应用;
常规计数:微博数,粉丝数等。
2.Hash
常用命令: hget,hset,hgetall 等。
Hash 是一个 string 类型的 ?eld 和 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 可以存活的
时间。 ......
2019-2-27 v 1.0 初版发布
2019-3-2 v 2.0 对于第一版进行了大幅度更新,除了修改了一些小错误之外,还增加了一些内容。
2019-4-18 v3.0 修复错误,完善内容,增加了少部分内容。
必看
本文档由 SnailClimb 整理,文章大部分内容来源于本人的开源项目 JavaGuide,你可以把这个文档看做JavaGuide
的精简版,适合面试前的突击。更多精彩内容,欢迎关注我的公众号:JavaGuide。如需转载对应的文章,请附上下
面一段内容:
本文转载自JavaGuide,地址:https:github.comSnailclimbJavaGuide,作者:SnailClimb
历史更新记录
建议阅读本文档的方式
本文档提供详细的目录,建议大家使用电脑阅读。如果大家用手机阅读的话,可以下载一个不错的PDF阅读器,比如
很多人常用的福昕PDF阅读器。
本文档提供详细的目录,大家可以根据自己的实际需要选择自己薄弱的知识章节阅读。
前言
不论是校招还是社招都避免不了各种面试、笔试,如何去准备这些东西就显得格外重要。不论是笔试还是面试都是有
章可循的,我这个“有章可循”说的意思只是说应对技术面试是可以提前准备。
运筹帷幄之后,决胜千里之外!不打毫无准备的仗,我觉得大家可以先从下面几个方面来准备面试:1. 自我介绍。(你可千万这样介绍:“我叫某某,性别,来自哪里,学校是那个,自己爱干什么”,记住:多说点简
历上没有的,多说点自己哪里比别人强!)
2. 自己面试中可能涉及哪些知识点、那些知识点是重点。
3. 面试中哪些问题会被经常问到、面试中自己改如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多
少?能记住多久?第二:背题的方式的学习很难坚持下去!)
4. 自己的简历该如何写。
“80%的o?er掌握在20%的人手中” 这句话也不是不无道理的。决定你面试能否成功的因素中实力固然占有很大一部
分比例,但是如果你的心态或者说运气不好的话,依然无法拿到满意的 o?er。运气暂且不谈,就拿心态来说,千万
不要因为面试失败而气馁或者说怀疑自己的能力,面试失败之后多总结一下失败的原因,后面你就会发现自己会越来
越强大。
另外,大家要明确的很重要的几点是:
1. 写在简历上的东西一定要慎重,这可能是面试官大量提问的地方;
2. 大部分应届生找工作的硬伤是没有工作经验或实习经历;
3. 将自己的项目经历完美的展示出来非常重要。
笔主能力有限,如果有不对的地方或者和你想法不同的地方,敬请雅正、不舍赐教。
一 面试前的准备
1.1 如何准备一场面试
不论是校招还是社招都避免不了各种面试、笔试,如何去准备这些东西就显得格外重要。不论是笔试还是面试都是有
章可循的,我这个“有章可循”说的意思只是说应对技术面试是可以提前准备。 我其实特别不喜欢那种临近考试就提前
背啊记啊各种题的行为,非常反对!我觉得这种方法特别极端,而且在稍有一点经验的面试官面前是根本没有用的。
建议大家还是一步一个脚印踏踏实实地走。
1.1.1 如何获取大厂面试机会?
在讲如何获取大厂面试机会之前,先来给大家科普对比一下两个校招非常常见的概念——春招和秋招。
1. 招聘人数 :秋招多于春招 ;
2. 招聘时间 : 秋招一般7月左右开始,大概一直持续到10月底。但是大厂(如BAT)都会早开始早结束,所以一
定要把握好时间。春招最佳时间为3月,次佳时间为4月,进入5月基本就不会再有春招了(金三银四)。
3. 应聘难度 :秋招略大于春招;
4. 招聘公司: 秋招数量多,而春招数量较少,一般为秋招的补充。
综上,一般来说,秋招的含金量明显是高于春招的。
下面我就说一下我自己知道的一些方法,不过应该也涵盖了大部分获取面试机会的方法。
1. 关注大厂官网,随时投递简历(走流程的网申);
2. 线下参加宣讲会,直接投递简历;
3. 找到师兄师姐认识的人,帮忙内推(能够让你避开网申简历筛选,笔试筛选,还是挺不错的,不过也还是需要
你的简历够棒);
4. 博客发文被看中Github优秀开源项目作者,大厂内部人员邀请你面试;
5. 求职类网站投递简历(不是太推荐,适合海投);除了这些方法,我也遇到过这样的经历:有些大公司的一些部门可能暂时没招够人,然后如果你的亲戚或者朋友刚好
在这个公司,而你正好又在寻求o?er,那么面试机会基本上是有了,而且这种面试的难度好像一般还普遍比其他正
规面试低很多。
1.1.2 面试必知
下面几点概括起来就是:了解自己的能力、要应聘的公司、自己要应聘的岗位,提前做好自己我介绍以及项目介绍等
等方面的功课,确保你能在面试过程中简短清晰的回答出来(可以用Star法则来组织自己的语言)。
1) 准备自己的自我介绍
从HR面、技术面到高管面部门主管面,面试官一般会让你先自我介绍一下,所以好好准备自己的自我介绍真的非常
重要。网上一般建议的是准备好两份自我介绍:一份对hr说的,主要讲能突出自己的经历,会的编程技术一语带过;
另一份对技术面试官说的,主要讲自己会的技术细节,项目经验,经历那些就一语带过。
我这里简单分享一下我自己的自我介绍的一个简单的模板吧:
面试官,您好!我叫某某。大学时间我主要利用课外时间学习某某。在校期间参与过一个某某系统的开发,另
外,自己学习过程中也写过很多系统比如某某系统。在学习之余,我比较喜欢通过博客整理分享自己所学知
识。我现在是某某社区的认证作者,写过某某很不错的文章。另外,我获得过某某奖,我的Github上开源的某个
项目已经有多少Star了。
2) 关于着装
穿西装、打领带、小皮鞋?NO!NO!NO!这是互联网公司面试又不是去走红毯,所以你只需要穿的简单大方就
好,不需要太正式。
3) 随身带上自己的成绩单和简历
有的公司在面试前都会让你交一份成绩单和简历当做面试中的参考。
4) 如果需要笔试就提前刷一些笔试题
平时空闲时间多的可以刷一下笔试题目(牛客网上有很多)。但是不要只刷面试题,不动手code,程序员不是为了
考试而存在的。
5) 花时间一些逻辑题
面试中发现有些公司都有逻辑题测试环节,并且都把逻辑笔试成绩作为很重要的一个参考。
6) 准备好自己的项目介绍
如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑:
1. 对项目整体设计的一个感受(面试官可能会让你画系统的架构图)
2. 在这个项目中你负责了什么、做了什么、担任了什么角色
3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用
4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个
棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用redis做缓存提高
访问速度和并发量、使用消息队列削峰和降流等等。
7) 提前了解公司以及要应聘的岗位 面试之前一定要提前对要应聘的公司以及岗位有所了解,这一点对于喜欢海投的同学来说要格外注意。如果你去一个
公司面试连公司的主要业务或者主要产品都不了解的话,那么面试官打心里肯定会觉得你并没有很重视他们公司,所
以他们为什么要重视你呢?你也要提前了解你所要应聘岗位对你的专业能力或者其他能力的要求,比如有的岗位就是
需要英语水平比较高,需要你通过六级或者托福雅思,假如你不满足的话,那就没必要再去投递简历面试了。
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 内存淘汰机制等等。
所以,首先,你要明确的一点是:你不会的东西就不要写在简历上。另外,你要考虑你该如何才能让你的亮点在简历
中凸显出来,比如:你在某某项目做了什么事情解决了什么问题(只要有项目就一定有要解决的问题)、你的某一个
项目里使用了什么技术后整体性能和并发量提升了很多等等。
面试和工作是两回事,聪明的人会把面试官往自己擅长的领域领,其他人则被面试官牵着鼻子走。虽说面试和工作是
两回事,但是你要想要获得自己满意的 o?er ,你自身的实力必须要强。
1.2.2 这3点你必须知道
1. 大部分公司的HR都说我们不看重学历(骗你的!),但是如果你的学校不出众的话,很难在一堆简历中脱颖而
出,除非你的简历上有特别的亮点,比如:某某大厂的实习经历、获得了某某大赛的奖等等。
2. 大部分应届生找工作的硬伤是没有工作经验或实习经历,所以如果你是应届生就不要错过秋招和春招。一旦错
过,你后面就极大可能会面临社招,这个时候没有工作经验的你可能就会面临各种碰壁,导致找不到一个好的
工作
3. 写在简历上的东西一定要慎重,这是面试官大量提问的地方;
4. 将自己的项目经历完美的展示出来非常重要。
1.2.3 你必须知道的两大法则
①STAR法则(Situation Task Action Result):
Situation: 事情是在什么情况下发生;
Task:: 你是如何明确你的任务的;
Action: 针对这样的情况分析,你采用了什么行动方式;
Result: 结果怎样,在这样的情况下你学习到了什么。
简而言之,STAR法则,就是一种讲述自己故事的方式,或者说,是一个清晰、条理的作文模板。不管是什么,合理
熟练运用此法则,可以轻松的对面试官描述事物的逻辑方式,表现出自己分析阐述问题的清晰性、条理性和逻辑性。
下面这段内容摘自百度百科,我觉得写的非常不错:
STAR法则,500强面试题回答时的技巧法则,备受面试者成功者和500强HR的推崇。 由于这个法则被广泛应用
于面试问题的回答,尽管我们还在写简历阶段,但是,写简历时能把面试的问题就想好,会使自己更加主动和
自信,做到简历,面试关联性,逻辑性强,不至于在一个月后去面试,却把简历里的东西都忘掉了(更何况有
些朋友会稍微夸大简历内容)。在我们写简历时,每个人都要写上自己的工作经历,活动经历,想必每一个同
学,都会起码花上半天甚至更长的时间去搜寻脑海里所有有关的经历,争取找出最好的东西写在简历上。但是
此时,我们要注意了,简历上的任何一个信息点都有可能成为日后面试时的重点提问对象,所以说,不能只管
写上让自己感觉最牛的经历就完事了,要想到今后,在面试中,你所写的经历万一被面试官问到,你真的能回
答得流利,顺畅,且能通过这段经历,证明自己正是适合这个职位的人吗?
②FAB 法则(Feature Advantage Bene?t):
Feature: 是什么;
Advantage: 比别人好在哪些地方;
Bene?t: 如果雇佣你,招聘方会得到什么好处。
简单来说,这个法则主要是让你的面试官知道你的优势、招了你之后对公司有什么帮助。1.2.4 项目经历怎么写?
简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。对于项目经历大家可以考虑
从如下几点来写:
简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。对于项目经历大家可以考虑
从如下几点来写:
1. 对项目整体设计的一个感受
2. 在这个项目中你负责了什么、做了什么、担任了什么角色
3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用
4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个
棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用redis做缓存提高
访问速度和并发量、使用消息队列削峰和降流等等。
1.2.5 专业技能该怎么写?
先问一下你自己会什么,然后看看你意向的公司需要什么。一般HR可能并不太懂技术,所以他在筛选简历的时候可
能就盯着你专业技能的关键词来看。对于公司有要求而你不会的技能,你可以花几天时间学习一下,然后在简历上可
以写上自己了解这个技能。比如你可以这样写:
Dubbo:精通
Spring:精通
Docker:掌握
SOA分布式开发 :掌握
Spring Cloud:了解
1.2.6 开源程序员简历模板分享
分享一个Github上开源的程序员简历模板。包括PHP程序员简历模板、iOS程序员简历模板、Android程序员简历模
板、Web前端程序员简历模板、Java程序员简历模板、CC++程序员简历模板、NodeJS程序员简历模板、架构师简历
模板以及通用程序员简历模板 。 Github地址:https:github.comgeekcompanyResumeSample
如果想学如何用 Markdown 写简历写一份高质量简历,请看这里:https:github.comSnailclimbJava-
Guideblobmaster面试必备手把手教你用Markdown写一份高质量的简历.md
1.2.7 其他的一些关于写简历的小tips
1. 尽量避免主观表述,少一点语义模糊的形容词,尽量要简洁明了,逻辑结构清晰。
2. 注意排版(不需要花花绿绿的),尽量使用Markdown语法。
3. 如果自己有博客或者个人技术栈点的话,写上去会为你加分很多。
4. 如果自己的Github比较活跃的话,写上去也会为你加分很多。
5. 注意简历真实性,一定不要写自己不会的东西,或者带有欺骗性的内容
6. 项目经历建议以时间倒序排序,另外项目经历不在于多,而在于有亮点。
7. 如果内容过多的话,不需要非把内容压缩到一页,保持排版干净整洁就可以了。
8. 简历最后最好能加上:“感谢您花时间阅读我的简历,期待能有机会和您共事。”这句话,显的你会很有礼貌。
1.3 如果面试官问你“你有什么问题问我吗?”时,你该如何回答 我还记得当时我去参加面试的时候,几乎每一场面试,特别是HR面和高管面的时候,面试官总是会在结尾问我:“问了
你这么多问题了,你有什么问题问我吗?”。这个时候很多人内心就会陷入短暂的纠结中:我该问吗?不问的话面试官
会不会对我影响不好?问什么问题?问这个问题会不会让面试官对我的影响不好啊?
1.3.1 这个问题对最终面试结果的影响到底大不大?
就技术面试而言,回答这个问题的时候,只要你不是触碰到你所面试的公司的雷区,那么我觉得这对你能不能拿到最
终o?er来说影响确实是不大的。我说这些并不代表你就可以直接对面试官说:“我没问题了。”,笔主当时面试的时候
确实也说过挺多次“没问题要问了。”,最终也没有导致笔主被pass掉(可能是前面表现比较好,哈哈,自恋一下)。
我现在回想起来,觉得自己当时做法其实挺不对的。面试本身就是一个双向选择的过程,你对这个问题的回答也会侧
面反映出你对这次面试的上心程度,你的问题是否有价值,也影响了你最终的选择与公司是否选择你。
面试官在技术面试中主要考察的还是你这样个人到底有没有胜任这个工作的能力以及你是否适合公司未来的发展需
要,很多公司还需要你认同它的文化,我觉得你只要不是太笨,应该不会栽在这里。除非你和另外一个人在能力上相
同,但是只能在你们两个人中选一个,那么这个问题才对你能不能拿到o?er至关重要。有准备总比没准备好,给面
试官留一个好的影响总归是没错的。
但是,就非技术面试来说,我觉得好好回答这个问题对你最终的结果还是比较重要的。
总的来说不管是技术面试还是非技术面试,如果你想赢得公司的青睐和尊重,我觉得我们都应该重视这个问题。
1.3.2 真诚一点,不要问太 Low 的问题
回答这个问题很重要的一点就是你没有必要放低自己的姿态问一些很虚或者故意讨好面试官的问题,也不要把自己从
面经上学到的东西照搬下来使用。面试官也不是傻子,特别是那种特别有经验的面试官,你是真心诚意的问问题,还
是从别处照搬问题来讨好面试官,人家可能一听就听出来了。总的来说,还是要真诚。除此之外,不要问太Low的问
题,会显得你整个人格局比较小或者说你根本没有准备(侧面反映你对这家公司不伤心,既然你不上心,为什么要要
你呢)。举例几个比较 Low 的问题,大家看看自己有没有问过其中的问题:
贵公司的主要业务是什么?(面试之前自己不知道提前网上查一下吗?)
贵公司的男女比例如何?(考虑脱单?记住你是来工作的!)
贵公司一年搞几次外出旅游?(你是来工作的,这些娱乐活动先别放在心上!)......
1.3.3 有哪些有价值的问题值得问?
针对这个问题。笔主专门找了几个专门做HR工作的小哥哥小姐姐们询问并且查阅了挺多前辈们的回答,然后结合自
己的实际经历,我概括了下面几个比较适合问的问题。
面对HR或者其他Level比较低的面试官时 1. 能不能谈谈你作为一个公司老员工对公司的感受? (这个问题比较容易回答,不会让面试官陷入无话可说的尴尬
境地。另外,从面试官的回答中你可以加深对这个公司的了解,让你更加清楚这个公司到底是不是你想的那样
或者说你是否能适应这个公司的文化。除此之外,这样的问题在某种程度上还可以拉进你与面试官的距离。)
2. 能不能问一下,你当时因为什么原因选择加入这家公司的呢或者说这家公司有哪些地方吸引你?有什么地方你觉
得还不太好或者可以继续完善吗? (类似第一个问题,都是问面试官个人对于公司的看法,)
3. 我觉得我这次表现的不是太好,你有什么建议或者评价给我吗?(这个是我常问的。我觉得说自己表现不好只是
这个语境需要这样来说,这样可以显的你比较谦虚好学上进。)
4. 接下来我会有一段空档期,有什么值得注意或者建议学习的吗? (体现出你对工作比较上心,自助学习意识比
较强。)
5. 这个岗位为什么还在招人? (岗位真实性和价值咨询)
6. 大概什么时候能给我回复呢? (终面的时候,如果面试官没有说的话,可以问一下)
7. ......
面对部门领导
1. 部门的主要人员分配以及对应的主要工作能简单介绍一下吗?
2. 未来如果我要加入这个团队,你对我的期望是什么? (部门领导一般情况下是你的直属上级了,你以后和他打
交道的机会应该是最多的。你问这个问题,会让他感觉你是一个对他的部门比较上心,比较有团体意识,并且
愿意倾听的候选人。)
3. 公司对新入职的员工的培养机制是什么样的呢? (正规的公司一般都有培养机制,提前问一下是对你自己的负
责也会显的你比较上心)
4. 以您来看,这个岗位未来在公司内部的发展如何? (在我看来,问这个问题也是对你自己的负责吧,谁不想发展
前景更好的岗位呢?)
5. 团队现在面临的最大挑战是什么? (这样的问题不会暴露你对公司的不了解,并且也能让你对未来工作的挑战或
困难有一个提前的预期。)
面对Level比较高的(比如总裁,老板)
1. 贵公司的发展目标和方向是什么? (看下公司的发展是否满足自己的期望)
2. 与同行业的竞争者相比,贵公司的核心竞争优势在什么地方? (充分了解自己的优势和劣势)
3. 公司现在面临的最大挑战是什么?
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: 我没有实习经历的话找工作是不是特别艰难?
没有实习经历没关系,只要你有拿得出手的项目或者大赛经历的话,你依然有可能拿到大厂的 o?er 。笔主当时
找工作的时候就没有实习经历以及大赛获奖经历,单纯就是凭借自己的项目经验撑起了整个面试。
如果你既没有实习经历,又没有拿得出手的项目或者大赛经历的话,我觉得在简历关,除非你有其他特别的亮
点,不然,你应该就会被刷。
Question4: 我该如何准备面试呢?面试的注意事项有哪些呢?
下面是我总结的一些准备面试的Tips以及面试必备的注意事项:
1. 准备一份自己的自我介绍,面试的时候根据面试对象适当进行修改(突出重点,突出自己的优势在哪里,切忌
流水账);
2. 注意随身带上自己的成绩单和简历复印件; (有的公司在面试前都会让你交一份成绩单和简历当做面试中的参
考。)
3. 如果需要笔试就提前刷一些笔试题,大部分在线笔试的类型是选择题+编程题,有的还会有简答题。(平时空闲
时间多的可以刷一下笔试题目(牛客网上有很多),但是不要只刷面试题,不动手code,程序员不是为了考试
而存在的。)另外,注意抓重点,因为题目太多了,但是有很多题目几乎次次遇到,像这样的题目一定要搞
定。4. 提前准备技术面试。 搞清楚自己面试中可能涉及哪些知识点、那些知识点是重点。面试中哪些问题会被经常问
到、自己改如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方
式的学习很难坚持下去!)
5. 面试之前做好定向复习。 也就是专门针对你要面试的公司来复习。比如你在面试之前可以在网上找找有没有你
要面试的公司的面经。
6. 准备好自己的项目介绍。 如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你
可以从下面几个方向来考虑:①对项目整体设计的一个感受(面试官可能会让你画系统的架构图;②在这个项
目中你负责了什么、做了什么、担任了什么角色;③ 从这个项目中你学会了那些东西,使用到了那些技术,学
会了那些新技术的使用;④项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开
发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能
比如:用redis做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。
7. 面试之后记得复盘。 面试遭遇失败是很正常的事情,所以善于总结自己的失败原因才是最重要的。如果失败,不要灰心;如果通过,切勿狂喜。
一些还算不错的 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 和 StringBu?er、StringBuilder 的区别是什么?String 为什
么是不可变的?
可变性
简单的来说:String 类中使用 ?nal 关键字字符数组保存字符串,private final char value[],所以 String
对象是不可变的。而StringBuilder 与 StringBu?er 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder
中也是使用字符数组保存字符串char[]value 但是没有用 ?nal 关键字修饰,所以这两种对象都是可变的。
StringBuilder 与 StringBu?er 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自
行查阅源码。
AbstractStringBuilder.java
线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与
StringBu?er 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共
方法。StringBu?er 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对
方法进行加同步锁,所以是非线程安全的。
性能
abstract class AbstractStringBuilder implements Appendable, CharSequence {
?char[] value;
?int count;
?AbstractStringBuilder {
}
?AbstractStringBuilder(int capacity) {
? ? ?value = new char[capacity];
}每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。
StringBu?er 每次都会对 StringBu?er 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用
StirngBuilder 相比使用 StringBu?er 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
1. 操作少量的数据 = String
2. 单线程操作字符串缓冲区下操作大量数据 = StringBuilder
3. 多线程操作字符串缓冲区下操作大量数据 = StringBu?er
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(aa==bb);
? ? ?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 关于 ?nal 关键字的一些总结
nal关键字主要用在三个地方:变量、方法、类。
1. 对于一个?nal变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的
变量,则在对其初始化之后便不能再让其指向另一个对象。
2. 当用?nal修饰一个类时,表明这个类不能被继承。?nal类中的所有成员方法都会被隐式地指定为?nal方法。
3. 使用?nal方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。
在早期的Java实现版本中,会将?nal方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的
任何性能提升(现在的Java版本已经不需要使用?nal方法进行这些优化了)。类中所有的private方法都隐式地
指定为?anl。
2.1.6 Object类的常见方法总结
Object类是一个特殊的类,是所有类的父类。它主要提供了以下11个方法:
2.1.7 Java 中的异常处理
public final native Class> getClassnative方法,用于返回当前运行时对象的Class对象,使用了
final关键字修饰,故不允许子类重写。
public native int hashCode native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的
HashMap。
public boolean equals(Object obj)用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户
比较字符串的值是否相等。
protected native Object clone throws CloneNotSupportedExceptionnaitive方法,用于创建并返回
当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone != x 为true,x.clone.getClass
== x.getClass 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生
CloneNotSupportedException异常。
public String toString返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方
法。
public final native void notifynative方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视
器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notifyAllnative方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒
在此对象监视器上等待的所有线程,而不是一个线程。
public final native void wait(long timeout) throws InterruptedExceptionnative方法,并且不能
重写。暂停线程的执行。注意: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块,则必须跟一个?nally块。catch 块:用于处理try捕获到的异常。
nally 块:无论是否捕获或处理异常,?nally块里的语句都会被执行。当在try块或catch块中遇到return语句
时,?nally语句块将在方法返回之前被执行。
在以下4种特殊情况下,?nally块不会被执行:
1. 在?nally语句块中发生了异常。
2. 在前面的代码中用了System.exit退出程序。
3. 程序所在的线程死亡。
4. 关闭CPU。
2.1.8 获取用键盘输入常用的的两种方法
方法1:通过 Scanner
方法2:通过 Bu?eredReader
2.1.9 接口和抽象类的区别是什么
1. 接口的方法默认是 public,所有方法在接口中不能有实现( Java 8 开始接口方法可以有默认实现),抽象类可以
有非抽象的方法
2. 接口中的实例变量默认是 ?nal 类型的,而抽象类中则不一定
3. 一个类可以实现多个接口,但最多只能实现一个抽象类
4. 一个类实现接口的话要实现接口的所有方法,而抽象类不一定
5. 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计层面来说,抽象是对类的抽
象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
备注:在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现
两个接口,接口中定义了一样的默认方法,必须重写,不然会报错。(详见
issue:https:github.comSnailclimbJavaGuideissues146)
2.2 Java 集合框架
2.2.1 Arraylist 与 LinkedList 异同
1. 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
2. 底层数据结构: 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; 3. 插入和删除是否受元素位置的影响: ① 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)。
4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通
过元素的序号快速获取元素对象(对应于get(int index)方法)。
5. 内存空间占用: ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空
间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数
据)。
补充内容:RandomAccess接口
查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过是一个
标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。
在binarySearch方法中,它要判断传入的list 是否RamdomAccess的实例,如果是,调用
indexedBinarySearch方法,如果不是,那么调用iteratorBinarySearch方法
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 底层使用的是双向循环链表数据结构。
public interface RandomAccess {
}
?public static
?int binarySearch(List extends Comparable super T>> list, T key) {
? ? ?if (list instanceof RandomAccess || list.size
? ? ? ? ?return Collections.indexedBinarySearch(list, key);
? ? ?else
? ? ? ? ?return Collections.iteratorBinarySearch(list, key);
}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.comp21673805
2.2.4 HashMap 和 Hashtable 的区别
1. 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过
synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
2. 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在
代码中使用它;
3. 对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的幂次方。
5. 底层数据结构: 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.comchengxiaop6842045.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
1. List
Arraylist: Object数组
Vector: Object数组
LinkedList: 双向链表( JDK1.6之前为循环链表,JDK1.7取消了循环) 详细可阅读JDK1.7-LinkedList循环链表优
化
2. 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
} 面试官在多线程这一部分很可能会问你有没有在项目中实际使用多线程的经历。所以,如果你在你的项目中有实
际使用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; 这段代码其实是分
为三步执行:
1. 为 uniqueInstance 分配内存空间
2. 初始化 uniqueInstance
3. 将 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?nally 语句块来完成),所以我们可以通过查看它的源代码,来看
它是如何实现的。
③ ReenTrantLock 比 synchronized 增加了一些高级功能
相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;
③可实现选择性通知(锁可以绑定多个条件)ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly来实现这个机制。也
就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等
待的线程先获得锁。 ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的
ReentrantLock(boolean fair)构造方法来制定是否是公平的。
synchronized关键字与wait和notifynotifyAll方法相结合可以实现等待通知机制,ReentrantLock类当然也
可以实现,但是需要借助于Condition接口与newCondition 方法。Condition是JDK1.5之后才有的,它具有很
好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视
器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵
活。 在使用notifynotifyAll方法进行通知时,被通知的线程是由 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方法的区别是什么呢?
1) 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 类的 objectFieldO?set 方法
是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueO?set。另外 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(共享):多个线程可同时执行,如SemaphoreCountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某
一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方
式即可,至于具体线程等待队列的维护(如获取资源失败入队唤醒出队等),AQS已经在顶层实现好了。
5.2.3 AQS底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应
用):
1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源
state的获取和释放)
2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:
默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且
通常应该简短而不是阻塞。AQS类中的其他方法都是?nal ,所以无法被其他类使用,只有这几个方法可以被其他类
使用。
返回同步状态的当前值
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.comwaterystonep4920797.html
https:www.cnblogs.comchengxiaoarchive201707247141160.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解析
2. TCP连接
3. 发送HTTP请求
4. 服务器处理请求并返回HTTP报文
5. 浏览器解析渲染页面
6. 连接结束
具体可以参考下面这篇文章:
https:segmentfault.coma1190000006879700
3.3 各种协议与HTTP协议之间的关系
一般面试官会通过这样的问题来考察你对计算机网络知识体系的理解。
图片来源:《图解HTTP》3.4 HTTP长连接、短连接
在HTTP1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中
断连接。当客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源(如JavaScript文件、图像
文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话。
而从HTTP1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:
在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客
户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时
间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。
HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。
—— 《HTTP长连接、短连接究竟是什么?》
3.5 TCP 三次握手和四次挥手(面试常客)
为了准确无误地把数据送达目标处,TCP协议采用了三次握手策略。
漫画图解:
图片来源:《图解HTTP》
Connection:keep-alive简单示意图:
客户端–发送带有 SYN 标志的数据包–一次握手–服务端
服务端–发送带有 SYNACK 标志的数据包–二次握手–客户端
客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端
为什么要三次握手?
三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是
双方确认自己与对方的发送与接收是正常的。
第一次握手:Client 什么都不能确认;Server 确认了对方发送正常
第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己接收正常,对方发
送正常
第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送接收正常
所以三次握手就能确认双发收发功能都正常,缺一不可。
为什么要传回 SYN
接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。
SYN 是 TCPIP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 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.netqzcsuarticledetails72861891
四 Linux 4.1 简单介绍一下 Linux 文件系统?
Linux文件系统简介
在Linux操作系统中,所有被操作系统管理的资源,例如网络接口卡、磁盘驱动器、打印机、输入输出设备、普通文
件或是目录都被看作是一个文件。
也就是说在LINUX系统中有一个重要的概念:一切都是文件。其实这是UNIX哲学的一个体现,而Linux是重写UNIX而
来,所以这个概念也就传承了下来。在UNIX系统中,把一切资源都看作是文件,包括硬件设备。UNIX系统把每个硬
件都看成是一个文件,通常称为设备文件,这样用户就可以用读写文件的方式实现对硬件的访问。
文件类型与目录结构
Linux支持5种文件类型 :
Linux的目录结构如下:Linux文件系统的结构层次鲜明,就像一棵倒立的树,最顶层是其根目录:
常见目录说明:
bin: 存放二进制可执行文件(ls,cat,mkdir等),常用命令一般都在这里;
etc: 存放系统管理和配置文件;
home: 存放所有用户文件的根目录,是用户主目录的基点,比如用户user的主目录就是homeuser,可以
用~user表示;
usr : 用于存放系统应用程序;
opt: 额外安装的可选应用程序包所放置的位置。一般情况下,我们可以把tomcat等都安装到这里;
proc: 虚拟文件系统目录,是系统内存的映射。可直接访问这个目录来获取系统信息;
root: 超级用户(系统管理员)的主目录(特权阶级^o^);
sbin: 存放二进制可执行文件,只有root才能访问。这里存放的是系统管理员使用的系统级别的管理命令和程
序。如ifcon?g等;
dev: 用于存放设备文件;
mnt: 系统管理员安装临时文件系统的安装点,系统提供这个目录是让用户临时挂载其他的文件系统;
boot: 存放用于系统引导时使用的各种文件;
lib : 存放着和系统运行相关的库文件 ;
tmp: 用于存放各种临时文件,是公用的临时文件存储点;
var: 用于存放运行时需要改变数据的文件,也是某些大文件的溢出区,比方说各种服务的日志文件(系统启
动日志等。)等;
lost+found: 这个目录平时是空的,系统非正常关机而留下“无家可归”的文件(windows下叫什么.chk)就在
这里。
4.2 一些常见的 Linux 命令了解吗?
目录切换命令
cd usr: 切换到该目录下usr目录
cd ..(或cd..): 切换到上一层目录
cd : 切换到系统根目录
cd ~: 切换到用户主目录
cd -: 切换到上一个所在目录
目录的操作命令(增删改查)
1. mkdir 目录名称: 增加目录2. ls或者ll(ll是ls -l的缩写,ll命令以看到该目录下的所有目录和文件的详细信息):查看目录信息
3. find 目录 参数: 寻找目录(查)
4. mv 目录名称 新目录名称: 修改目录的名称(改)
注意:mv的语法不仅可以对目录进行重命名而且也可以对各种文件,压缩包等进行 重命名的操作。mv命令用
来对文件或目录重新命名,或者将文件从一个目录移到另一个目录中。后面会介绍到mv命令的另一个用法。
5. mv 目录名称 目录的新位置: 移动目录的位置---剪切(改)
注意:mv语法不仅可以对目录进行剪切操作,对文件和压缩包等都可执行剪切操作。另外mv与cp的结果不
同,mv好像文件“搬家”,文件个数并未增加。而cp对文件进行复制,文件个数增加了。
6. cp -r 目录名称 目录拷贝的目标位置: 拷贝目录(改),-r代表递归拷贝
注意:cp命令不仅可以拷贝目录还可以拷贝文件,压缩包等,拷贝文件和压缩包时不 用写-r递归
7. rm [-rf] 目录 : 删除目录(删)
注意:rm不仅可以删除目录,也可以删除其他文件或压缩包,为了增强大家的记忆, 无论删除任何目录或文
件,都直接使用rm -rf 目录文件压缩包
文件的操作命令(增删改查)
1. touch 文件名称 : 文件的创建(增)
2. catmorelesstail 文件名称 文件的查看(查)
cat: 只能显示最后一屏内容
more: 可以显示百分比,回车可以向下一行, 空格可以向下一页,q可以退出查看
less: 可以使用键盘上的PgUp和PgDn向上 和向下翻页,q结束查看
tail-10 : 查看文件的后10行,Ctrl+C结束
注意:命令 tail -f 文件 可以对某个文件进行动态监控,例如tomcat的日志文件, 会随着程序的运行,日志会变
化,可以使用tail -f catalina-2016-11-11.log 监控 文 件的变化
3. vim 文件: 修改文件的内容(改)
vim编辑器是Linux中的强大组件,是vi编辑器的加强版,vim编辑器的命令和快捷方式有很多,但此处不一一阐
述,大家也无需研究的很透彻,使用vim编辑修改文件的方式基本会使用就可以了。
在实际开发中,使用vim编辑器主要作用就是修改配置文件,下面是一般步骤:
vim 文件------>进入文件----->命令模式------>按i进入编辑模式----->编辑文件 ------->按Esc进入底行模式----->输
入:wqq! (输入wq代表写入内容并退出,即保存;输入q!代表强制退出不保存。)
4. 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杀掉
网络通信命令:
查看当前系统的网卡信息:ifcon?g
查看与某台机器的连接情况: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来说,则没有这种缓存。
2. 是否支持事务和崩溃后的安全恢复: MyISAM 强调的是性能,每次查询具有原子性,其执行数度比InnoDB类型
更快,但是不提供事务支持。但是InnoDB 提供事务支持事务,外部键等高级数据库功能。 具有事务
(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID
compliant))型表。
3. 是否支持外键: 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语句,默认会这样
做:1. 定位到记录所在的页:需要遍历双向链表,找到所在的页
2. 从所在的页内中查找相应的记录:由于不是根据主键查询,只能遍历所在页的单链表了
很明显,在数据量很大的情况下这样查找会很慢!这样的时间复杂度为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 TABLE `table_name` ADD PRIMARY KEY ( `column` )
ALTER TABLE `table_name` ADD UNIQUE ( `column` )
ALTER TABLE `table_name` ADD INDEX index_name ( `column` )
ALTER TABLE `table_name` ADD FULLTEXT ( `column`)
ALTER TABLE `table_name` ADD INDEX index_name ( `column1`, `column2`, `column3` )5.3 当MySQL单表记录数过大时,数据库的CRUD性能会明显下
降,一些常见的优化措施如下:
当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下:
1. 限定数据的范围: 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时
候,我们可以控制在一个月的范围内。;
2. 读写分离: 经典的数据库拆分方案,主库负责写,从库负责读;
3. 垂直分区: 根据数据库里面数据表的相关性进行拆分。 例如,用户表中既有用户的登录信息又有用户的基本信
息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。 如下图所示,这样来说大家应该就更容易理解了。
垂直拆分的优点: 可以使得行数据变小,在查询时减少读取的Block数,减少IO次数。此外,垂直分区可以简
化表的结构,易于维护。垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过
在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂;
4. 水平分区: 保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达
到了分布式的目的。 水平拆分可以支撑非常大的数据量。 水平拆分是指数据表行的拆分,表的行数超过200万
行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多
个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。
水平拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的
数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 水平拆分最好分库 。水平拆分能
够 支持非常大的数据量存储,应用端改造也少,但 分片事务难以解决 ,跨界点Join性能较差,逻辑复杂。
《Java工程师修炼之道》的作者推荐 尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂
度 ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择
客户端分片架构,这样可以减少一次和中间件的网络IO。
下面补充一下数据库分片的两种常见方案:客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的 Sharding-
JDBC 、阿里的TDDL是两种比较常用的实现。
中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat
、360的Atlas、网易的DDB等等都是这种架构的实现。
5.4 事务隔离级别(图文详解)
什么是事务?
事务是逻辑上的一组操作,要么都执行,要么都不执行。
事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就
是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩
溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要
失败。
事物的特性(ACID)
1. 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
2. 一致性: 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
3. 隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
4. 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影
响。
并发事务带来的问题
在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对统一数据进行操
作)。并发虽然是必须的,但可能会导致以下的问题。
脏读(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.comdocrefman5.7en
Mysql 锁:灵魂七拷问
Innodb 中的事务隔离级别和锁的关系
六 Redis
6.1 redis 简介
简单来说 redis 就是一个数据库,不过与传统数据库不同的是 redis 的数据是存在内存中的,所以存写速度非常快,因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业
务场景。除此之外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。
6.2 为什么要用 redis 为什么要用缓存
主要从“高性能”和“高并发”这两点来看待这个问题。
高性能:
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数
缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当
快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!高并发:
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中
去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。6.3 为什么要用 redis 而不用 mapguava 做缓存?
下面的内容来自 segmentfault 一位网友的提问,地址:https:segmentfault.comq1010000009106416
缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是
轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓
存不具有一致性。
使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致
性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。
6.4 redis 和 memcached 的区别
对于 redis 和 memcached 我总结了下面四点。现在公司一般都是用 redis 来实现缓存,而且 redis 自身也越来越强
大了!
1. redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的kv类型的数据,同时还提供
list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
2. Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而
Memecache把数据全部存在内存之中。
3. 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前
是原生支持 cluster 模式的.
4. Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。
来自网络上的一张图,这里分享给大家!6.5 redis 常见数据结构以及使用场景分析
1. String
常用命令: set,get,decr,incr,mget 等。
String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 常规key-value缓存应用;
常规计数:微博数,粉丝数等。
2.Hash
常用命令: hget,hset,hgetall 等。
Hash 是一个 string 类型的 ?eld 和 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 可以存活的
时间。 ......
您现在查看是摘要介绍页, 详见PDF附件(8254KB,124页)。





