基本概念
什么是面向对象编程?
通过建模形式抽象思维过程的编程方法。面向对象编程的核心思想体现在四个方面:抽象、封装、继承、多态。
- 抽象
- 抽象是定义对象的属性和行为,具体体现为将同一类对象(模型)的数据和行为定义为类。
- 封装
- 封装是一种信息隐蔽技术,隐藏一切可隐藏的数据或行为,只对外提供最简单的操作数据的接口。
- 继承
- 继承是从已有类得到继承信息创建新类的过程。继承不仅支持了系统的可重用性,而且还促进了系统的可扩充性。
- 多态
- 多态是指允许不同子类型的对象对同一消息作出不同的响应。
操作系统中heap和stack的区别?
- 功能方面,Stack空间用来存储基础类型的值和对象引用,Heap则用来存储对象和数组。
- 使用方面,Stack空间由操作系统自动分配释放,Heap空间由程序员手动申请释放。
- 限制方面,Stack空间有限,Heap则是空间很大的自由区。
- 效率方面,Stack简单、速度快;Heap灵活,但是速度慢且容易产生内存碎片。
什么是AOP?
切面编程(AOP)是通过切面非侵入式的操作程序方法,常用的场景如日志记录、权限判断、读写分离等,其原理是基于动态代理技术实现,通过ProxyBeanFactory配置工厂bean,将实现MethodInterceptor接口的拦截器链织入到代理Bean中,完成方法调用拦截。具体实现是通过@AspectJ注解将Bean转换为一个aspectj切面,通过@Pointcut、@Before、@After、@Around注解定制具体的切面织入的内容。
什么是IOC?
IOC/DI:控制反转,本质是将原来程序中对象创建、依赖的代码,反转交由容器去协助实现以达到统一管理对象的目的。Spring中的IOC容器,它的主要作用是完成对象的创建和依赖的管理注入等等。
依赖注入可以通过setter方法注入(设值注入)、构造器注入和接口注入三种方式来实现,Spring支持setter注入和构造器注入,通常使用构造器注入来注入必须的依赖关系,对于可选的依赖关系,则setter注入是更好的选择,setter注入需要类提供无参构造器或者无参的静态工厂方法来创建对象。
什么是ORM?
对象关系映射(Object-Relational Mapping,简称ORM)是一种为了解决程序的面向对象模型与数据库的关系模型互不匹配问题的技术;简单的说,ORM是将程序中的对象自动持久化到关系数据库中或者将关系数据库表中的行转换成Java对象,其本质上就是将数据从一种形式转换到另外一种形式。
什么是反射?
反射是指程序可以访问、检测和修改它本身状态或行为的一种能力,反射的用途主要有:获取类型的相关信息,动态调用方法,动态构造对象,从程序集中获得类型。
事务特性ACID是什么?
- 原子性(Atomic):事务中各项操作,要么全做要么全不做,任何一项操作的失败都会导致整个事务的失败;
- 一致性(Consistent):事务结束后系统状态是一致的;
- 隔离性(Isolated):并发执行的事务彼此无法看到对方的中间状态;
- 持久性(Durable):事务完成后所做的改动都会被持久化,即使发生灾难性的失败。通过日志和同步备份可以在故障发生后重建数据。
数据库隔离级别是有哪些?
数据库为我们提供的四种隔离级别:
- Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
- Repeatable read (可重复读):可避免脏读、不可重复读的发生。
- Read committed (读已提交):可避免脏读的发生。
- Read uncommitted (读未提交):最低级别,任何情况都无法保证。

