面经软件测试岗位常见面试题全套合集系列4-1
Posted 测试架构师北凡
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面经软件测试岗位常见面试题全套合集系列4-1相关的知识,希望对你有一定的参考价值。
目录
3.5 用例包含哪些部分,哪些用例设计方法,你一般常用哪些方法?
3.9 bug怎么管理的,bug的生命周期或者是bug的状态
3.15 测试中,如何判断是前端的bug还是后端的bug呢?
3.19 你测试数据是从哪里获得?怎么获得的,假如不告诉你,你怎么处理?
3.30 怎么保证测试质量或者你怎么保证你100%覆盖了需求
3.33 测试过程中,发现很多用例重复的,有的人认为没必要再测,你怎么看?
4.6 项目的送代周期? 多久一选代? 一个版本你们发现多少bug
4.7 你们整个项目写了多少用例,你负责的模块大概写了多少用例?
4.9 你的需求分析一般几天,用例大概写了多长时间?执行了多长时间?
4.12 每个阶段测试开发在干嘛(比如你写用例的时候开发在干嘛?)
4.14 你这个项目做了多久? 你这个项目现在的用户里有多少? 活跃量多少?
一、简介
大家好我是你们的凡叔最近很多小伙伴在面试跳槽,凡叔这边就收集的一下最近所有做软件测试的朋友去面试的题目给大家写了出了这么一份一线城市的面经,希望给各位小伙伴带来帮助哈,如果喜欢凡叔的作品,认可凡叔的呢可以给凡叔点个关注哈。
1.2 背景
基本概括了深圳与广州,上干家公司的面试问题并结合编写与整理的面试文档,基本盖了,95%以上的面试问题进行知识点复习与面试指导。
有很多的萌新小伙伴呢面试很紧张,这个是千万不能有的哦,不说别的,你一紧张工资这方面就比较难谈了,所以千万不能紧张,这只是一个简简单单的面试而已。
这里就提前先祝各位小伙伴们面试成功咯。
二、职业规划
2.1 简单的自我介绍下
面试宫,您好,我叫XXX,来自于XXXX,目前从事软件测试工作,已经三年工作经验,个人性格,比较开朗,跟人关系比较好,做事也比较细心三年测试工作经验中,过了不少项目,积累不少项目经验,前面1-2年主要是功能测试,后面这一年主要做接口测试,app自动化测试能够独立完成软件产品测试工作,能够独立编写测试文档,包括用例,计划,报告等,熟悉lnux跟数据库,熟悉 jmeter与 python + request进行接口测试,也可以使用 pytest框架进行接口自动化测试, python + selenium + pytest框架进行自动化测试,python + appnium + pytest移动app自动化测试框架,熟悉使用Jenkins持续集成,熟悉app专项测试与小程序测试,熟悉抓包工具。
我个人平常喜欢…看书…
我个人觉得测试这一块,主要是对需求了解,需求理解到位,工作当中,一定细心耐心,技术这块,不断学习能力
如果面试官没有说话, 这个是我的一个简单自我介绍,看面试官还有什么需要了解的
2.2 为什么离职
①、尽量谈化敏感答案比如,人际关系不好处理,与上司相处不好、工作压力大等。
人际关系复杂,现代企业讲求团队精神,要求所有成员都能有与别人合作的能力,你对人际关系的胆怯和避讳,可能会被认为是心理状况不佳,处于忧郁焦躁孤独的心境之中,从而妨碍了你的从业取向。
收入太低,这样回答会使对方认为你是单纯为了收入取向,很计较个人得失,并且会把“如有更高的收入,会毫不犹豫地跳槽而去的”这种观念形成对你的思维定势。
分配不公平。现在企业中实行效益薪金、浮动工资制度是很普遍的,旨在用物质刺激手段提高业绩和效率;同时,很多单位都开始了员工收入保密的措施,如果你在面试时将此作为离开原单位的借口,则一方面你将失去竟争优势,另一方面你会有爱打探别人收入乃至隐私的嫌疑。
工作压力太大。现代企业生存状况是快节奏的,企业中的各色人等皆处于高强度的工作生存状态下,有的单位在招聘启事上干脆直言相告,要求应聘者能在压力下完成工作,这是越来越明显的趋向
②、尽量采用与工作能力关系不大、能为人所理解的离职原因。
寻求更大的发展:现有的企业岗位设置难以满足自身职业进一步发展的要求,
换一个更好的平台来挑战自我。
原公司发生了重大客观变化:公司重组或部内部变动,导致工作内容发生重大变化,
无法再继续履行劳动合同,或者直接被裁员等
与公司文化无法融合:每一家企业其实都有自己特有的“文化”,如果你在这家公司里工作,
却无法认可这家公司提倡的一些文化,这就会对企业的发展以及对自己的发展都非常的不利。
所以你想要走出企业的束缚,找到一家跟你更契合的公司工作也是可以的。
个人原因:上班太远影响工作、家中有事、充电、休假、生病等等。注意:避免敏感答案,
并不意味着欺骗,如招聘人员问及细节问题,应如实回答。否则求职者的诚信度可能大打折扣,
成功可能性更小。
③、不要诋毁你的老东家
相信很少会有人犯这样的错误,这的确是一个不可取的方式,你应该把你的离职原因集中表述
在“寻找新机会或新的平台”以及尝试在新的岗位上提升自己。职场虽然没有战争那么血腥,
但也有很多委屈、不被理解或被无故受伤,这些都很正常的。我们要用一颗阳光的心去面对,
用一颗阳光的心去照耀。
在离职后,永远要用赞美的词语来评价你的老东家,哪怕他在公开场合去骂你,你都要用最美的词语去评价他。别忘记,你赞美他,是你素养高;他骂你,是他素质的问题。你永远改变不了别人,但你有能力改变自己。
④、体现你的忠诚度
如果你轻描淡写地就离开了之前的团队,那么面试官会觉得你在新公司也可能会轻易走人,
所以,在体现忠诚度的时候,你可以试着谈谈你离开上一家公司时有多么痛苦依依不舍(即使并没有),聊聊如果有办法使你能在原来的岗位上持续得到提升或者如果不是因为股东之间的权利斗争(可适当显得痛心疾首些),你肯定不会离开。
而在体现责任感时,你需要表达两层意思:
首先,你从上家公司离职时已经为继任者做了充分的交接。你需要清楚地表明:你在上家公司也很认真尽职,并且同事之间一直保持互助互利的工作氛围。也许你可以说说你也想过要早些辞职,但是考虑某个未完成的重要项目、或是继任者短期内还不能胜任角色所以晚了一些。
其次,就是你很期望承担新的职责,并表现出你的热枕,这种热枕除了对工作的热忱之外,
也可以适当的通过向你的面试官(不仅是HR)提问表现出来对面试官的兴趣、对他们技能的
认可以及共鸣,例如,“那么,你是如何做到现任职位的?”或:“如果我有幸担任这个职位,
你会给我哪些建议?”
通用说法:家人在这边,或者想到大城市发展。
2.3 加班的看法
1、(将问题抛给HR,)在回答您的问题之前我想了解一下贵公司的加班制度是怎样的。
(这种回答,其实是把问题抛回给HR,让HR表明公司对于加班的态度,其实很多大公司
对于加班都有明确的规定,什么情况该加班,加班会有什么福利等等问题都是确定的,
而小公司就随意很多,往往是老板要求加班员工就得无条件加斑。因此,如果公司的加班制度明确,
那么HR就能够明确地向你介绍,你在了解过后再给出回应也不迟。面试本就是双向选择的过程,
你也没必要为了通过面试,满口答应自己愿意加斑。)
2、在IT行业里,加班是比较正常的,首先我会了解加班的原因,如果公司近段时间需要赶
项目进度或者是站在重要的关键节点上需要加班,我会站守自己的岗位,把自己手上的事做好,
和团队一起加班,让公司按预期的进度推进项目,这在我看来是必要的加班,
如果是其他的原因,比如个人原因,我会努力不加班,在保证工作质量的前提下,
我会提高自己的工作效率,避免加班。
如果说公司基本上天天都要加班,加班的频车较高,我希望可以减少不必要的加班,让员工
得到充分的休息,有休息才会把工作效率提升上来,工作才会更有效率,另外有些工作上的
能力炼可以在其它地方,而不在工作的本身,比如对生活的理解和感悟等之类,
是从工作中学习不到的,正所谓功夫在诗外嘛。
还有,我之前在上一个公司上班,住的地方离公司较远,作为一个女生,出于个人人身安全考虑,
我更希望不加班,毕竟生命健康是从事一切活动的前提嘛。
通用说法:如果项目组比较忙,加班是没有问题的。
2.4 你还有什么问题要问?
少问一些福利相关的问题
1.公司现在做什么项目 2.公司目前做哪方面测试 3.公司这边测试人员分配比例
4.进入公司,我这边大概的工作安排 5,公司这么后续发展机会还有培养
6,有没有培训 7,面试没有回答上的问题,再去请教
2.5 你的职业发展规划和职业目标
根据公司况,个人原因来说:看公司的岗位要求,招岗位就是攻能
公司只做功能测过
接下来一年时间内,更加完善自己的功能测试,2-3年内熟自动化或者性能,
3-5年内系统成为自动化或者性能,成为资深技术人员
公司做自动化与性能测试
1-2年内熟恐自动化或者性能,3-5系统成为自动化或者性能,成为资深技术人员性能与自动测试
找一个比较稳定平台,跟公司长期发展,后期走管理或者产品路线
2.6 你最近测试的论坛和网站是什么?
工作中积累,查看网站论坛(51 testing),CSDN,书籍《性能测试专家》,《性能之巅》
偏开发, python自动化, selenium自动化
2.7 对于一个新的工具你有什么看法
1.自己先去研究 2,找会的去请教 3,百度去找资料 4,工具原始文档
2.8 你做了3年测试收获,你的测试心得
3年的测试经验,对我来说也是3年的工作经验,在这3年的工作经验当中,我觉得态度比能力要重要,做好一个测试,最主要是性格,信心,耐心,细心还需要良好的沟通能力。不断学习的能力,产品质量,测试流程这块很关键,好的计划加好的执行才能成就好的产品。
2.9 怎么转行的,毕业之后一直从事软件测试吗
非专业
1、培训,不要说刚培训出来2019年底开始
2、自学,不断一直学习
3、家里有关系,带你入行,后面学习过程很勤奋
计算机相关专业
1,实习开始,公司分配到做测试,做测试过程,比较喜欢测试,一直做下来
2.10 以前在哪里上班,公司地址,测试多少人,开发多少人
根据你简历上面写的,公司详细地址
产品1、项目1个、架构师1个、前端3个、后端 5个、0os 1个、android 1个,
测试3个(测试主管,核心测试人员)、运维1个、ui 1个
2.11 你的期望薪资是多少?
1,期望薪资不要说区间比如说:10-15,那肯定是10
2,如果原来公司在二线城市原有薪资不要说太高,
3,深圳那边薪资,比广州高10% - 15%
2.12 多久能入职?
1,公司比较满意,直接随时过去。
2,不是很满意,下周一,个人有点事情,比如说回家一趟等等。
2.13 是怎么考虑做软件测试呢?
1、个人性格合适 2、前景还可以 3、个人技能也匹配
2.14 是否有考虑往开发方面测试?
我觉得,IT行业,没有具体的界限,后期,开发也要懂测试,测试也要懂开发,
如果公司有机会,愿意去尝试
2.15 计算机专业课程有哪些?
公共课程:数学(高等数学、线性代数、概率论与数理统计、离散数学、数值分析),
政治(马克思主义思想概论、毛泽东思想概论与中国特色社会主义思想、
思想道德修养与法律基础、中国近现代史纲要)、大学英语、体育。
专业基础课程:电路原理、模拟电子技术、数字逻辑、微机原理、汇编语言、操作系统原理、
编译原理、算法与数据结构、面向对象方法、C语言/C++语言等。
专业方向课程:计算机数据库原理、python语言、图形学、人工智能,多媒体技术、
网络安全、人机交互、无线互联网技术、软件开发方法、高性能技术系统仿真和虚拟现实等。
大专3年、本科4年,
本科四级、大学英语四级及格425分、总分710
2.16 说一下上一个公司背景?
了解公司主要是什么项目,百度查下,
如果公司主营产品跟你项目不匹配
比如:原来公司做医疗设备,那就说:我们是项目外包的部门,专门接项目
2.17 超过26岁女孩子,目前有没有打算结婚
暂时没有结婚的计划与打算,如果已经有小孩,说暂时不考虑二胎,
有小孩,可以说小孩在老家(原来有学员因为这个问题,被公司pas过)
三、测试理论
3.1 你们原来项目的测试流程是怎么样的?
我们的测试流程主要有三个阶段:需求了解分析、测试准备、测试执行。
1、需求了解分析阶段
我们的SE会把需求文档给我们自己先去了解一到两天这样,之后我们会有一个需求澄清会议,
我们会把不明白不理解的需求在会议上说出来,包含需求的合理性还有需求的可测性等,
产品这边解答,目的是让我们测试这边和开发对需求的理解达到一致。
2、测试准备阶段
会议结束之后我们开始准备测试工作,我们测试这边会写一个测试计划,分配每个人负责的模块,
然后我们就根据自己负责的模块用 xmind(思维导图)进行测试需求分析,分析测试点,
以及编写测试用例,之后我们会在自己的组内先进行评审,评审修改之后还会在我们的项目组评审,
评审完后进行修改测试用例。
3、测试执行阶段
开发人员编写好代码之后,我们会把代码包通过 Jelkins部署到测试环境提测,进行SIT测试,
在正式测试之前我们会先做一个冒烟测试,冒烟测试通过之后我们才转测,在执行测试的过程中,
我们如果发现bug就会用tapd(或者禅道)记录并且提交bug,也会进行bug复测,以及回归测试,
每一轮测试结束之后我们都会写一个测试报告,一般情况下,测试4-5轮之后会达到上线要求,
当达到上线的标准后,测试报告会认为测试通过,上线前我们会做预发布测试,预发布通过后,
由项目组与产品决定时间上线,上线完成,一周左右我们会写一个项目总结测试报告,
总结我们在上一个版本中遇到的问题以及今后有哪些地方需要改进,在产品选代过程中,
我们会跑自动化用例来进行回归测试。
3.2 如果需求不明确的话你怎么办?
需求不明确的话我会在需求澄清会议上面提出来,问清楚这个需求只有明确需求,
才能更好的完成工作,后续工作中还是不清楚,可以找产品再去确认这个需求。
3.3 有哪些需要评审,哪些人在
1、 xmind思维导图评审,主要是测试人员
2、测试用例需要评审,测试人员,开发人员,产品人员
3、需求文档,项目组所有的人员,都会到场
3.4 有没有写过测试计划,具体包括哪些内容?
参考答案1:
测试计划内容:
(1)目的和范围 (2)规程 (3)测试方案和方法 (4)测试的准入和准出
(5)测试计划(流程、时间安排、对应人员) (6)测试的环境配置和人员安排 (7)交付件
参考答案2
我们公司之前按照考核要求写过测试计划,不过后面老大觉得太耽误工作进度,
后面一般都不再写测试计划,而是写版本计划,这个在版本计划,每个人的任务列出来,
负责人列出来,自己根据自己的情况分配时间,然后汇总,大家一起开个小会评审就可以了。
3.5 用例包含哪些部分,哪些用例设计方法,你一般常用哪些方法?
原来我们用例包含
测试项目,用例编号、测试标题、优先级、预置条件、操作步骤、测试数据、预期结果
黑盒测试用例设计方法:主要是等价类、边界值、错误推测法、判定表、因果图、正交表、
流程分析法、状态迁移法、异常分析法。
常用的:等价类、边界值、判定表、流程分析法、错误推测法。
等价类是指某个输入域的子集合,在该子集合中,
各个输入数据对于揭露程序中的错误都是等效的,
并合理地假定:测试某等价类的代表值就等于对这一类其它值的测试,因此,可以把全部
输入数据合理划分为若干等价类,在每一个等价类中取一个数据作为测试的输入条件,
就可以用少量代表性的测试数据取得较好的测试结果,
等价类划分可有两种不同的情况有效等价类和无效等价类。
边界值的话就是对等价类划分方法的补充。测试工作经验告诉我,大量的错误往往是发生在输入或输出范围的边界上而不是发生在输入输出范围的内部,因此的话针对各种边界情况来设计测试用例,可以查出更多的错误,使用边界值分析方法设计测试用例的话,首先应该确定边界情况,通常输入和输出等价类的边界,就是应着重测试的边界情况应当选取正好等于,刚刚大于或刚刚小于边界的值作为测试数据,而不是选取等价类中的典型值或任意值作为测试数据。
对于错误推断法,这个是基于经验和直觉推测程序中所有可能存在的各种错误,
从而有针对性的去设计测试用例的方法的,主要就是列举出程序中所有可能有的错误和容易发生错误的特殊情况去根据这些情况来选择测试用例,例如,在单元测试时曾列出的许多在模块中常见的错误以前产品测试中曾经发现的错误等,这些就是经验的总结。还有,输入数据和输出数据为0的情况。
输入表格为空格或输入表格只有一行。这些都是容易发生错误的情况,可选择这些情况下的例子作为测试用例。
前面介绍的等价类划分方
面经Java岗位常见面试题
面向对象
什么是面向对象?
对比面向过程,是两种不同的处理问题的角度
面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者(对象)、及各自需要做 什么
比如:洗衣机洗衣服
面向过程会将任务拆解成一系列的步骤(函数),1、打开洗衣机----->2、放衣服----->3、放洗衣粉-----
>4、清洗 >5、烘干
面向对象会拆出人和洗衣机两个对象: 人:打开洗衣机 放衣服 放 洗 衣 粉 洗衣机:清洗 烘干
从以上例子能看出,面向过程比较直接高效,而面向对象更易于复用、扩展和维护
面向对象
封装:封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项内部细节对外部调用透明,外部调用无需修改或者关心内部实现
1、javabean的属性私有,提供getset对外访问,因为属性的赋值或者获取逻辑只能由javabean本身决 定。而不能由外部胡乱修改
private String name;
public void setName(String name) this.name = "tuling_"+name;
该name有自己的命名规则,明显不能由外部直接赋值
2、orm框架
操作数据库,我们不需要关心链接是如何建立的、sql是如何执行的,只需要引入mybatis,调方法即可
继承:继承基类的方法,并做出自己的改变和/或扩展
子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的
多态:基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。 继承,方法重写,父类引用指向子类对象
父类类型 变量名 = new 子类对象 ;
变量名.方法名();
无法调用子类特有的功能
JDK JRE JVM
JDK:
Java Develpment Kit java 开发工具
JRE:
Java Runtime Environment java运行时环境JVM:
java Virtual Machine java 虚拟机
==和equals比较
==对比的是栈中的值,基本数据类型是变量值,引用类型是堆中内存对象的地址
equals:object中默认也是采用==比较,通常会重写Object
public boolean equals(Object obj) return (this == obj);
String
上述代码可以看出,String类中被复写的equals()方法其实是比较两个字符串的内容。
public boolean equals(Object anObject) if (this == anObject)
return true;
if (anObject instanceof String)
String anotherString = (String)anObject; int n = value.length;
if (n == anotherString.value.length) char v1[] = value;
char v2[] = anotherString.value; int i = 0;
while (n-- != 0)
if (v1[i] != v2[i]) return false;
i++;
return true;
return false;
public class StringDemo
public static void main(String args[]) String str1 = "Hello";
String str2 = new String("Hello"); String str3 = str2; // 引用传递
System.out.println(str1 == str2); // false System.out.println(str1 == str3); // false System.out.println(str2 == str3); // true System.out.println(str1.equals(str2)); // true System.out.println(str1.equals(str3)); // true System.out.println(str2.equals(str3)); // true
hashCode与equals
hashCode介绍:
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,Java中的任何类都包含有
hashCode() 函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用 到了散列码!(可以快速找到所需要的对象)
为什么要有hashCode:
以“HashSet如何检查重复”为例子来说明为什么要有hashCode:
对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,看该位置是否有 值,如果没有、HashSet会假设对象没有重复出现。但是如果发现有值,这时会调用equals()方法来 检查两个对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样就大大减少了equals的次数,相应就大大提高了执行速度。
如果两个对象相等,则hashcode一定也是相同的
两个对象相等,对两个对象分别调用equals方法都返回true 两个对象有相同的hashcode值,它们也不一定是相等的
因此,equals方法被覆盖过,则hashCode方法也必须被覆盖
hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个 对象无论如何都不会相等(即使这两个对象指向相同的数据)
final
最终的
修饰类:表示类不可被继承
修饰方法:表示方法不可被子类覆盖,但是可以重载修饰变量:表示变量一旦被赋值就不可以更改它的值。
修饰成员变量
如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。如果final修饰的是成员变量,可以在非静态初始化块、声明该变量或者构造器中执行初始值。
修饰局部变量
系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用final修饰局部变量时, 即可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码 中对final变量赋初值(仅一次)
public class FinalVar
final static int a = 0;//再声明的时候就需要赋值 或者静态代码块赋值
/** static
a = 0;
*/
final int b = 0;//再声明的时候就需要赋值 或者代码块中赋值 或者构造器赋值
/*
b = 0;
*/
public static void main(String[] args)
final int localA; //局部变量只声明没有初始化,不会报错,与final无关。localA = 0;//在使用之前一定要赋值
//localA = 1; 但是不允许第二次赋值
修饰基本类型数据和引用类型数据
如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;
如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。但是引用的值是可变 的。
public class FinalReferenceTest public static void main()
final int[] iArr=1,2,3,4; iArr[2]=-3;//合法
iArr=null;//非法,对iArr不能重新赋值
final Person p = new Person(25);
p.setAge(24);//合法
p=null;//非法
为什么局部内部类和匿名内部类只能访问局部final变量?
编译之后会生成两个class文件,Test.class Test1.class
public class Test
public static void main(String[] args)
//局部final变量a,b
public void test(final int b) //jdk8在这里做了优化, 不用写,语法糖,但实际上也是有的,也不能修改
final int a = 10;
//匿名内部类new Thread()
public void run() System.out.println(a); System.out.println(b);
;
.start();
class OutClass
private int age = 12;
public void outPrint(final int x) class InClass
public void InPrint()
System.out.println(x); System.out.println(age);
new InClass().InPrint();
首先需要知道的一点是: 内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着方法的执行完毕就被销毁。
这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有 没有人再引用它时,才会死亡)。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解 决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以 访问它,实际访问的是局部变量的"copy"。这样就好像延长了局部变量的生命周期
将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修 改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?
就将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和方法的局部变量的一致性。这实际上也是一种妥协。使得局部变量与内部类内建立的拷贝保持一致。
String、StringBuffer、StringBuilder
String是final修饰的,不可变,每次操作都会产生新的String对象StringBuffer和StringBuilder都是在原对象上操作
StringBuffer是线程安全的,StringBuilder线程不安全的
StringBuffer方法都是synchronized修饰的性能:StringBuilder > StringBuffer > String
场景:经常需要改变字符串内容时使用后面两个
优先使用StringBuilder,多线程使用共享变量时使用StringBuffer
重载和重写的区别
重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方 法。
public int add(int a,String b) public String add(int a,String b)
//编译报错
接口和抽象类的区别
抽象类可以存在普通成员函数,而接口中只能存在public abstract 方法。
抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。抽象类只能继承一个,接口可以实现多个。
接口的设计目的,是对类的行为进行约束(更准确的说是一种“有”约束,因为接口不能规定类不可以有 什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无, 但不对如何实现行为进行限制。
而抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类。在这个抽象类中实 现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自己 实现。正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A-B时,无法执
行)。
抽象类是对类本质的抽象,表达的是 is a 的关系,比如: BMW is a Car 。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。
而接口是对行为的抽象,表达的是 like a 的关系。比如: Bird like a Aircraft (像飞行器一样可以飞),但其本质上 is a Bird 。接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现的,接口并不关心。
使用场景:当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。
抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为高级语言来说(从实际设计上来说也 是)每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功 能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口。在设计 阶段会降低难度
List和Set的区别
List:有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出 所有元素,在逐一遍历,还可以使用get(int index)获取指定下标的元素
Set:无序,不可重复,最多允许有一个Null元素对象,取元素时只能用Iterator接口取得所有元 素,在逐一遍历各个元素
ArrayList和LinkedList区别
ArrayList:基于动态数组,连续内存存储,适合下标访问(随机访问),扩容机制:因为数组长度固 定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会 涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能、甚 至超过linkedList(需要创建大量的node对象)
LinkedList:基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询:需要逐一遍历
遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体内通过get(i)取得某一元素时都需 要对list重新进行遍历,性能消耗极大。
另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexlOf对list进行了遍历,当结 果为空时会遍历整个列表。
HashMap和HashTable有什么区别?其底层实现是什么?
区别 :
- HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全;
- HashMap允许key和value为null,而HashTable不允许
底层实现:数组+链表实现
jdk8开始链表高度到8、数组长度超过64,链表转变为红黑树,元素以内部类Node节点存在计算key的hash值,二次hash然后对数组长度取模,对应到数组下标,
如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组,
如果产生hash冲突,先进行equal比较,相同则取代该元素,不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表
key为null,存在下标0的位置
数组扩容
ConcurrentHashMap原理,jdk7和jdk8版本的区别
jdk7:
数据结构:ReentrantLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个
HashEntry又是一个链表结构
元素查询:二次hash,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部
锁:Segment分段锁 Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响,并发度为segment个数,可以通过构造函数指定,数组扩容不会影响其他的segment
get方法无需加锁,volatile保证jdk8:
数据结构:synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性
查找,替换,赋值操作都使用CAS
锁:锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容
读操作无锁:
Node的val和next使用volatile修饰,读写线程对该变量互相可见数组用volatile修饰,保证扩容时被读线程感知
什么是字节码?采用字节码的好处是什么?
java中的编译器和解释器:
Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。
编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系 统的机器码执行。在Java中,这种供虚拟机理解的代码叫做 字节码(即扩展名为 .class的文件),它不面向任何特定的处理器,只面向虚拟机。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节 码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机 器上的机器码,然后在特定的机器上运行。这也就是解释了Java的编译与解释并存的特点。
Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器 >机器可执
行的二进制机器码 >程序运行。
采用字节码的好处:
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器, 因此,Java程序无须重新编译便可在多种不同的计算机上运行。
Java中的异常体系
Java中的所有异常都来自顶级父类Throwable。Throwable下有两个子类Exception和Error。
Error是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行。
Exception不会导致程序停止,又分为两个部分RunTimeException运行时异常和CheckedException检 查异常。
RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。CheckedException常常发生在程序编译过程中,会导致程序编译不通过。
Java类加载器
JDK自带有三个类加载器:bootstrap ClassLoader、ExtClassLoader、AppClassLoader。
BootStrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%lib下的jar包和class文件。
ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的jar包和class 类 。 AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件。系统类加载器,线程上下 文加载器
继承ClassLoader实现自定义类加载器
双亲委托模型
双亲委派模型的好处:
主要是为了安全性,避免用户自己编写的类动态替换 Java的一些核心类,比如 String。
同时也避免了类的重复加载,因为 JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不同的 ClassLoader加载就是不同的两个类
GC如何判断对象可以被回收
引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计 数为0时可以回收,
可达性分析法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC
Roots 没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。
引用计数法,可能会出现A 引用了 B,B 又引用了 A,这时候就算他们都不再使用了,但因为相互引用 计数器=1 永远无法被回收。
GC Roots的对象有:
虚拟机栈(栈帧中的本地变量表)中引用的对象方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象
可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至 少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是在由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法。
当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否 则,对象“复活”
每个对象只能触发一次finalize()方法
由于finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用,建议遗忘它。
线程的生命周期?线程有几种状态
- 线程通常有五种状态,创建,就绪,运行、阻塞和死亡状态。
- 阻塞的情况又分为三种:
- 、等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是object类的方法
- 、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
- 、其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状 态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
sleep是Thread类的方法
- 新建状态(New):新创建了一个线程对象。
- 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
- 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
- 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
- 死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期。
sleep()、wait()、join()、yield()的区别
锁池
所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线 程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到 后会进入就绪队列进行等待cpu资源分配。
等待池
当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了
notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放 到锁池,而notifyAll()是将等待池的所有线程放到锁池当中
1、sleep 是 Thread 类的静态本地方法,wait 则是 Object 类的本地方法。
2、sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
sleep就是把cpu的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回cpu资源,参与cpu 的调度,获取到cpu资源后就可以继续运行了。而如果sleep时该线程有锁,那么sleep不会释放这个锁,而 是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁。也就是说无法执行程 序。如果在睡眠期间其他线程调用了这个线程的interrupt方法,那么这个线程也会抛出
interruptexception异常返回,这点和wait是一样的。
3、sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
4、sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。
5、sleep 一般用于当前线程休眠,或者轮循暂停操作,wait 则多用于多线程之间的通信。
6、sleep 会让出 CPU 执行时间且强制上下文切换,而 wait 则不一定,wait 后可能还是有机会重新竞争到锁继续执行的。
yield()执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格, 所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行
join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队列,直到线程A结束或中断线程
public static void main(String[] args) throws InterruptedException Thread t1 = new Thread(new Runnable()
@Override
public void run() try
Thread.sleep(3000);
catch (InterruptedException e)
e.printStackTrace();
System.out.println("22222222");
);
t1.start();
t1.join();
// 这行代码必须要等t1全部执行完毕,才会执行
System.out.println("1111");
22222222
1111
对线程安全的理解
不是线程安全、应该是内存安全,堆是共享内存,可以被所有线程访问
当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获 得正确的结果,我们就说这个对象是线程安全的
堆是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分 配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了 要还给操作系统,要不然就是内存泄漏。
在Java中,堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚 拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及 数组都在这里分配内存。
栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈 互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语 言里面显式的分配和释放。
目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己 的内存空间,而不能访问别的进程的,这是由操作系统保障的。
在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以 访问到该区域,这就是造成问题的潜在原因。
Thread、Runable的区别
Thread和Runnable的实质是继承关系,没有可比性。无论使用Runnable还是Thread,都会new
Thread,然后执行run方法。用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执行一个任务,那就实现runnable。
//会卖出多一倍的票public class Test
public static void main(String[] args)
// TODO Auto-generated method stub
new MyThread().start(); new MyThread().start();
static class MyThread extends Thread private int ticket = 5;
public void run() while(true)
System.out.println("Thread ticket = " + ticket--); if(ticket < 0)
break;
//正常卖出
public class Test2
public static void main(String[] args)
// TODO Auto-generated method stub MyThread2 mt=new MyThread2();
new Thread(mt).start(); new Thread(mt).start();
static class MyThread2 implements Runnable private int ticket = 5;
public void run() while(true)
System.out.println("Runnable ticket = " + ticket--); if(ticket < 0)
break;
原因是:MyThread创建了两个实例,自然会卖出两倍,属于用法错误
对守护线程的理解
守护线程:为所有非守护线程提供服务的线程;任何一个守护线程都是整个JVM中所有非守护线程的保姆;
守护线程类似于整个进程的一个默默无闻的小喽喽;它的生死无关重要,它却依赖整个进程而运行;哪 天其他线程结束了,没有要执行的了,程序就结束了,理都没理守护线程,就把它中断了;
注意: 由于守护线程的终止是自身无法控制的,因此千万不要把IO、File等重要操作逻辑分配给它;因为它不靠谱;
守护线程的作用是什么?
举例, GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线 程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
应用场景:(1)来为其它线程提供服务支持的情况;(2) 或者在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要 正确地关闭掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。通常都 是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。
thread.setDaemon(true)必须在thread.start(之前设置,否则会跑出一个
IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
在Daemon线程中产生的新线程也是Daemon的。
守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作 的中间发生中断。
Java自带的多线程框架,比如ExecutorService,会将守护线程转换为用户线程,所以如果要使用后台线 程就不能用Java的线程池。
ThreadLocal的原理和使用场景
每一个Thread 对象均含有一个ThreadLocalMap 类型的成员变量threadLocals ,它存储本线程中所有ThreadLocal对象及其对应的值
ThreadLocalMap 由一个个Entry 对象构成
Entry 继承自WeakReference<ThreadLocal<?>> ,一个Entry 由ThreadLocal 对象和Object 构成。由此可见, Entry 的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该
key就会被垃圾收集器回收
当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。
get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap
对象。再以当前ThreadLocal对象为key,获取对应的value。
由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在 线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。
使用场景:
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。
Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的
connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种 隔离
ThreadLocal内存泄露原因,如何避免
内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露 堆积后果很严重,无论多少内存,迟早会被占光,
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
强引用:使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,
Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用
java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。
ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal
实例,value为线程变量的副本
hreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时, Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这 些key为null的Entry的value就会一直存在一条强引用链(红色链条)
key 使用强引用
当hreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强 引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
key 使用弱引用
当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱 引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal正确的使用方法
每次使用完ThreadLocal都调用它的remove()方法清除数据
将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
并发、并行、串行的区别
串行在时间上不可能发生重叠,前一个任务没搞定,下一个任务就只能等着并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行。
并发允许两个任务彼此干扰。统一时间点、只有一个任务运行,交替执行
并发的三大特性
原子性
原子性是指在一个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要 不都不执行。就好比转账,从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元, 往账户B加上1000元。2个操作必须全部完成。
private long count = 0;
public void calc() count++;
1:将 count 从主存读到工作内存中的副本中
2:+1的运算
3:将结果写入工作内存
4:将工作内存的值刷回主存(什么时候刷入由操作系统决定,不确定的)
那程序中原子性指的是最小的操作单元,比如自增操作,它本身其实并不是原子性操作,分了3步的, 包括读取变量的原始值、进行加1操作、写入工作内存。所以在多线程中,有可能一个线程还没自增
完,可能才执行到第二部,另一个线程就已经读取了值,导致结果错误。那如果我们能保证自增操作是 一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据。
关键字:synchronized
可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。
//线程1
boolean stop = false; while(!stop)
doSomething();
//线程2
stop = true;
如果线程2改变了stop的值,线程1一定会停止吗?不一定。当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
关键字:volatile、synchronized、final
有序性
虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按 照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对 变量的值没有造成影响,但有可能会出现线程安全问题。
int a = 0;
bool flag = false;
public void write()
a = 2; //1
flag = true; //2
public void multiply() if (flag) //3
int ret = a * a;//4
write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果, 再到线程1,这时候a才赋值为2,很明显迟了一步
关键字:volatile、synchronized
volatile本身就包含了禁止指令重排序的语义,而synchronized关键字是由“一个变量在同一时刻只允许 一条线程对其进行lock操作”这条规则明确的。
synchronized关键字同时满足以上三种特性,但是volatile关键字不满足原子性。
在某些情况下,volatile的同步机制的性能确实要优于锁(使用synchronized关键字或
java.util.concurrent包里面的锁),因为volatile的总开销要比锁低。
我们判断使用volatile还是加锁的唯一依据就是volatile的语义能否满足使用的场景(原子性)
volatile
- 保证被volatile修饰的共享变量对所有线程总是可见的,也就是当一个线程修改了一个被volatile修 饰共享变量的值,新值总是可以被其他线程立即得知。
//线程1
boolean stop = false; while(!stop)
doSomething();
//线程2
stop = true;
如果线程2改变了stop的值,线程1一定会停止吗?不一定。当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
● 禁止指令重排序优化。
int a = 0;
bool flag = false;
public void write()
a = 2; //1
flag = true; //2
public void multiply() if (flag) //3
int ret = a * a;//4
write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果, 再到线程1,这时候a才赋值为2,很明显迟了一步。
但是用volatile修饰之后就变得不一样了
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
其实是两个步骤,先加加,然后再赋值。不是原子性操作,所以volatile不能保证线程安全。
inc++;
为什么用线程池?解释下线程池参数?
1、降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。
2、提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。
3、提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控。
代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会 消除,而是一种常驻线程
corePoolSize
代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程
maxinumPoolSize
数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但 是线程池内线程总数不会超过最大线程数
keepAliveTime 、 unit 表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过
来设置空闲时间
setKeepAliveTime
workQueue 用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程
ThreadFactory 实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建
工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择 自定义线程工厂,一般我们会根据业务来制定不同的线程工厂
Handler 任务拒绝策略,有两种情况,第一种是当我们调用shutdown 等方法关闭线程池后,这
时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程 池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提 交的任务时,这是也就拒绝
简述线程池处理流程
线程池中阻塞队列的作用?为什么是先添加列队而不是先创建最大线程?
1、一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务 了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。
阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用cpu资源
2、在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。
就好比一个企业里面有10个(core)正式工的名额,最多招10个正式工,要是任务超过正式工人数
(task > core)的情况下,工厂领导(线程池)不是首先扩招工人,还是这10人,但是任务可以稍微积压一下,即先放到队列去(代价低)。10个正式工慢慢干,迟早会干完的,要是任务还在继续增加,超 过正式工的加班忍耐极限了(队列满了),就的招外包帮忙了(注意是临时工)要是正式工加上外包还 是不能完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)。
线程池中线程复用原理
线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对
Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的 run 方法串联起来。
如何实现一个IOC容器
1、配置文件配置包扫描路径
2、递归包扫描获取.class文件
3、反射、确定需要交给IOC管理的类
4、对需要注入的类进行依赖注入
配置文件中指定需要扫描的包路径
定义一些注解,分别表示访问控制层、业务服务层、数据持久层、依赖注入注解、获取配置文件注 解
从配置文件中获取需要扫描的包路径,获取到当前路径下的文件信息及文件夹信息,我们将当前路 径下所有以.class结尾的文件添加到一个Set集合中进行存储
遍历这个set集合,获取在类上有指定注解的类,并将其交给IOC容器,定义一个安全的Map用来存储这些对象
遍历这个IOC容器,获取到每一个类的实例,判断里面是有有依赖其他的类的实例,然后进行递归注入
spring是什么?
轻量级的开源的J2EE框架。它是一个容器框架,用来装javabean(java对象),中间层框架(万能胶) 可以起一个连接作用,比如说把Struts和hibernate粘合在一起运用,可以让我们的企业开发更快、更简洁
Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架
--从大小与开销两方面而言Spring都是轻量级的。
--通过控制反转(IoC)的技术达到松耦合的目的
--提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务进行内聚性的
开发
--包含并管理应用对象(Bean)的配置和生命周期,这个意义上是一个容器。
--将简单的组件配置、组合成为复杂的应用,这个意义上是一个框架。
谈谈你对AOP的理解
系统是由许多不同的组件所组成的,每一个组件各负责一块特定功能。除了实现自身核心功能之外,这 些组件还经常承担着额外的职责。例如日志、事务管理和安全这样的核心服务经常融入到自身具有核心 业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为它们会跨越系统的多个组件。
当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。
日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。 在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP:将程序中的交叉业务逻辑(比如安全,日志,事务等),封装成一个切面,然后注入到目标对象
(具体业务逻辑)中去。AOP可以对某个对象或某些对象的功能进行增强,比如对象中的方法进行增强,可以在执行某个方法之前额外的做一些事情,在某个方法执行之后额外的做一些事情
谈谈你对IOC的理解
容器概念、控制反转、依赖注入
ioc容器:实际上就是个map(key,value),里面存的是各种对象(在xml里配置的bean节点、
@repository、@service、@controller、@component),在项目启动的时候会读取配置文件里面的bean节点,根据全限定类名使用反射创建对象放到map里、扫描到打上上述注解的类还是通过反射创建对象放到map里。
这个时候map里就有各种对象了,接下来我们在代码里需要用到里面的对象时,再通过DI注入
(autowired、resource等注解,xml里bean节点内的ref属性,项目启动的时候会读取xml节点ref属性根据id注入,也会扫描这些注解,根据类型或id注入;id就是对象名)。
控制反转:
没有引入IOC容器之前,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。
引入IOC容器之后,对象A与对象B之间失去了直接联系,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。
通过前后的对比,不难看出来:对象A获得依赖对象B的过程,由主动行为变为了被动行为, 以上是关于面经软件测试岗位常见面试题全套合集系列4-1的主要内容,如果未能解决你的问题,请参考以下文章