什么是脏读、幻读、不可重复读、第一类丢失更新、第二类丢失更新?
- 脏读:指在一个事务处理过程里读取了另一个未提交的事务中的数据。
- 不可重复读:在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
- 幻读:幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。
- 第一类丢失更新:事务A撤销时,覆盖事务B已经提交的数据,造成事务B所做操作丢失。
- 第二类丢失更新:事务A提交时,覆盖事务B已经提交的数据,造成事务B所做操作丢失。
阐述下SOLID原则?
- 单一职责原则(Single Responsibility Principle):不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。
- 开闭原则(Open-Close Principle):一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
- 里氏替换原则(Liskov Substitution Priciple):子类可以扩展父类的功能,但不能改变父类原有的功能。
子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。子类中可以增加自己特有的方法。当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。 - 依赖倒置原则(Dependence Inversion Principle):要依赖于抽象,不要依赖于具体。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
- 接口隔离原则(Interface Segregation Principle):拆分非常庞大臃肿的接口成为更小的和更具体的接口,使用多个专门的接口比使用单一的总接口要好。目的是系统解开耦合,从而容易重构,更改和重新部署。
- 最少知识原则(Least Knowledge Principle):迪米特法则(Law of Demeter)又叫作最少知道原则(Least Knowledge Principle 简写LKP),就是说一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。门面模式和调停者模式实际上就是迪米特法则的应用。广义的迪米特法则在类的设计上的体现:优先考虑将一个类设置成不变类。尽量降低一个类的访问权限。谨慎使用Serializable。尽量降低成员的访问权限。
BS、CS、P2P架构的联系与区别?
典型的网络应用模式大致有三类:B/S、C/S、P2P。其中B代表浏览器(Browser)、C代表客户端(Client)、S代表服务器(Server),P2P是对等模式,不区分客户端和服务器。
- B/S应用模式中可以视为特殊的C/S应用模式,只是将C/S应用模式中的特殊的客户端换成了浏览器,因为几乎所有的系统上都有浏览器,那么只要打开浏览器就可以使用应用,没有安装、配置、升级客户端所带来的各种开销。
- P2P应用模式中,成千上万台彼此连接的计算机都处于对等的地位,整个网络一般来说不依赖专用的集中服务器。网络中的每一台计算机既能充当网络服务的请求者,又对其它计算机的请求作出响应,提供资源和服务。通常这些资源和服务包括:信息的共享和交换、计算资源(如CPU的共享)、存储共享(如缓存和磁盘空间的使用)等,这种应用模式最大的阻力安全性、版本等问题,目前有很多应用都混合使用了多种应用模型,最常见的网络视频应用,它几乎把三种模式都用上了。
补充:此题要跟”电子商务模式”区分开,因为有很多人被问到这个问题的时候马上想到的是B2B(如阿里巴巴)、B2C(如当当、亚马逊、京东)、C2C(如淘宝、拍拍)、C2B(如威客)、O2O(如美团、饿了么)。对于这类问题,可以去百度上面科普一下。
什么是SOAP、WSDL、UDDI?
- SOAP:简单对象访问协议(Simple Object Access Protocol),是Web Service中交换数据的一种协议规范。
- WSDL:Web服务描述语言(Web Service Description Language),它描述了Web服务的公共接口。这是一个基于XML的关于如何与Web服务通讯和使用的服务描述;也就是描述与目录中列出的Web服务进行交互时需要绑定的协议和信息格式。通常采用抽象语言描述该服务支持的操作和信息,使用的时候再将实际的网络协议和信息格式绑定给该服务。
- UDDI:统一描述、发现和集成(Universal Description, Discovery and Integration),它是一个基于XML的跨平台的描述规范,可以使世界范围内的企业在互联网上发布自己所提供的服务。简单的说,UDDI是访问各种WSDL的一个门面(可以参考设计模式中的门面模式)。
XML与JSON的区别?
XML与JSON都是简单、人类可读的数据格式,二者应用场景有所不同:
- XML更适合数据定义、数据存储、高级检索(XPath),由于其标记语言标准、规范、强类型的特性使其更易于被机器理解、实现跨平台交互,HTML是XML应用成功的有力案例(其实大部分UI语言XAML等使用JSON作为数据交换语言都是一场灾难,因为相对于XML,JSON由于其弱类型的特性,致使它在数据定义、数据检查、数据检索等方面要差的多)。
- JSON则更适合于数据传输,由于其作为一种轻量级数据交换语言的特性,其占用空间更少、传输和解析速度更快,所以现在大部分API服务都在使用JSON作为数据传输语言,例如:微信、高德。
JRE、JDK、JVM、JIT的联系与区别?
- JRE代表 Java 运行时(Java run-time),是运行 Java 引用所必须的。
- JDK代表 Java 开发工具(Java development kit),是 Java 程序的开发工具,如 Java 编译器,它也包含 JRE。
- JVM代表 Java 虚拟机(Java virtual machine),它的责任是运行 Java 应用。
- JIT 代表即时编译(Just In Time compilation),当代码执行的次数超过一定的阈值时,会将 Java 字节码转换为本地代码,如,主要的热点代码会被转换为本地代码,这样有利大幅度提高 Java 应用的性能。
MVC是什么,具体如何实现?
Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写。目前我们采用的是Spring MVC框架,其中控制器部分由Servlet路由的具体Controller Bean负责,视图则采用的是velocity框架,模型则采用的是Pojo实体对象,Controller Bean通过服务层取得具体业务数据填充到Pojo实体对象,交由velocity框架对View和Model进行绑定填充,最后将响应内容通过Servlet反馈到客户端。
RPC 和 RMI 区别?
RPC是远程过程调用协议,通过网络从远程计算机上请求调用某种服务,具体应用有Dubbo;RMI是远程方法调用框架,JAVA中通过RMI实现跨JVM进行远程方法调用。二者的区别是:
- 二者方法调用的方式不同,RPC是通过网络协议向远程主机发送请求,而RMI则是通过在客户端的Stub对象作为接口进行远程方法调用。
- 二者适用语言范围不同,RPC是网络协议,与操作系统和语言无关,而RMI则只适用于JAVA。
- 二者调用结果的返回形式不同,RPC传送的消息由外部数据表示 (ExternalData Representation,XDR) 语言表示,这种语言抽象了字节序类和数据类型结构之间的差异。只有由 XDR 定义的数据类型才能被传递, RPC 不允许传递对象。而RMI调用远程对象方法,允许方法返回 Java 对象以及基本数据类型。
实现会话跟踪的技术有哪些?
由于HTTP协议本身是无状态的,服务器为了区分不同的用户,就需要对用户会话进行跟踪,简单的说就是为用户进行登记,为用户分配唯一的ID,下一次用户在请求中包含此ID,服务器据此判断到底是哪一个用户。
- URL 重写:在URL中添加用户会话的信息作为请求的参数,或者将唯一的会话ID添加到URL结尾以标识一个会话。
- 设置表单隐藏域:将和会话跟踪相关的字段添加到隐式表单域中,这些信息不会在浏览器中显示但是提交表单时会提交给服务器。
这两种方式很难处理跨越多个页面的信息传递,因为如果每次都要修改URL或在页面中添加隐式表单域来存储用户会话相关信息,事情将变得非常麻烦。 - cookie:cookie有两种,一种是基于窗口的,浏览器窗口关闭后,cookie就没有了;另一种是将信息存储在一个临时文件中,并设置存在的时间。当用户通过浏览器和服务器建立一次会话后,会话ID就会随响应信息返回存储在基于窗口的cookie中,那就意味着只要浏览器没有关闭,会话没有超时,下一次请求时这个会话ID又会提交给服务器让服务器识别用户身份。会话中可以为用户保存信息。会话对象是在服务器内存中的,而基于窗口的cookie是在客户端内存中的。如果浏览器禁用了cookie,那么就需要通过下面两种方式进行会话跟踪。当然,在使用cookie时要注意几点:首先不要在cookie中存放敏感信息;其次cookie存储的数据量有限(4k),不能将过多的内容存储cookie中;再者浏览器通常只允许一个站点最多存放20个cookie。当然,和用户会话相关的其他信息(除了会话ID)也可以存在cookie方便进行会话跟踪。
- HttpSession:在所有会话跟踪技术中,HttpSession对象是最强大也是功能最多的。当一个用户第一次访问某个网站时会自动创建HttpSession,每个用户可以访问他自己的HttpSession。可以通过HttpServletRequest对象的getSession方法获得HttpSession,通过HttpSession的setAttribute方法可以将一个值放在HttpSession中,通过调用HttpSession对象的getAttribute方法,同时传入属性名就可以获取保存在HttpSession中的对象。与上面三种方式不同的是,HttpSession放在服务器的内存中,因此不要将过大的对象放在里面,即使目前的Servlet容器可以在内存将满时将HttpSession中的对象移到其他存储设备中,但是这样势必影响性能。添加到HttpSession中的值可以是任意Java对象,这个对象最好实现了Serializable接口,这样Servlet容器在必要的时候可以将其序列化到文件中,否则在序列化时就会出现异常。
HTML5中可以使用Web Storage技术通过JavaScript来保存数据,例如可以使用localStorage和sessionStorage来保存用户会话的信息,也能够实现会话跟踪。
简述一下你了解的设计模式。
- 适配器模式(Adapter):适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作,一般有两种实现方式:类适配器、对象适配器。
- 类适配器:通过继承来实现适配器功能。适配器实现接口A,通过继承接口B访问B接口。
- 对象适配器:通过组合来实现适配器功能。适配器实现接口A,通过组合接口B访问B接口。
- 具体案例:Collections.SynchronizedCollection(Collection
c)通过对象适配器方式返回线程安全的集合对象适配器。
- 观察者模式(Observer):观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
- 具体案例:Spring事件监听机制的实现。
- 具体案例:订单确认后的返券、送积分、记录日志等流程的处理:订单确认完成后会有一系列的后续操作,这些操作的处理依赖订单确认状态的更新,由于可能会增加新后续操作(新增发送订单已确认邮件)或者操作本身产生业务变更(送积分逻辑调整),为了适应后续操作的变更,将订单处理抽象为主题,后续一系列操作抽象为观察者,通过解耦订单处理与后续操作来适应后续操作的变化。
- 模版方法模式(Template):定义一个操作中算法的框架,而将一些步骤延迟到子类中,使得子类可以不改变算法的结构即可重定义该算法中的某些特定步骤。
- 具体案例:Servlet使用:核心处理在HttpServlet.service中,通过继承HttpServlet后重写doGet、doPost实现具体的业务。
- 具体案例:订单运费计算:运费计算算法为分别按照按地区收费、按重收费、满额收费、免邮几种方式计算运费,最后选出最便宜的运费为运费计算方式。承运方式有物流、快递两种方式,运费计算算法一致。采用模版方法模式,订单运费的基本算法定义为模版,不同承运方式的运费具体算法定义为子类,分别实现具体的运费计算方式。
- 命令模式(Command):将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
- 具体案例:多线程的Runnable。
- 工厂模式(Factory):通常由应用程序直接使用new创建新的对象,为了将对象的创建和使用相分离,采用工厂模式,即应用程序将对象的创建及初始化职责交给工厂对象。
- 具体案例:Spring Bean。
- 责任链模式(Chain of Responsibility):为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。
- 具体案例:Servlet 的 Filter。
- 具体案例:View的解析:预处理、各动态标签的解析(每个标签分别为一个责任者)。
基础
String是最基本的数据类型吗?
不是。Java中的基本数据类型只有8个:byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(reference type),Java 5以后引入的枚举类型也算是一种比较特殊的引用类型。
float f=3.4;是否正确?
不正确。3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成float f =3.4F;。
short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗?
对于short s1=1; s1=s1+1;由于1是int类型,因此s1+1运算结果也是int型,需要强制转换类型才能赋值给short型。而short s1=1; s1+=1;可以正确编译,因为s1+=1;相当于s1=(short)(s1 + 1);其中有隐含的强制类型转换。
&和&&的区别?
&运算符有两种用法:(1)按位与;(2)逻辑与。&&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true整个表达式的值才是true。&&之所以称为短路运算是因为,如果&&左边的表达式的值是false,右边的表达式会被直接短路掉,不会进行运算。
Math.round(11.5) 等于多少?Math.round(-11.5)等于多少?
Math.round(11.5)的返回值是12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加0.5然后进行下取整。
switch 是否能作用在byte 上,是否能作用在long 上,是否能作用在String上?
在Java 5以前,switch(expr)中,expr只能是byte、short、char、int。从Java 5开始,Java中引入了枚举类型,expr也可以是enum类型,从Java 7开始,expr还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。
用最有效率的方法计算2乘以8?
2 « 3(左移3位相当于乘以2的3次方,右移3位相当于除以2的3次方)。
对象的hashCode有什么要求?
- 两个相同的对象必须要有相同的hashCode,两个不相等的对象允许有相同的 hashcode 值
- 对象的hashCode必须始终是一致的,不可以是随机数
为什么在重写 equals 方法的时候需要重写 hashCode 方法?
因为有强制的规范指定需要同时重写 hashCode 与 equals 方法,许多容器类,如 HashMap、HashSet 都依赖于 hashCode 与 equals 的规定。
“a==b”和”a.equals(b)”有什么区别?
如果 a 和 b 都是对象,则 a==b 是比较两个对象的引用,只有当 a 和 b 指向的是堆中的同一个对象才会返回 true,而 a.equals(b) 是进行逻辑比较,所以通常需要重写该方法来提供逻辑一致性的比较。例如,String 类重写 equals() 方法,所以可以用于两个不同对象,但是包含的字母相同的比较。
是否可以继承String类?
String 类是final类,不可以被继承。
String和StringBuilder、StringBuffer的区别?
Java平台提供了两种类型的字符串:String和StringBuffer/StringBuilder,它们可以储存和操作字符串。其中String是只读字符串,也就意味着String引用的字符串内容是不能被改变的。而StringBuffer/StringBuilder类表示的字符串对象可以直接进行修改。StringBuilder是Java 5中引入的,它和StringBuffer的方法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方面都没有被synchronized修饰,因此它的效率也比StringBuffer要高。
重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。不能根据返回类型来区分重载。
为什么不能根据返回类型区分重载?
因为调用时如果不能指定返回类型信息时,编译器是不知道你要调用哪个函数的,函数的返回值只是作为函数运行之后的一个“状态”,并不能作为某个方法的“标识”。
char 型变量中能不能存贮一个中文汉字,为什么?
char类型可以存储一个中文汉字,因为Java中使用的编码是Unicode(不选择任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法),一个char类型占2个字节(16比特),所以放一个中文是没问题的。
抽象类(abstract class)和接口(interface)有什么异同?
相同:
- 抽象类和接口都不能够实例化,但可以定义抽象类和接口类型的引用。
- 一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类。
不同: - 接口比抽象类更加抽象,因为抽象类中可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其中的方法全部都是抽象方法。
- 抽象类中的成员可以是private、默认、protected、public的,而接口中的成员全都是public的。
- 抽象类中可以定义成员变量,而接口中定义的成员变量实际上都是常量。有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法。
阐述静态变量和实例变量的区别。
- 静态变量是被static修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷贝。
- 实例变量必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。
是否可以从一个静态(static)方法内部发出对非静态(non-static)方法的调用?
不可以,静态方法只能访问静态成员,因为非静态方法的调用要先创建对象,在调用静态方法时可能对象并没有被初始化。
如何实现对象克隆?
有两种方式:
- 实现Cloneable接口并重写Object类中的clone()方法。
- 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆,代码如下。
final、finalize 和 finally 的不同之处?
- final 是一个修饰符,可以修饰变量、方法和类。如果 final 修饰变量,意味着该变量的值在初始化后不能被改变;修饰类,表示该类不能被继承;修饰方法,表示方法不能被重写。
- finally 是一个关键字,与 try 和 catch 一起用于异常的处理。finally 块一定会被执行,无论在 try 块中是否有发生异常。
- finalize 方法是在对象被回收之前调用的方法,且只会被系统自动调用一次,给对象自己最后一个复活的机会,但是如同对象何时被GC无法被保证,什么时候调用 finalize 也无法保证,使用它实现关闭外部资源等功能完全可以通过finally实现,所以建议忘记这个方法。
Static Nested Class 和Inner Class的不同?
Static nested class(嵌套类)是将内部类声明为static,嵌套类只能访问外部类的static成员,创建对象时只需通过外部类调用构造函数即可:Outer.StaticNestedClass(); Inner Class(内部类)依附于外部类对象,内部不允许有static成员,内部持有一个外部类对象引用,可以直接访问外部类静态、对象成员,创建对象时需要先创建外部类对象,然后再通过外部类对象调用其构造函数:(new Outer()).new InnerClass();
内部类可以引用它的包含类(外部类)的成员吗?有没有什么限制?
一个内部类对象可以访问创建它的外部类对象的成员,包括私有成员。
Anonymous Inner Class(匿名内部类)是否可以继承其它类?是否可以实现接口?
可以继承其他类或实现其他接口,在Android开发中常用此方式来实现事件监听和回调。
Error和Exception有什么区别?
Error表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题;比如内存溢出,不可能指望程序能处理这样的情况;Exception表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题;也就是说,它表示如果程序运行正常,从不会发生的情况。
try{}里有一个return语句,那么紧跟在这个try后的finally{}里的代码会不会被执行,什么时候被执行,在return前还是后?
会执行,在方法返回调用者前执行。
列出一些你常见的运行时异常?
- ArithmeticException(算术异常)
- ClassCastException (类转换异常)
- IllegalArgumentException (非法参数异常)
- IndexOutOfBoundsException (下标越界异常)
- NullPointerException (空指针异常)
- SecurityException (安全异常)
Object有哪些公用方法?
- 方法equals测试的是两个对象是否相等
- 方法clone进行对象拷贝
- 方法getClass返回和当前对象相关的Class对象
- 方法notify,notifyAll,wait都是用来对给定对象进行线程同步的
Java的四种引用,强弱软虚,以及用到的场景?
利用软引用和弱引用解决OOM问题:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。
通过软可及对象重获方法实现Java对象的高速缓存:比如我们创建了一个Employee的类,如果每次需要查询一个雇员的信息。哪怕是几秒中之前刚刚查询过的,都要重新构建一个实例,这是需要消耗很多时间的。我们可以通过软引用和 HashMap 的结合,先是保存引用方面:以软引用的方式对一个Employee对象的实例进行引用并保存该引用到HashMap 上,key 为此雇员的 id,value为这个对象的软引用,另一方面是取出引用,缓存中是否有该Employee实例的软引用,如果有,从软引用中取得。如果没有软引用,或者从软引用中得到的实例是null,重新构建一个实例,并保存对这个新建实例的软引用。
- 强引用:如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。
- 软引用:在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。
- 弱引用:具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象。
- 虚引用:顾名思义,就是形同虚设,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。
Java中如何实现序列化,有什么意义?
序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决对象流读写操作时可能引发的问题(如果不进行序列化可能会存在数据乱序的问题)。
要实现序列化,需要让一个类实现Serializable接口,该接口是一个标识性接口,标注该类对象是可被序列化的,然后使用一个输出流来构造一个对象输出流并通过writeObject(Object)方法就可以将实现对象写出(即保存其状态);如果需要反序列化则可以用一个输入流建立对象输入流,然后通过readObject方法从流中读取对象。序列化除了能够实现对象的持久化之外,还能够用于对象的深度克隆。
Java中有几种类型的流?
字节流和字符流。字节流继承于InputStream、OutputStream,字符流继承于Reader、Writer。在java.io 包中还有许多其他的流,主要是为了提高性能和使用方便。关于Java的I/O需要注意的有两点:一是两种对称性(输入和输出的对称性,字节和字符的对称性);二是两种设计模式(适配器模式和装潢模式)。
集合框架
描述一下集合框架?

集合框架中的泛型有什么优点?
泛型允许我们为集合提供一个可以容纳的对象类型,因此,如果你添加其它类型的任何元素,它会在编译时报错。这避免了在运行时出现ClassCastException,因为你将会在编译时得到报错信息。泛型也使得代码整洁,我们不需要使用显式转换和instanceOf操作符。它也给运行时带来好处,因为不会产生类型检查的字节码指令。
为何Collection不从Cloneable和Serializable接口继承?
Collection接口指定一组对象,对象即为它的元素。如何维护这些元素由Collection的具体实现决定。当与具体实现打交道的时候,克隆或序列化的语义和含义才发挥作用。所以特定的实现应该决定它是否可以被克隆和序列化。
为何Map接口不继承Collection接口?
尽管Map接口和它的实现也是集合框架的一部分,但Map不是集合,集合也不是Map。Map包含key-value对,它提供抽取key或value列表集合的方法,但是它不适合“一组对象”规范。
Iterator是什么?
Iterator接口提供遍历任何Collection的接口。我们可以从一个Collection中使用迭代器方法来获取迭代器实例。迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者在迭代过程中通过Iterator.remove()移除元素。
Enumeration和Iterator接口的区别?
Enumeration的速度是Iterator的两倍,也使用更少的内存。Enumeration是非常基础的,也满足了基础的需要。但是,与Enumeration相比,Iterator更加安全,因为当一个集合正在被遍历的时候,它会阻止其它线程去修改集合。迭代器允许调用者从集合中移除元素,而Enumeration不能做到。
为何没有像Iterator.add()这样的方法,向集合中添加元素?
ListIterator没有提供一个add操作,它要确保迭代的顺序。
实现集合排序的方式有哪些?
- 使用有序集合,如 TreeSet、TreeMap,注意如果不提供Comparator的话,对象需实现Comparable接口,重写其compareTo()方法
- 使用顺序集合,如 List(ArrayList、LinkedList),然后通过 Collections.sort(List
list) 来排序,对象需实现Comparable接口,重写其compareTo()方法
List、Set、Map 和 Queue 之间的区别?
- List 是一个有序集合,允许元素重复,允许 null 元素。
- Set 是一个不包含重复的元素的无序集合,不允许元素重复,允许且只允许有一个 null 元素。
- Map 是一个key-value映射的无序集合,key不允许有重复,,允许且只允许有一个 null key,value允许重复。
- Queue 是一个先入先出(FIFO)的数据结构,也叫队列,有序,允许元素重复,不允许 null 元素。
Queue实现通常不允许插入null元素,尽管某些实现(如LinkedList)并不禁止插入null。即使在允许 null 的实现中,也不应该将null插入到Queue中,因为null也用作poll方法的一个特殊返回值,表明队列不包含元素。
Queue 的 poll() 方法和 remove() 方法的区别?
poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。
Java 中 LinkedHashMap 和 PriorityQueue 的区别是什么?
PriorityQueue 保证最高或者最低优先级的的元素总是在队列头部,但是 LinkedHashMap 维持的顺序是元素插入的顺序。当遍历一个 PriorityQueue 时,没有任何顺序保证,但是 LinkedHashMap 课保证遍历顺序是元素插入的顺序。
ArrayList 与 LinkedList 的不区别?
最明显的区别是 ArrrayList 底层的数据结构是数组,该数据结构支持随机访问,而 LinkedList 的底层数据结构是双向链表,该数据结构不支持随机访问。使用下标访问一个元素,ArrayList 的时间复杂度是 O(1),而 LinkedList 是 O(n)。
写一段代码在遍历 ArrayList 时移除一个元素?
通过Iterator遍历时不可通过Iterator.remove()之外的其他方法进行更新操作,否则会抛出ConcurrentModificationException,注意对集合类进行foreach操作也是通过Iterator实现的。通过索引下标的方式遍历时则可以,但要注意删除元素会导致集合发生变化,遍历索引下标也要随之调整,否则可能无法正确遍历完集合。
for(index=0;index<list.size();index++){
list.get(index);
list.remove(index);
index--;
}
ArrayList 和 HashMap 的默认大小是多少?
在 Java 7 中,ArrayList 的默认大小是 10 个元素,HashMap 的默认大小是16个元素(必须是2的幂)。这就是 Java 7 中 ArrayList 和 HashMap 类的代码片段:
// from ArrayList.java JDK 1.7
private static final int DEFAULT_CAPACITY = 10;
//from HashMap.java JDK 7
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
Iterater和ListIterator之间有什么区别?
- 我们可以使用Iterator来遍历Set和List集合,而ListIterator只能遍历List。
- Iterator只可以向前遍历,而LIstIterator可以双向遍历。
- ListIterator从Iterator接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
通过迭代器fail-fast属性,你明白了什么?
每次我们尝试获取下一个元素的时候,Iterator fail-fast属性检查当前集合结构里的任何改动。如果发现任何改动,它抛出ConcurrentModificationException。
fail-fast与fail-safe有什么区别?
Iterator的fail-fast属性与当前的集合共同起作用,因此它不会受到集合中任何改动的影响。Java.util包中的所有集合类都被设计为fail-fast的,而java.util.concurrent中的集合类都为fail-safe的。Fail-fast迭代器抛出ConcurrentModificationException,而fail-safe迭代器从不抛出ConcurrentModificationException。
在迭代一个集合的时候,如何避免ConcurrentModificationException?
在遍历一个集合的时候,我们可以使用并发集合类来避免ConcurrentModificationException,比如使用CopyOnWriteArrayList,而不是ArrayList。
UnsupportedOperationException是什么?
UnsupportedOperationException是用于表明操作不支持的异常。在JDK类中已被大量运用,在集合框架java.util.Collections.UnmodifiableCollection将会在所有add和remove操作中抛出这个异常。
在Java中,HashMap是如何工作的?
HashMap底层实现是数组,数组的每一项是一条链表(Java8中是链表或者红黑树),这样设计的目的是为了综合数组的快速访问性和链表的快速存储性。 HashMap通过hash的方法,使用put和get存储和获取对象。
- 存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。
- 获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来。
Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。
碰撞:调用hashCode计算hash从而得到bucket位置处已存在元素的情况称为发生碰撞,即存在相同hashcode的元素。
在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。
ConcurrentHashMap与HashMap有何不同,它是如何工作的?
HashMap是非线程安全的,ConcurrentHashMap则是线程安全的。
ConcurrentHashMap在JDK1.8和JDK1.7中实现是不同。
JDK1.8
JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap。
通过Synchronized和CAS实现线程安全的put和get:
- put
- 如果没有初始化就先调用initTable()方法来进行初始化过程
- 如果没有hash冲突就直接CAS插入
- 如果还在进行扩容操作就先进行扩容
- 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
- 最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环
- 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容
- get
- 计算hash值,定位到该table索引位置,如果是首节点符合就返回
- 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
- 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null
JDK1.7
HashMap的数据结构是Map.Entry数组组成,每个Map.Entry是一条链表。ConcurrentHashMap的数据结构则是由一个Segment数组和多个HashEntry组成。
通过Segment锁和hash实现线程安全的put和get:
- 当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒。
- ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null。
ConcurrentHashMap通过bucket锁分离技术,提升HashMap在并发环境下的读写能力。
当一个集合被作为参数传递给一个函数时,如何才可以确保函数不能修改它?
在作为参数传递之前,我们可以使用Collections.unmodifiableCollection(Collection c)方法创建一个只读集合,这将确保改变集合的任何操作都会抛出UnsupportedOperationException。
我们如何从给定集合那里创建一个synchronized的集合?
我们可以使用Collections.synchronizedCollection(Collection c)根据指定集合来获取一个synchronized(线程安全的)集合。
集合框架里实现的通用算法有哪些?
Java集合框架提供常用的算法实现,比如排序和搜索。Collections类包含这些方法实现。大部分算法是操作List的,但一部分对所有类型的集合都是可用的。部分算法有排序、搜索、混编、最大最小值。
与Java集合框架相关的有哪些最好的实践?
- 根据需要选择正确的集合类型。比如,如果指定了大小,我们会选用Array而非ArrayList。如果我们想根据插入顺序遍历一个Map,我们需要使用TreeMap。如果我们不想重复,我们应该使用Set。
- 一些集合类允许指定初始容量,所以如果我们能够估计到存储元素的数量,我们可以使用它,就避免了重新哈希或大小调整。
- 基于接口编程,而非基于实现编程,它允许我们后来轻易地改变实现。
- 总是使用类型安全的泛型,避免在运行时出现ClassCastException。
- 使用JDK提供的不可变类作为Map的key,可以避免自己实现hashCode()和equals()。
- 尽可能使用Collections工具类,或者获取只读、同步或空的集合,而非编写自己的实现。它将会提供代码重用性,它有着更好的稳定性和可维护性。
多线程
线程和进程有什么区别?
线程是进程的子集,线程是操作系统能够进行运算调度的最小单位,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。
多线程的具体应用场景有哪些?实际中需要注意些什么?
- 后台任务,例如:发短信、发邮件、发MQ消息、记录日志。
- 并行计算,例如:调度任务处理、处理海量数据(统计用户历史订单消费总额,计算加盟商月度服务费等)。
- WEB开发,例如:Servlet容器本身就是多线程环境,只是由于大部分情况我们都是基于使用无状态单例Bean处理业务,从而避免了处理线程安全等问题。 当然,多线程给我们带来提高处理能力,增加性能,充分利用服务器资源等众多好处的同时,也有许多要注意的地方:
- 既然我们使用了多线程,那我们就对数据的实时性不那么严格,包括方法的返回值、数据的入库操作等。所以说,使用多线程的方法一定要做好日志记录,因为发生了异常,前端是反馈不到的。当然,多线程也是可以有返回值的,可以返回Future,里面的get方法可以阻塞到该线程执行完毕。
- 非常重要的一点就是线程安全的问题了,在多线程高并发情况下,如果处理不好,可能会出现些我们意想不到的结果。
- 对于自己不确定有多少线程的操作,一定要使用线程池,不然不但提高不了性能,反而会降低性能(线程切换过频耗时等),甚至会导致jvm内存溢出。
- 对于线程池大小的问题,理论上通过区分是IO密集还是CPU密集型制定,实际使用中通过测试达到自己的要求即可。
什么是线程同步、异步?
- 线程同步是多个线程同时访问共享资源时,等待资源访问结束。多线程访问共享资源时,需要通过线程同步机制将并行访问调整为串行访问,以避免造成数据不一致问题。
- 线程异步是多个线程在访问资源时在空闲等待时同时访问其他资源。多线程访问共享资源时,如果资源被其他线程占用,不再等待,继续访问其他资源,以提高效率,节省时间。
什么是线程安全?具体聊聊你是如何发现和处理线程安全问题的?
多线程访问同一代码,不会产生不确定的结果,我们就称其为是线程安全的。
线程安全主要从原子性、可见性、顺序性三个角度考虑:
- 原子性角度的具体案例有数据库事务处理问题(相关联的业务操作未放在同一事务中导致脏读现象:下单时同步订单支付状态与扣减余额操作未放在同一事务导致调度同步支付失败订单)、多线程下的i++问题(i++为多步操作,无法保证原子性,采用AtomicInteger.incrementAndGet()的CAS机制保证原子操作)、秒杀的扣减库存问题(秒杀活动库存扣减为多步操作,无法保证原子性,采用Redis的原子操作incrBy);
- 可见性角度的具体案例有volatile、ThreadLocal,并发场景下的单例模式(通过双重检查加锁的方式实现,其中通过volatile的可见性实现多线程下的单例检查,通过volatile的避免重排序性避免返回未初始化的对象引起问题)
public class Singleton { public static volatile Singleton singleton; private Singleton() {}; public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); //包含三个步骤,分配内存并返回地址、初始化对象、赋值地址给singleton引用, //这三个步骤可能会被重排序成 分配内存并返回地址、赋值地址给singleton引用、初始化对象;这可能会导致其他线程直接使用未初始化的对象而出现问题。 } } } return singleton; } }ThreadLocal 保证不同线程拥有不同实例,相同线程一定拥有相同的实例,即为每一个使用该变量的线程提供一个该变量值的副本,每一个线程都可以独立改变自己的副本,而不是与其它线程的副本冲突。
- 顺序性角度的具体案例有数据库事务死锁问题(多个用户同时对相同的多个商品下单扣减库存时,由于扣减商品顺序不同导致多个事务互相抢占行排它锁而发生的死锁现象,扣减库存时通过对扣减商品进行排序可以避免相互抢占导致的死锁情况)
线程的基本状态以及状态之间的关系?

说明:其中Running表示运行状态,Runnable表示就绪状态(万事俱备,只欠CPU),Blocked表示阻塞状态,阻塞状态又有多种情况,可能是因为调用wait()方法进入等待池,也可能是执行同步方法或同步代码块进入等锁池,或者是调用了sleep()方法或join()方法等待休眠或其他线程结束,或是因为发生了I/O中断。
- 新建状态(New)
用new语句创建的线程处于新建状态,此时它和其他Java对象一样,仅仅在堆区中被分配了内存。 - 就绪状态(Runnable)
当一个线程对象创建后,其他线程调用它的start()方法,该线程就进入就绪状态,Java虚拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中,等待获得CPU的使用权。 - 运行状态(Running)
处于这个状态的线程占用CPU,执行程序代码。只有处于就绪状态的线程才有机会转到运行状态。 - 阻塞状态(Blocked)
阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU。直到线程重新进入就绪状态,它才有机会转到运行状态。 阻塞状态可分为以下3种:- 位于对象等待池中的阻塞状态(Blocked in object’s wait pool):当线程处于运行状态时,如果执行了某个对象的wait()方法,Java虚拟机就会把线程放到这个对象的等待池中,这涉及到“线程通信”的内容。
- 位于对象锁池中的阻塞状态(Blocked in object’s lock pool):当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中,这涉及到“线程同步”的内容。
- 其他阻塞状态(Otherwise Blocked):当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求时,就会进入这个状态。
- 死亡状态(Dead)
当线程退出run()方法时,就进入死亡状态,该线程结束生命周期。
死锁是什么?如何避免死锁?
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
从死锁的四个必要条件来看,破坏其中的任意一个条件就可以避免死锁。但互斥条件是由资源本身决定的,不剥夺条件一般无法破坏,要实现的话得自己写更多的逻辑。
- 避免无限期的锁:用Lock.tryLock(),wait/notify等锁定时设定超时时间,避免无限期锁定。
- 注意锁的顺序:以固定的顺序获取锁,可以避免死锁。
- 降低锁粒度:即只对有请求的资源进行封锁。尽可能的降低对资源封锁的粒度,避免对请求无关的资源进行封锁。
- 少用锁或不用锁:最后,如果能避免使用多个锁,甚至写出无锁的线程安全程序是再好不过了。
Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行,它们有什么区别?
sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态)。
wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。
编写多线程程序有几种实现方式?
Java 5以前实现多线程有两种实现方法:一种是继承Thread类;另一种是实现Runnable接口。
两种方式都要通过重写run()方法来定义线程的行为,推荐使用后者,因为Java中的继承是单继承,一个类有一个父类,如果继承了Thread类就无法再继承其他类了,显然使用Runnable接口更为灵活。注意:Thread类也是Runnable接口的子类。
Java 5以后创建线程还有第三种方式:实现Callable接口,配合线程池框架使用,该接口中的call方法可以在线程执行结束时产生一个返回值。
ExecutorService threadPool = Executors.newSingleThreadExecutor();
Future<String> future = threadPool.submit(new MyThread());
线程同步有哪些方式?
- 使用synchronized(支持方法、代码块):基于对象锁机制实现线程同步。
- 使用重入锁(ReentrantLock):基于CAS乐观锁实现线程同步。
- 使用wait/notify:基于对象锁机制实现线程同步。
平时项目中使用锁和synchronized比较多,而很少使用volatile,难道就没有保证可见性?
锁和synchronized即可以保证原子性,也可以保证可见性。都是通过保证同一时间只有一个线程执行目标代码段来实现的。
锁和synchronized为何能保证可见性?
根据JDK 7的Java doc中对concurrent包的说明,一个线程的写结果保证对另外线程的读操作可见,只要该写操作可以由happen-before原则推断出在读操作之前发生。
既然锁和synchronized即可保证原子性也可保证可见性,为何还需要volatile?
synchronized和锁需要通过操作系统来仲裁谁获得锁,开销比较高,而volatile开销小很多。因此在只需要保证可见性的条件下,使用volatile的性能要比使用锁和synchronized高得多。
既然锁和synchronized可以保证原子性,为什么还需要AtomicInteger这种的类来保证原子操作?
锁和synchronized需要通过操作系统来仲裁谁获得锁,开销比较高,而AtomicInteger是通过CPU级的CAS操作来保证原子性,开销比较小。所以使用AtomicInteger的目的还是为了提高性能。
除了同步,还有没有别的办法保证线程安全?
有。尽可能避免引起非线程安全的条件——共享变量。如果能从设计上避免共享变量的使用,即可避免非线程安全的发生,也就无须通过锁或者synchronized以及volatile解决原子性、可见性和顺序性的问题。
synchronized即可修饰非静态方式,也可修饰静态方法,还可修饰代码块,有何区别?
synchronized修饰非静态同步方法时,锁住的是当前实例;synchronized修饰静态同步方法时,锁住的是该类的Class对象;synchronized修饰静态代码块时,锁住的是synchronized关键字后面括号内的对象。
synchronized可以对null对象加锁么?
不可以,会报NullPointerException错误。另外要注意synchronized的对象地址最好不要变,否则会出现线程安全问题。
启动一个线程是调用run()还是start()方法?
启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM 调度并执行,这并不意味着线程就会立即运行。run()方法是线程启动后要进行回调(callback)的方法。直接调用run()方法则是当前线程(创建线程对象的线程)下的一次普通调用。
如何在两个线程间共享数据?
你可以通过共享对象来实现这个目的,或者是使用像阻塞队列这样并发的数据结构。
Java中notify 和 notifyAll有什么区别?
notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。
为什么wait, notify 和 notifyAll这些方法不在thread类里面?
JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
为什么wait和notify方法要在同步块中调用?
主要是因为Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。还有一个原因是为了避免wait和notify之间产生竞态条件。
Java中的同步集合与并发集合有什么区别?
同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在Java1.5之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。
什么是线程池? 为什么要使用它?
创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限,无节制的创建线程反而会导致性能降低(线程切换过频、内存溢出)。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。
Java中活锁和死锁有什么区别?
活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。简单的说就是,活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行。
怎么检测一个线程是否拥有锁?
在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。
JVM中哪个参数是用来控制线程的堆栈大小的?
-Xss参数用来控制线程的堆栈大小
Thread类中的yield方法有什么作用?
Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。
如果你提交任务时,线程池队列已满。会时发会生什么?
如果一个任务不能被调度执行那么ThreadPoolExecutor’s submit()方法将会抛出一个RejectedExecutionException异常。
Java线程池中submit() 和 execute()方法有什么区别?
两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。
什么是阻塞式方法?
阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。
Java中的ReadWriteLock是什么?
一般而言,读写锁是用来提升并发程序性能的锁分离技术的成果。Java中的ReadWriteLock是Java 5 中新增的一个接口,一个ReadWriteLock维护一对关联的锁,一个用于只读操作一个用于写。在没有写线程的情况下一个读锁可能会同时被多个读线程持有。写锁是独占的,你可以使用JDK中的ReentrantReadWriteLock来实现这个规则,它最多支持65535个写锁和65535个读锁。
多线程中的忙循环是什么?
忙循环就是程序员用循环让一个线程等待,不像传统方法wait(), sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。
volatile 变量和 atomic 变量有什么不同?
首先,volatile 变量和 atomic 变量看起来很像,但功能却不一样。Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用volatile修饰count变量那么 count++ 操作就不是原子性的。而AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
如果同步块内的线程抛出异常会发生什么?
无论你的同步块是正常还是异常退出的,线程都会释放锁,线程内未处理异常则会中断执行,线程进入dead状态;但对于Lock接口的锁则不会,需要在finally里释放。
如何强制启动一个线程?
目前还没有觉得方法,虽然你可以使用System.gc()来进行垃圾回收,但是不保证能成功。在Java里面没有办法强制启动一个线程,它是被线程调度器控制着且Java没有公布相关的API。
Java中的fork join框架是什么?
fork join框架是JDK7中出现的一款高效的工具,Java开发人员可以通过它充分利用现代服务器上的多处理器。它是专门为了那些可以递归划分成许多子模块设计的,目的是将所有可用的处理能力用来提升程序的性能。fork join框架一个巨大的优势是它使用了工作窃取算法,可以完成更多任务的工作线程可以从其它线程中窃取任务来执行。
简述synchronized 和java.util.concurrent.locks.Lock的异同?
Lock是Java 5以后引入的新的API,和关键字synchronized相比主要相同点:Lock 能完成synchronized所实现的所有功能;
主要不同点:
- Lock有比synchronized更精确的线程语义、更细的锁粒度、更好的性能,而且不强制性的要求一定要获得锁,获取失败后还可以有其他选择,而synchronized必须获取锁才能继续向下执行。
- synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且最好在finally 块中释放(这是释放外部资源的最好的地方)。
你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?
缓存其实就是一个哈希表(Key-Value容器),缓存容器的特性是读多写少,适用于此场景的类似容器有CopyOnWriteArrayList,所以参照CopyOnWriteArrayList实现CopyOnWriteHashMap。
public class CopyOnWriteHashMap<K, V> implements Map<K, V>, Cloneable {
private volatile Map<K, V> internalMap;
public CopyOnWriteHashMap() {
internalMap = new HashMap<K, V>();
}
public V put(K key, V value) {
synchronized (this) {
Map<K, V> newMap = new HashMap<K, V>(internalMap);
V val = newMap.put(key, value);
internalMap = newMap;
return val;
}
}
public V get(Object key) {
return internalMap.get(key);
}
public void putAll(Map<? extends K, ? extends V> newData) {
synchronized (this) {
Map<K, V> newMap = new HashMap<K, V>(internalMap);
newMap.putAll(newData);
internalMap = newMap;
}
}
}
现在有t1、t2两个线程,你怎样保证t2在t1执行完后执行?
可以用Thread.join方法实现。
public class JoinTest {
public static void main(String [] args) throws InterruptedException {
ThreadJoinTest t1 = new ThreadJoinTest("t1");
ThreadJoinTest t2 = new ThreadJoinTest("t2");
t1.start();
/**join的意思是使得放弃当前线程的执行,并返回对应的线程,例如下面代码的意思就是:
程序在main线程中调用t1线程的join方法,则main线程放弃cpu控制权,并返回t1线程继续执行直到线程t1执行完毕
所以结果是t1线程执行完后,才到主线程执行,相当于在main线程中同步t1线程,t1执行完了,main线程才有执行的机会
*/
t1.join();
t2.start();
}
}
class ThreadJoinTest extends Thread{
public ThreadJoinTest(String name){
super(name);
}
@Override
public void run(){
for(int i=0;i<1000;i++){
System.out.println(this.getName() + ":" + i);
}
}
}
什么是乐观锁和悲观锁?
- 乐观锁:乐观锁认为竞争不总是会发生,因此它不需要持有锁,适用于读多写少的场景,可以提高吞吐量。乐观锁采取在写时先读出当前值版本号传入,然后将传入版本号和当前值版本号比较,一样则更新,如果失败则要重复 读-比较-写 的操作,如:CAS(AtomicInt),注意:如果不采用版本号进行比较可能会存在ABA问题。
- 悲观锁:悲观锁认为竞争总是会发生,适用于写多读少的场景,因此每次对某资源进行操作时,都会持有一个独占的锁,然后操作资源,如:synchronized、lock。
什么是happens-before原则?
happens-before是Java内存模型的核心,编译器和处理器基于这种原则对程序执行进行优化(重排序)。重排序优化的基本原则是:先保证正确性,在考虑执行效率问题。具体happen-before的定义如下:
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
- 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。
happens-before规则总共有六条规则: - 程序顺序规则:一个线程中的每个操作,happens-before于随后该线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的获取。
- volatile变量规则:对一个volatile域的写,happens-before于对这个变量的读。
- 传递性:如果A happens-before B,B happens-before C,那么A happens-before C。
- start规则:如果线程A执行线程B的start方法,那么线程A的ThreadB.start()happens-before于线程B的任意操作。
- join规则:如果线程A执行线程B的join方法,那么线程B的任意操作happens-before于线程A从TreadB.join()方法成功返回。
什么是竞争条件?你怎样发现和解决竞争?
当两个或以上的线程对同一个数据进行操作的时候,可能会产生“竞争条件”的现象。
通过分析问题,对问题涉及的共享资源和共享资源所涉及的处理线程进行分析,最终找到具体原因。解决竞争的方式通常是通过对临界区的线程同步来避免竞争。
什么是AQS?
简单说一下AQS,AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。
如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。
AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。
单例模式的线程安全性?
首先要说的是单例模式的线程安全意味着: 某个类的实例在多线程环境下只会被创建一次出来 。单例模式有很多种的写法,我总结一下:
- 饿汉式单例模式的写法:线程安全
- 懒汉式单例模式的写法:非线程安全
- 双检锁单例模式的写法:线程安全
线程辅助类Semaphore、CyclicBarrier、CountDownLatch有什么作用?
这三个线程辅助类都可以起到限制代码段并发数的作用。通过构造函数传入限制并发数的数量,然后通过具体的计数操作统计并发数量。具体使用如下:
- Semaphore是一个信号量,通过 acquire() 获取信号量,信号量内部计数器减1,如果没有就等待;而 release() 释放信号量,信号量内部计数器加1,通过 tryAcquire() 这个方法试图获取信号量,如果能够获取返回true,否则返回false。
- CountDownLatch是一个只能使用一次的屏障,通过 await() 挂起线程,通过 countDown() 进行递减计数,直到计数递减到0则继续执行挂起的线程。到达屏障后,不会重置计数器,所以无法重复使用。
- CyclicBarrier是一个可以循环使用的屏障,通过 await() 挂起线程并自动递减计数,直到计数递减到0则继续执行全部挂起的线程。到达屏障后,执行完全部挂起的线程,自动重置计数器,方便下次重复使用;在未到达屏障时,可以重置计数器,挂起的线程会报BrokenBarrierException。
线程类的构造方法、静态块是被哪个线程调用的?
线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。
如果说上面的说法让你感到困惑,那么我举个例子, 假设Thread2中new了Thread1,main函数中new了Thread2,那么:
- Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的
- Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的
同步方法和同步块,哪个是更好的选择?
同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越少越好。
高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
- 高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换。
- 并发不高、任务执行时间长的业务要区分开看: a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以适当加大线程池中的线程数目,让CPU处理更多的业务 b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和①一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
- 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置请参考②。 最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件(MQ)对任务进行拆分和解耦。
什么是原子操作?Java中的原子操作是什么?
原子操作是不可分割的操作,一个原子操作中间是不会被其他线程打断的,所以不需要同步一个原子操作。多个原子操作合并起来后就不是一个原子操作了,就需要同步了。 i++不是一个原子操作,它包含 读取-修改-写入 操作,在多线程状态下是不安全的。 另外,java内存模型允许将64位的读操作或写操作分解为2个32位的操作,所以对long和double类型的单次读写操作并不是原子的,注意使用volitile使他们成为原子操作。
你将如何使用thread dump?你将如何分析Thread dump?
在linux中你可以使用kill -3 pid,然后thread dump将会打印日志(catalina.out);在windows中你可以使用”CTRL+Break”,在控制台即可查看日志。
常见问题方案:
现象1:cpu高,load高,响应很慢
- 一个请求过程中多次dump
- 对比多次dump文件的runnable线程,如果执行的方法有比较大变化,说明比较正常。如果在执行同一个方法,就有一些问题了。
现象2:查找占用cpu最多的线程信息
- 使用命令: top -H -p pid(pid为被测系统的进程号),找到导致cpu高的线程id。 上述Top命令找到的线程id,对应着dump thread信息中线程的nid,只不过一个是十进制,一个是十六进制。
- 在thread dump中,根据top命令查找的线程id,查找对应的线程堆栈信息。
- 多次dump,比较方法调用
现象3:cpu使用率不高但是响应时间很长
- 进行dump,查看是否有很多thread struck在了i/o、数据库等地方,定位瓶颈原因。
1)Waiting on condition:等待某个资源或条件发生来唤醒自己。比如线程正在sleep,网络读写繁忙而等待。
2)Blocked:阻塞,waiting for monitor entry的线程。
现象4:请求无法响应
- 多次dump,对比是否所有的runnable线程都一直在执行相同的方法,如果是的,锁住了!
Servlet
阐述Servlet和CGI的区别?
Servlet与CGI的区别在于Servlet处于服务器进程中,它通过多线程方式运行其service()方法,一个实例可以服务于多个请求,并且其实例一般不会销毁,而CGI对每个请求都产生新的进程,服务完成后就销毁,所以效率上低于Servlet。
转发(forward)和重定向(redirect)的区别?
forward是容器中控制权的转向,是服务器请求资源,服务器直接访问目标地址的URL,把那个URL 的响应内容读取过来,然后把这些内容再发给浏览器,浏览器根本不知道服务器发送的内容是从哪儿来的,所以它的地址栏中还是原来的地址。redirect就是服务器端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址,因此从浏览器的地址栏中可以看到跳转后的链接地址,很明显redirect无法访问到服务器保护起来资源,但是可以从一个网站redirect到其他网站。forward更加高效,所以在满足需要时尽量使用forward(通过调用RequestDispatcher对象的forward()方法,该对象可以通过ServletRequest对象的getRequestDispatcher()方法获得),并且这样也有助于隐藏实际的链接;在有些情况下,比如需要访问一个其它服务器上的资源,则必须使用重定向(通过HttpServletResponse对象调用其sendRedirect()方法实现)。
过滤器有哪些作用和用法?
Java Web开发中的过滤器(filter)是从Servlet 2.3规范开始增加的功能,并在Servlet 2.4规范中得到增强。对Web应用来说,过滤器是一个驻留在服务器端的Web组件,它可以截取客户端和服务器之间的请求与响应信息,并对这些信息进行过滤。当Web容器接受到一个对资源的请求时,它将判断是否有过滤器与这个资源相关联。如果有,那么容器将把请求交给过滤器进行处理。在过滤器中,你可以改变请求的内容,或者重新设置请求的报头信息,然后再将请求发送给目标资源。当目标资源对请求作出响应时候,容器同样会将响应先转发给过滤器,在过滤器中你可以对响应的内容进行转换,然后再将响应发送到客户端。
常见的过滤器用途主要包括:对用户请求进行统一认证、对用户的访问请求进行记录和审核、对用户发送的数据进行过滤或替换、转换图象格式、对响应内容进行压缩以减少传输量、对请求或响应进行加解密处理、触发资源访问事件、对XML的输出应用XSLT等。
和过滤器相关的接口主要有:Filter、FilterConfig和FilterChain。
监听器有哪些作用和用法?
Java Web开发中的监听器(listener)就是application、session、request三个对象创建、销毁或者往其中添加修改删除属性时自动执行代码的功能组件,如下所示:
- ServletContextListener:对Servlet上下文的创建和销毁进行监听。
- ServletContextAttributeListener:监听Servlet上下文属性的添加、删除和替换。
- HttpSessionListener:对Session的创建和销毁进行监听。
补充:session的销毁有两种情况:1). session超时(可以在web.xml中通过/ 标签配置超时时间);2). 通过调用session对象的invalidate()方法使session失效。 - HttpSessionAttributeListener:对Session对象中属性的添加、删除和替换进行监听。
- ServletRequestListener:对请求对象的初始化和销毁进行监听。
- ServletRequestAttributeListener:对请求对象属性的添加、删除和替换进行监听。
web.xml文件中可以配置哪些内容?
web.xml用于配置Web应用的相关信息,如:监听器(listener)、过滤器(filter)、 Servlet、相关参数、会话超时时间、安全验证方式、错误页面等,下面是一些开发中常见的配置:
配置Spring上下文加载监听器加载Spring配置文件并创建IoC容器:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
配置Spring的OpenSessionInView过滤器来解决延迟加载和Hibernate会话关闭的矛盾:
<filter>
<filter-name>openSessionInView</filter-name>
<filter-class>
org.springframework.orm.hibernate3.support.OpenSessionInViewFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>openSessionInView</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
配置会话超时时间为10分钟:
<session-config>
<session-timeout>10</session-timeout>
</session-config>
配置404和Exception的错误页面:
<error-page>
<error-code>404</error-code>
<location>/error.jsp</location>
</error-page>
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/error.jsp</location>
</error-page>
配置安全认证方式:
<security-constraint>
<web-resource-collection>
<web-resource-name>ProtectedArea</web-resource-name>
<url-pattern>/admin/*</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>BASIC</auth-method>
</login-config>
<security-role>
<role-name>admin</role-name>
</security-role>
说明:对Servlet(小服务)、Listener(监听器)和Filter(过滤器)等Web组件的配置,Servlet 3规范提供了基于注解的配置方式,可以分别使用@WebServlet、@WebListener、@WebFilter注解进行配置。
Servlet 3中的异步处理指的是什么?
在Servlet 3中引入了一项新的技术可以让Servlet异步处理请求。有人可能会质疑,既然都有多线程了,还需要异步处理请求吗?答案是肯定的,因为如果一个任务处理时间相当长,那么Servlet或Filter会一直占用着请求处理线程直到任务结束,随着并发用户的增加,容器将会遭遇线程超出的风险,这这种情况下很多的请求将会被堆积起来而后续的请求可能会遭遇拒绝服务,直到有资源可以处理请求为止。异步特性可以帮助应用节省容器中的线程,特别适合执行时间长而且用户需要得到结果的任务,如果用户不需要得到结果则直接将一个Runnable对象交给Executor并立即返回即可。
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
final AsyncContext ctx = req.startAsync();
ctx.start(new Runnable() {
@Override
public void run() {
ctx.complete();
}
});
}
Tomcat服务器优化了解吗?
- 内存优化:主要是对Tomcat启动参数进行优化,我们可以在Tomcat启动脚本中修改它的最大内存数等等。
- 线程数优化:Tomcat的并发连接参数,主要在Tomcat配置文件中server.xml中配置,比如修改最小空闲连接线程数,用于提高系统处理性能等等。
- 优化缓存:打开压缩功能,修改参数,比如压缩的输出内容大小默认为2KB,可以适当的修改。
Spring
Spring中自动装配的方式有哪些?
- no:不进行自动装配,手动设置Bean的依赖关系。
- byName:根据Bean的名字进行自动装配。
- byType:根据Bean的类型进行自动装配。
- constructor:类似于byType,不过是应用于构造器的参数,如果正好有一个Bean与构造器的参数类型相同则可以自动装配,否则会导致错误。
- autodetect:如果有默认的构造器,则通过constructor的方式进行自动装配,否则使用byType的方式进行自动装配。
说明:自动装配没有自定义装配方式那么精确,而且不能自动装配简单属性(基本类型、字符串等),在使用时应注意。
你如何理解AOP中的连接点(Joinpoint)、切点(Pointcut)、增强(Advice)、引介(Introduction)、织入(Weaving)、切面(Aspect)这些概念?
- 连接点(Joinpoint):程序执行的某个特定位置(如:某个方法调用前、调用后,方法抛出异常后)。一个类或一段程序代码拥有一些具有边界性质的特定点,这些代码中的特定点就是连接点。Spring仅支持方法的连接点。
- 切点(Pointcut):如果连接点相当于数据中的记录,那么切点相当于查询条件,一个切点可以匹配多个连接点。Spring AOP的规则解析引擎负责解析切点所设定的查询条件,找到对应的连接点。
- 增强(Advice):增强是织入到目标类连接点上的一段程序代码。Spring提供的增强接口都是带方位名的,如:BeforeAdvice、AfterReturningAdvice、ThrowsAdvice等。很多资料上将增强译为“通知”,这明显是个词不达意的翻译,让很多程序员困惑了许久。
- 引介(Introduction):引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过引介功能,可以动态的未该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。
- 织入(Weaving):织入是将增强添加到目标类具体连接点上的过程,AOP有三种织入方式:①编译期织入:需要特殊的Java编译期(例如AspectJ的ajc);②装载期织入:要求使用特殊的类加载器,在装载类的时候对类进行增强;③运行时织入:在运行时为目标类生成代理实现增强。Spring采用了动态代理的方式实现了运行时织入,而AspectJ采用了编译期织入和装载期织入的方式。
- 切面(Aspect):切面是由切点和增强(引介)组成的,它包括了对横切关注功能的定义,也包括了对连接点的定义。
Spring中如何使用注解来配置Bean?有哪些相关的注解?
首先需要在Spring配置文件中增加如下配置:
<context:component-scan base-package="org.example"/>
然后可以用@Component、@Controller、@Service、@Repository注解来标注需要由Spring IoC容器进行对象托管的类。这几个注解没有本质区别,只不过@Controller通常用于控制器,@Service通常用于业务逻辑类,@Repository通常用于仓储类(例如我们的DAO实现类),普通的类用@Component来标注。
Spring支持的事务管理类型有哪些?你在项目中使用哪种方式?
Spring支持编程式事务管理和声明式事务管理。许多Spring框架的用户选择声明式事务管理,因为这种方式和应用程序的关联较少,因此更加符合轻量级容器的概念。声明式事务管理要优于编程式事务管理,尽管在灵活性方面它弱于编程式事务管理,因为编程式事务允许你通过代码控制业务。 事务分为全局事务和局部事务。全局事务由应用服务器管理,需要底层服务器JTA支持(如WebLogic、WildFly等)。局部事务和底层采用的持久化方案有关,例如使用JDBC进行持久化时,需要使用Connetion对象来操作事务;而采用Hibernate进行持久化时,需要使用Session对象来操作事务。 Spring提供了如下所示的事务管理器。
- DataSourceTransactionManager(注入DataSource)【常用】
- HibernateTransactionManager(注入SessionFactory)
- JdoTransactionManager(管理JDO事务)
- JtaTransactionManager(使用JTA管理事务)
- PersistenceBrokerTransactionManager(管理Apache的OJB事务)
Spring MVC的工作原理是怎样的?
Spring MVC的工作原理如下图所示:

① 客户端的所有请求都交给前端控制器DispatcherServlet来处理,它会负责调用系统的其他模块来真正处理用户的请求。
② DispatcherServlet收到请求后,将根据请求的信息(包括URL、HTTP协议方法、请求头、请求参数、Cookie等)以及HandlerMapping的配置找到处理该请求的Handler(任何一个对象都可以作为请求的Handler)。
③在这个地方Spring会通过HandlerAdapter对该处理器进行封装。
④ HandlerAdapter是一个适配器,它用统一的接口对各种Handler中的方法进行调用。
⑤ Handler完成对用户请求的处理后,会返回一个ModelAndView对象给DispatcherServlet,ModelAndView顾名思义,包含了数据模型以及相应的视图的信息。
⑥ ModelAndView的视图是逻辑视图,DispatcherServlet还要借助ViewResolver完成从逻辑视图到真实视图对象的解析工作。
⑦ 当得到真正的视图对象后,DispatcherServlet会利用视图对象对模型数据进行渲染。
⑧ 客户端得到响应,可能是一个普通的HTML页面,也可以是XML或JSON字符串,还可以是一张图片或者一个PDF文件。
阐述Spring框架中Bean的生命周期?
① Spring IoC容器找到关于Bean的定义并实例化该Bean。
② Spring IoC容器对Bean进行依赖注入。
③ 如果Bean实现了BeanNameAware接口,则将该Bean的id传给setBeanName方法。
④ 如果Bean实现了BeanFactoryAware接口,则将BeanFactory对象传给setBeanFactory方法。
⑤ 如果Bean实现了BeanPostProcessor接口,则调用其postProcessBeforeInitialization方法。
⑥ 如果Bean实现了InitializingBean接口,则调用其afterPropertySet方法。
⑦ 如果有和Bean关联的BeanPostProcessors对象,则这些对象的postProcessAfterInitialization方法被调用。
⑧ 当销毁Bean实例时,如果Bean实现了DisposableBean接口,则调用其destroy方法。
Spring MVC与Struts的区别?你会选哪个?
- Spring MVC核心控制器是Servlet,而Struts2是Filter。
- Spring Mvc会比Struts快一些(理论上)。Spring Mvc是基于方法设计,而Sturts是基于对象,每次发一次请求都会实例一个action,每个action都会被注入属性,而Spring更像Servlet一样,只有一个实例,每次请求执行对应的方法即可(注意:由于是单例实例,所以应当避免全局变量的修改,这样会产生线程安全问题)。
- Spring框架对Spring MVC支持更好,而且提供了全注解方式进行管理,各种功能的注解都比较全面,使用简单,而Struts2需要采用XML很多的配置参数来管理。
- Struts2中自身提供多种参数接受,其实都是通过(ValueStack)进行传递和赋值,而Spring MVC是通过方法的参数进行接收。
- Spring MVC相对于Struts2使用更为简单,学习成本更低。
- Struts有自己的interceptor机制,Spring MVC用的是独立的AOP方式。Spring MVC是方法级别的拦截,一个方法对应一个request上下文,而方法同时又跟一个url对应,所以说从架构本身上Spring MVC就容易实现restful url。Struts2是类级别的拦截,一个类对应一个request上下文;实现restful url要费劲,因为Struts2 action的一个方法可以对应一个url;而其类属性却被所有方法共享,这也就无法用注解或其他方式标识其所属方法了。Spring MVC的方法之间基本上独立的,独享request response数据,请求数据通过参数获取,处理结果通过ModelMap交回给框架方法之间不共享变量,而Struts2搞的就比较乱,虽然方法之间 也是独立的,但其所有Action变量是共享的,这不会影响程序运行,却给我们编码,读程序时带来麻烦。
- Spring MVC处理ajax请求,直接通过返回数据,方法中使用注解@ResponseBody,Spring MVC自动帮我们对象转换为JSON数据。
Dubbo
你了解dubbo的原理么?
dubbo默认使用的是什么通信框架,还有别的选择吗?
默认推荐使用的是netty通信框架,当然还可以选择mina。
dubbo服务调用是阻塞的吗?
默认是阻塞的,可以异步调用,没有返回值的可以这么做。
<dubbo:reference id="demoServicemy2" interface="com.test.dubboser.ServiceDemo2">
<dubbo:method name="getPerson" async="true" />
</dubbo:reference>
dubbo一般使用什么注册中心?还有别的选择吗?
Zookeeper 是 Dubbo 官方推荐的注册中心。还有Multicast、Redis等方式,但不推荐。
Zookeeper注册中心支持以下功能:
- 当提供者出现断电等异常停机时,注册中心能自动删除提供者信息。
- 当注册中心重启时,能自动恢复注册数据,以及订阅请求。
dubbo默认使用什么序列化框架,你知道的还有哪些?
默认使用Hessian序列化,还有Duddo、FastJson、Java自带序列化。
dubbo服务提供者能实现失效踢出是什么原理?
服务失效踢出基于zookeeper的临时节点原理(临时节点驻存在ZooKeeper中,当连接和session断掉时会自动删除)。
dubbo服务上线怎么不影响旧版本?
采用多版本开发,不影响旧版本。当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引用。
<dubbo:service interface="com.foo.BarService" version="1.0.0" />
如何解决dubbo服务调用链过长的问题?
可以通过Filter自定义调用参数结合zipkin实现分布式服务追踪。
说说dubbo核心的配置有哪些?
核心配置有:
dubbo:service/
dubbo:reference/
dubbo:protocol/
dubbo:registry/
dubbo:application/
dubbo:provider/
dubbo:consumer/
dubbo:method/
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 应用名,用于计算依赖关系-->
<dubbo:application name="JKDService-User" />
<dubbo:protocol name="dubbo" port="-1" />
<dubbo:registry address="${service.registry.address}" />
<!-- provider -->
<dubbo:service interface="com.jiuxian.service.user.UserService" ref="userServiceImpl" group="${service.group}" timeout="${service.timeout}" />
<!-- consumer -->
<dubbo:reference id="demoServicemy2" interface="com.test.dubboser.ServiceDemo2">
<dubbo:method name="getPerson" async="true" return="false" />
</dubbo:reference>
</beans>
dubbo推荐用什么协议?
默认使用dubbo协议。dubbo协议是基于TCP单一长连接模式,序列化使用的Hessian二进制序列化。因为服务的现状大都是服务提供者少,服务消费者多,所以单一长连接模式更适合于这种常规远程服务方法调用请求频繁、传输数据量小的情况。
另外还有rmi协议、http协议的传输模式
同一个服务多个注册的情况下可以直连某一个服务吗?
可以直连,修改配置即可,也可以通过telnet直接调用某个服务。
Dubbo集群容错怎么做?
Dubbo集群容错模式有:
- Failover(失败后重试其他服务,可通过retries=”2”来设置重试次数(不含第一次))
- Failfast(快速失败,只发起一次调用,失败立即报错)
- Failsafe(失败安全,出现异常时,直接忽略)
- Failback(失败自动恢复,后台记录失败请求,定时重发)
- Forking(并行调用多个服务器,只要一个成功即返回,可通过forks=”2”来设置最大并行数)
- Broadcast(广播调用所有提供者,逐个调用,任意一台报错则报错)
<dubbo:service cluster="failsafe"/>
dubbo和dubbox之间的区别?
dubbox是当当网基于dubbo上做了一些扩展,如加了服务可restful调用,更新了开源组件等。
你还了解别的分布式服务框架吗?
别的还有spring的spring cloud,facebook的thrift,twitter的finagle等。
在使用过程中都遇到了些什么问题?
- 序列化问题,dubbo目前只支持对java内置数据类型、集合类型的序列化与反序列化,像guava中的集合转换操作会将集合类型改变导致传输时无法序列化与反序列化。
- 版本控制问题,服务版本号管理不当导致业务异常。
MySQL
聊聊MySQL的主从复制机制?
Mysql的 Replication 是一个异步的复制过程(mysql5.1.7以上版本分为异步复制和半同步两种模式),从一个 Mysql instace(我们称之为 Master)复制到另一个 Mysql instance(我们称之 Slave)。在 Master 与 Slave 之间的实现整个复制过程主要由三个线程来完成,其中两个线程(Sql线程和IO线程)在 Slave 端,另外一个线程(IO线程)在 Master 端。
1.Slave 上面的IO线程连接上 Master,并请求从指定日志文件的指定位置(或者从最开始的日志)之后的日志内容;
2.Master 接收到来自 Slave 的 IO 线程的请求后,通过负责复制的 IO 线程根据请求信息读取指定日志指定位置之后的日志信息,返回给 Slave 端的 IO 线程。返回信息中除了日志所包含的信息之外,还包括本次返回的信息在 Master 端的 Binary Log 文件的名称以及在 Binary Log 中的位置;
3.Slave 的 IO 线程接收到信息后,将接收到的日志内容依次写入到 Slave 端的Relay Log文件(mysql-relay-bin.xxxxxx)的最末端,并将读取到的Master端的bin-log的文件名和位置记录到master- info文件中,以便在下一次读取的时候能够清楚的高速Master“我需要从某个bin-log的哪个位置开始往后的日志内容,请发给我”
4.Slave 的 SQL 线程检测到 Relay Log 中新增加了内容后,会马上解析该 Log 文件中的内容成为在 Master 端真实执行时候的那些可执行的 Query 语句,并在自身执行这些 Query。这样,实际上就是在 Master 端和 Slave 端执行了同样的 Query,所以两端的数据是完全一样的。
复制的几种模式:
- 基于SQL语句的复制(statement-based replication, SBR)
- 基于行的复制(row-based replication, RBR)
- 混合模式复制(mixed-based replication, MBR)(常用)
MySQL如何实现高可用(两主多从架构)?

- 1)Master(192.168.31.230)为正常运行环境下的主库,为两个Slave(192.168.31.231和192.168.31.232)提供“主-从”复制功能;
- 2)Master_Backup(192.168.31.233)是Master的备份库,只要Master是正常的,它不对外提供服务。它与Master之间属于”主-主”复制关系,即自己既是主机,又是对方的从机;
- 3)同理,192.168.31.234和192.168.31.235为Slave_Backup,分别为192.168.31.231和 192.168.31.232的备份库,只要Slave是正常的,对应的备份机不对外提供服务;
- 4)Slave在此架构中的目的是为了实现读写分离,对应用程序来说,Master只负责写,两个Slave只负责读。Slave的数据来源于Master的复制操作;
- 5)如果Master由于某种原因(例如:宕机和断电等)导致不能正常运行,则此时需要让Master_Backup自动切换为新主机,而Slave和Slave_Backup也能自动切换数据源到Master_Backup;
- 6)同理,如果Slave由于某种原因(例如:宕机和断电等)导致不能正常运行,则此时需要让对应的Slave_Backup自动切换为新从机;
- 7)无论是Master还是切换后的Master_Backup,它们向客户端提供的连接地址应保持一致,如上图提供的VIP+Port,即192.168.31.201:3306,Slave和Slave_Backup也应如此,对外提供的连接地址始终是192.168.31.202:3306和192.168.31.203:3306。
Hibernate
- 项目中还是多使用mybatis,因为Hibernate配置相对复杂、对于复杂的sql优化困难。
- Hibernate的主要实现是通过SessionFactory创建Session,然后通过Session操作缓存,最后完成数据库表与对象的状态数据同步。
- Hibernate的缓存技术分为一级缓存(Session缓存,Session关闭后就消失),二级缓存(一般通过ehcache实现的sessionFactory应用级别只读缓存)
- Hibernate数据对象存在三种状态:临时(刚new未交给Session处理)、持久化(交给Session处理)、游离(Session清空或关闭)
- Session.flush()清理缓存是指按照缓存中对象的状态的变化来同步更新数据库,但不清空缓存;Session.clear()清空是把Session 的缓存置空, 但不同步更新数据库。
- load()支持延迟加载,get()不支持延迟加载,如果数据库中,没有 OID 指定的对象。通过 get方法加载,则返回的是一个null;通过load加载,则返回一个代理对象,如果后面代码如果调用对象的某个属性会抛出异常:org.hibernate.ObjectNotFoundException。
- openSession()是直接 new 一个新的 Session 并返回,并且需要手动关闭;getCurrentSession()通过ThreadLocal使Session在线程间隔离且在同一线程内共享,基于Spring管理时通过此机制实现事务及会话管理,无需手动创建、关闭Session。
JVM
请描述一下java的内存模型?
Java虚拟机规范中将Java运行时数据分为六种。
- 程序计数器:是一个数据结构,用于保存当前正常执行的程序的内存地址。Java虚拟机的多线程就是通过线程轮流切换并分配处理器时间来实现的,为了线程切换后能恢复到正确的位置,每条线程都需要一个独立的程序计数器,互不影响,该区域为“线程私有”。
- Java虚拟机栈:线程私有的,与线程生命周期相同,用于存储局部变量表,操作栈,方法返回值。局部变量表放着基本数据类型,还有对象的引用。
- 本地方法栈:跟虚拟机栈很像,不过它是为虚拟机使用到的Native方法服务。
- Java堆:所有线程共享的一块内存区域,对象实例几乎都在这分配内存。
- 方法区:各个线程共享的区域,储存虚拟机加载的类信息,常量,静态变量,编译后的代码。
- 运行时常量池:代表运行时每个class文件中的常量表。包括几种常量:编译时的数字常量、方法或者域的引用。
描述一下JVM加载class文件的原理机制?
JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。
由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。
类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。
加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。
最后JVM对类进行初始化,包括:
1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;
2)如果类中存在初始化语句(static{}),就依次执行这些初始化语句。
类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。
从Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。
下面是关于几个类加载器的说明:
- Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
- Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
- System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。
解释内存中的栈(stack)、堆(heap)和方法区(method area)的用法。
通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用JVM中的栈空间;而通过new关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为Eden、Survivor(又可分为From Survivor和To Survivor)、Tenured;方法区和堆都是各个线程共享的内存区域,用于存储已经被JVM加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据;程序中的字面量(literal)如直接书写的100、”hello”和常量都是放在常量池中,常量池是方法区的一部分,。栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过JVM的启动参数来进行调整,栈空间用光了会引发StackOverflowError,而堆和常量池空间不足则会引发OutOfMemoryError。
GC是什么?GC的原理是什么?
GC是垃圾收集的意思(Garbage Collection),JVM通过垃圾回收机制自动回收内存来保证可用内存空间。
JVM的垃圾回收器在JDK1.7以前常用的CMS收集器,特点是短时间停顿,JDK1.7开始使用G1收集器,与其他收集器相比,它具有如下优点:并行与并发、分代收集、空间整合、可预测的停顿等。
GC的工作主要包含三方面内容:
- 确定可回收内存
- 确定可回收内存的内容就是确定不可用(已死)对象并进行标记,确认对象不可用(已死)的算法有:引用计数算法和可达性分析算法,其中引用计数算法存在无法处理对象之间循环引用的问题,所以采用的是可达性分析算法 可达性分析算法的内容大概是:通过GC Roots对象作为搜索起点沿着引用链对所有对象进行可达性搜索,当一个对象到GC Roots对象没有任何引用链相连时,则会标记此对象不可用(已死)。
- 确定何时回收内存
- 对象在Eden区中分配且没有足够空间时,会触发一次Minor GC;
- 老年代(Tenured区)没有足够空间时,会触发一次Full GC;
- 方法区(Permanent区)没有足够空间时,会触发一次Full GC;
- 调用System.gc() 或者 Runtime.gc()时(系统建议执行Full GC,但是不必然执行),会触发一次Full GC;
- 执行回收内存
- 回收时采用的是分代回收机制,对于新生代(Eden区+Survivor区)采用“复制-清理”算法,需要分配担保,对于老年代(Tenured区)采用“标记-整理”或“标记-清理”算法,无需分配担保。
详细的GC工作流程如下:
- 对象在Eden区中分配且没有足够空间时,会触发一次Minor GC,在触发Minor GC前,会首先检查Survivor区的“From”区中是否存在达到晋升老年代(Tenured区)的年龄的可用(存活)对象。
- 如果不存在则执行Minor GC,首先标记Eden、Survivor区中对象是否可用(存活),然后将Eden区的可用对象复制到Survivor区的“To”区。
- 接下来将Survivor区的“From”区中未达到晋升老年代(Tenured区)的年龄的可用(存活)对象复制到Survivor区的“To”区。
- 以上工作完成后,将Eden区和Survivor区的“From”区清空,最后将Survivor区的“From”区和“To”区位置置换(“To”区变“From”区、“From”区变“To”区),保证Survivor区的“To”区始终为空。
- 还有如果Survivor区的“To”区被填满,会将其中所有可用(存活)对象转移到Tenured区,至此Minor GC结束。
- 在触发Minor GC前,Survivor区的“From”区中如果存在达到晋升老年代(Tenured区)的年龄的可用(存活)对象,则会触发一次Full GC,不再执行Minor GC。
- 另外除了以上Full GC的触发条件,然后一些其他条件会触发Full GC,例如:
- 老年代(Tenured区)没有足够空间时
- 方法区(Permanent区)没有足够空间时
- 调用System.gc() 或者 Runtime.gc()时(系统建议执行Full GC,但是不必然执行)
- Full GC中会对Tenured区、Permanent区中的对象是否可用进行标记,然后清理不可用对象,整理可用对象。
要注意的是,GC对于大对象的处理效率低下(Minor GC时会发生大量内存复制),所以在处理大对象的分配时,为了提升效率,会直接将大对象分配到Tenured区,避免GC。
分代回收策略中的Minor GC是对新生代进行回收,采用的是“复制-清理”算法,而Major GC是对老年代进行回收,采用的则是“标记-整理”或“标记-清理”算法,Major GC和Minor GC属于Full GC的一部分,而Full GC则是对整个堆进行收集,包括新生代、老年代、永久代。
”所有的 Minor GC 都会触发”全世界的暂停(stop-the-world)”,停止应用程序的线程。”,对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。其中的真相就是,Eden区中的大部分对象都能被认为是垃圾,永远也不会被复制到Survivor区或者Tenured区,所以一般Minor GC执行暂停的时间很短。如果正好相反,Eden区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。
JVM配置参数有哪些,各有什么作用?
- 堆设置
- -Xms:初始堆大小
- -Xmx:最大堆大小
- -XX:NewSize=n:设置年轻代大小
- -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
- -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
- -XX:MaxPermSize=n:设置持久代大小
- 收集器设置
- -XX:+UseSerialGC:设置串行收集器
- -XX:+UseParallelGC:设置并行收集器
- -XX:+UseParalledlOldGC:设置并行年老代收集器
- -XX:+UseConcMarkSweepGC:设置并发收集器
- 垃圾回收统计信息
- -XX:+PrintGC
- -XX:+PrintGCDetails
- -XX:+PrintGCTimeStamps
- -Xloggc:filename
- 并行收集器设置
- -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
- -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
- -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
- 并发收集器设置
- -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
- -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
你能保证 GC 执行吗?
不能,虽然你可以调用 System.gc() 或者 Runtime.gc(),但是没有办法保证 GC 的执行。
开放性问题
比较一下Java和JavaSciprt
- 面向对象和基于对象:Java是一种面向对象语言,必须设计对象,而后进行编程;JavaScript是一种脚本语言,基于对象(Object-Based)和事件驱动(Event-Driven)进行编程。
- 编译和解释:Java的源代码在执行之前,必须经过编译,而后再进行解释执行。JavaScript是一种解释性编程语言,其源代码不需经过编译,由浏览器直接解释执行。
- 强类型和弱类型:Java采用强类型变量检查,即所有变量在编译之前必须作声明;JavaScript中变量是弱类型的,甚至在使用变量前可以不作声明,JavaScript的解释器在运行时检查推断其数据类型。
你用过的网站前端优化的技术有哪些?
① 浏览器访问优化:
- 减少HTTP请求数量:合并CSS、合并JavaScript、合并图片(CSS Sprite)
- 使用浏览器缓存:通过设置HTTP响应头中的Cache-Control和Expires属性,将CSS、JavaScript、图片等在浏览器中缓存,当这些静态资源需要更新时,可以更新HTML文件中的引用来让浏览器重新请求新的资源
- 启用压缩
- CSS前置,JavaScript后置
- 减少Cookie传输(精简Cookie,域名独立Cookie独立)
② CDN加速:CDN(Content Distribute Network)的本质仍然是缓存,将数据缓存在离用户最近的地方,CDN通常部署在网络运营商的机房,不仅可以提升响应速度,还可以减少应用服务器的压力。当然,CDN缓存的通常都是静态资源。
③ 反向代理:反向代理相当于应用服务器的一个门面,可以保护网站的安全性,也可以实现负载均衡的功能,当然最重要的是它缓存了用户访问的热点资源,可以直接从反向代理将某些内容返回给用户浏览器。
你使用过的应用服务器优化技术有哪些?
① 分布式缓存:
缓存的本质就是内存中的哈希表,如果设计一个优质的哈希函数,那么理论上哈希表读写的渐近时间复杂度为O(1)。缓存主要用来存放那些读写比很高、变化很少的数据,这样应用程序读取数据时先到缓存中读取,如果没有或者数据已经失效再去访问数据库或文件系统,并根据拟定的规则将数据写入缓存。对网站数据的访问也符合二八定律(Pareto分布,幂律分布),即80%的访问都集中在20%的数据上,如果能够将这20%的数据缓存起来,那么系统的性能将得到显著的改善。当然,使用缓存需要解决以下几个问题:
- 频繁修改的数据;
- 数据不一致与脏读;
- 缓存雪崩(可以采用分布式缓存服务器集群加以解决,memcached是广泛采用的解决方案);
- 缓存预热;
- 缓存穿透(恶意持续请求不存在的数据)。
② 异步操作:可以使用消息队列将调用异步化,通过异步处理将短时间高并发产生的事件消息存储在消息队列中,从而起到削峰作用。电商网站在进行促销活动时,可以将用户的订单请求存入消息队列,这样可以抵御大量的并发订单请求对系统和数据库的冲击。目前,绝大多数的电商网站即便不进行促销活动,订单系统都采用了消息队列来处理。
③ 使用集群。
④ 代码优化:
- 多线程:基于Java的Web开发基本上都通过多线程的方式响应用户的并发请求,使用多线程技术在编程上要解决线程安全问题,主要可以考虑以下几个方面:A. 将对象设计为无状态对象(这和面向对象的编程观点是矛盾的,在面向对象的世界中被视为不良设计),这样就不会存在并发访问时对象状态不一致的问题。B. 在方法内部创建对象,这样对象由进入方法的线程创建,不会出现多个线程访问同一对象的问题。使用ThreadLocal将对象与线程绑定也是很好的做法,这一点在前面已经探讨过了。C. 对资源进行并发访问时应当使用合理的锁机制。
- 非阻塞I/O: 使用单线程和非阻塞I/O是目前公认的比多线程的方式更能充分发挥服务器性能的应用模式,基于Node.js构建的服务器就采用了这样的方式。Java在JDK 1.4中就引入了NIO(Non-blocking I/O),在Servlet 3规范中又引入了异步Servlet的概念,这些都为在服务器端采用非阻塞I/O提供了必要的基础。
- 资源复用:资源复用主要有两种方式,一是单例,二是对象池,我们使用的数据库连接池、线程池都是对象池化技术,这是典型的用空间换取时间的策略,另一方面也实现对资源的复用,从而避免了不必要的创建和释放资源所带来的开销。
什么是XSS攻击?什么是SQL注入攻击?什么是CSRF攻击?
- XSS(Cross Site Script,跨站脚本攻击)是向网页中注入恶意脚本在用户浏览网页时在用户浏览器中执行恶意脚本的攻击方式。跨站脚本攻击分有两种形式:反射型攻击(诱使用户点击一个嵌入恶意脚本的链接以达到攻击的目标,目前有很多攻击者利用论坛、微博发布含有恶意脚本的URL就属于这种方式)和持久型攻击(将恶意脚本提交到被攻击网站的数据库中,用户浏览网页时,恶意脚本从数据库中被加载到页面执行,QQ邮箱的早期版本就曾经被利用作为持久型跨站脚本攻击的平台)。XSS虽然不是什么新鲜玩意,但是攻击的手法却不断翻新,防范XSS主要有两方面:消毒(对危险字符进行转义)和HttpOnly(防范XSS攻击者窃取Cookie数据)。
- SQL注入攻击是注入攻击最常见的形式(此外还有OS注入攻击(Struts 2的高危漏洞就是通过OGNL实施OS注入攻击导致的)),当服务器使用请求参数构造SQL语句时,恶意的SQL被嵌入到SQL中交给数据库执行。SQL注入攻击需要攻击者对数据库结构有所了解才能进行,攻击者想要获得表结构有多种方式:(1)如果使用开源系统搭建网站,数据库结构也是公开的(目前有很多现成的系统可以直接搭建论坛,电商网站,虽然方便快捷但是风险是必须要认真评估的);(2)错误回显(如果将服务器的错误信息直接显示在页面上,攻击者可以通过非法参数引发页面错误从而通过错误信息了解数据库结构,Web应用应当设置友好的错误页,一方面符合最小惊讶原则,一方面屏蔽掉可能给系统带来危险的错误回显信息);(3)盲注。防范SQL注入攻击也可以采用消毒的方式,通过正则表达式对请求参数进行验证,此外,参数绑定也是很好的手段,这样恶意的SQL会被当做SQL的参数而不是命令被执行,JDBC中的PreparedStatement就是支持参数绑定的语句对象,从性能和安全性上都明显优于Statement。
- CSRF攻击(Cross Site Request Forgery,跨站请求伪造)是攻击者通过跨站请求,以合法的用户身份进行非法操作(如转账或发帖等)。CSRF的原理是利用浏览器的Cookie或服务器的Session,盗取用户身份,其原理如下图所示。防范CSRF的主要手段是识别请求者的身份,主要有以下几种方式:(1)在表单中添加令牌(token);(2)验证码;(3)检查请求头中的Referer(前面提到防图片盗链接也是用的这种方式)。令牌和验证都具有一次消费性的特征,因此在原理上一致的,但是验证码是一种糟糕的用户体验,不是必要的情况下不要轻易使用验证码,目前很多网站的做法是如果在短时间内多次提交一个表单未获得成功后才要求提供验证码,这样会获得较好的用户体验。
聊聊你所处理过的技术难点?
秒杀模块开发难点:瞬时流量高(6万活跃用户,TPS峰值3000左右)、库存有限。
设计方案大致如下:
- 秒杀时间到,客户端放行秒杀请求。
- 服务端对用户请求进行有效性(是否登录、是否是恶意用户、是否实名认证、是否已秒杀)检查拦截,请求无效直接返回。
- 接下来进行活动有效性检查拦截,活动无效直接放回。
- 然后进行活动库存扣减动作,扣减失败直接返回。
- 扣减库存成功,记录用户秒杀成功标识到缓存Redis,然后调用购物车服务将商品加入购物车
- 用户到购物车自行下单,后续流程处理与正常下单一致
- 超时未下单自动取消并返还活动库存 该方案主要从缓存、拦截、隔离三个角度着手解决:
- 缓存
- 静态资源(活动页内容)缓存到运营商CDN,服务器只处理秒杀请求
- 秒杀请求处理中使用的活动信息(活动商品、活动时间、活动库存等)全部缓存到Redis,尽量避免服务调用
- 扣减活动库存直接在Redis缓存中处理,保证原子操作,提高吞吐量
- 拦截
- 客户端通过验证码等技术降低用户请求频度
- 服务端通过登录验证、用户风险等级检查、实名认证等方式拦截无效请求
- 隔离
- 秒杀服务、缓存Redis独立部署,避免影响主线业务运行
- 独立域名,精简Cookie传输大小
聊聊你们的系统是如何管理库存的?如何应对并发以及是否会出现超卖?
供应链系统与销售端系统的库存分开维护。其中供应链系统在下单、调拨、退厂出库,采购、调拨入库操作时会维护供应链库存。
销售端系统前端在下单时会对库存进行维护,同时销售端系统后台上下架商品操作时会通过发消息通知销售端系统前端对库存进行维护。
供应链、销售端系统的库存在各自的Redis服务器中维护,基于Redis有效处理库存并发访问问题。
由于供应链系统与销售端系统同步库存是基于库存变动消息进行同步的,虽然一般时间很短,但还是会存在库存不一致的情况。
对于销售端和供应链系统库存不一致的情况,用户下单在销售端系统库存检查通过后,订单状态为待确认状态,通过OMS系统流转到供应链系统时会再次进行供应链系统库存是否充足等各种检查操作,如果此时库存不足,订单会变为无效状态,同时备注库存不足。
聊聊你们的系统架构?

分布式系统架构概要:
- 高可用
- 集群
- 负载均衡
- 高并发
- 服务化
- 无状态
- 消息队列
- 缓存
- 连接池
你对目前的业务优化有什么建议?
首先梳理一遍业务流程;接下来点出不足之处;最后就不足之处提出有效优化、建议。例如:
- 入库预约流程
- 入库流程:预约》验收》入库。目前预约的方式是线下通过电话预约,管理员人工核实仓库容量情况后确定入库时间。
- 优化建议:通过系统统计仓库容量情况,然后通过系统预约,这样可以简化预约流程、减轻人员工作量。
- 出库流程
- 出库流程:订单拣货》打包》核单》预约物流》出库。目前预约物流是通过线下通过电话预约承运商确定出库时间,出库后通过货运单人工电话追踪物流信息。
- 优化建议:通过系统对接第三方承运商服务,系统预约承运商,同时实现物流状态自动追踪。
面试技巧
注意点:言简意赅、沉着冷静、微笑、有礼貌。
简历编写有啥要注意的?
- 尽可能简单,别超过1页
- 只写主要的:自我介绍(姓名、联系方式、学历、在职状态、期望职位)、项目经验、掌握技能。
做个自我介绍吧?
- 打招呼,报姓名。
- 工作经验介绍
- 目前从事行业
示例:您好,我叫小明,5年JAVA开发经验,目前从事于互联网电商行业。自我介绍不超过4句话。
回答问题注意什么?
- 只用一句话10分,两句话5分,超过三句都是废话。
- 不管面试官笑不笑,你尽量保持微笑。
- 技术面试,是对你基础的考察,仅此而已。
- 如果你的EQ不是很高,那就别随便开玩笑,否则可能会弄巧成拙。
你为什么从公司离职?
离职的原因主要有两方面:当然可以有一方面的原因,也可以都有。
- 1.薪资待遇
- 公司由于发展受限而压缩成本,薪资短期内无法提升。
- 2.发展空间
- 平台业务拓展较慢造成个人能力成长空间狭窄,所以需要更宽广的平台。
你还有什么想问的吗?
你最好问一点问题,否则对方可能认为你对这份工作没什么兴趣。
- 请问您部门里会为员工进行哪些培训?
- 请问您这个职位未来的发展空间或晋升途径有哪些?
- 请问您咱们公司的福利都有什么?比如健身设施、打车报销、加班费、弹性工作时间等。
参考文献
Java 面试题:百度前200页都在这里了
Java 面试题全集上
Java 面试题全集中
Java 面试题全集下
双重检查锁定与延迟初始化
如何分析Thread Dump(收集)
Java线程经典面试题
mysql数据库主从配置详解以及主从实现原理分析
MySQL双主一致性架构优化