Java面试总结
一、基础篇
1.1、Java基础
-
面向对象的特征:继承、封装和多态
final, finally, finalize 的区别 -
final用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。
内部类要访问局部变量,局部变量必须定义成final类型,例如,一段代码……
finally是异常处理语句结构的一部分,表示总是执行。
finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。JVM不保证此方法总被调用
-
Exception、Error、运行时异常与一般异常有何异同
-
exception 表示一种设计或实现问题。也就是说,它表示如果程序运行正常,从不会发生的情况
-
error表示恢复不是不可能但很困难的情况下的一种严重问题。比如说内存溢出。不可能指望程序能处理这样的情况。
异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误。java编译器要求方法必须声明抛出可能发生的非运行时异常,但是并不要求必须声明抛出未被捕获的运行时异常。
-
异常是指java程序运行时(非编译)所发生的非正常情况或错误,与现实生活中的事件很相似,现实生活中的事件可以包含事件发生的时间、地点、人物、情节等信息,可以用一个对象来表示,Java使用面向对象的方式来处理异常,它把程序中发生的每个异常也都分别封装到一个对象来表示的,该对象中包含有异常的信息。
Java对异常进行了分类,不同类型的异常分别用不同的Java类表示,所有异常的根类为java.lang.Throwable,Throwable下面又派生了两个子类:Error和Exception,Error表示应用程序本身无法克服和恢复的一种严重问题,程序只有死的份了,例如,说内存溢出和线程死锁等系统问题。Exception表示程序还能够克服和恢复的问题,其中又分为系统异常和普通异常,系统异常是软件本身缺陷所导致的问题,也就是软件开发人员考虑不周所导致的问题,软件使用者无法克服和恢复这种问题,但在这种问题下还可以让软件系统继续运行或者让软件死掉,例如,数组脚本越界(ArrayIndexOutOfBoundsException),空指针异常(NullPointerException)、类转换异常(ClassCastException);普通异常是运行环境的变化或异常所导致的问题,是用户能够克服的问题,例如,网络断线,硬盘空间不够,发生这样的异常后,程序不应该死掉。
java为系统异常和普通异常提供了不同的解决方案,编译器强制普通异常必须try..catch处理或用throws声明继续抛给上层调用方法处理,所以普通异常也称为checked异常,而系统异常可以处理也可以不处理,所以,编译器不强制用try..catch处理或用throws声明,所以系统异常也称为unchecked异常。
提示答题者:就按照三个级别去思考:虚拟机必须宕机(就是死机)的错误,程序可以死掉也可以不死掉的错误,程序不应该死掉的错误;
-
请写出5种常见到的runtime exception
-
这道题主要考你的代码量到底多大,如果你长期写代码的,应该经常都看到过一些系统方面的异常,你不一定真要回答出5个具体的系统异常,但你要能够说出什么是系统异常,以及几个系统异常就可以了,当然,这些异常完全用其英文名称来写是最好的,如果实在写不出,那就用中文吧,有总比没有强!
所谓系统异常,就是…..,它们都是RuntimeException的子类,在jdk doc中查RuntimeException类,就可以看到其所有的子类列表,也就是看到了所有的系统异常。我比较有印象的系统异常有:NullPointerException,ArrayIndexOutOfBoundsException,ClassCastException,SQLException,FileNotFoundException,IOException......
-
int 和 Integer 有什么区别,Integer的值缓存范围
-
int是java提供的8种原始数据类型之一。Java为每个原始类型提供了封装类,Integer是java为int提供的封装类。int的默认值为0,而Integer的默认值为null,即Integer可以区分出未赋值和值为0的区别,int则无法表达出未赋值的情况,例如,要想表达出没有参加考试和考试成绩为0的区别,则只能使用Integer。在JSP开发中,Integer的默认为null,所以用el表达式在文本框中显示时,值为空白字符串,而int默认的默认值为0,所以用el表达式在文本框中显示时,结果为0,所以,int不适合作为web层的表单数据的类型。
在Hibernate中,如果将OID定义为Integer类型,那么Hibernate就可以根据其值是否为null而判断一个对象是否是临时的,如果将OID定义为了int类型,还需要在hbm映射文件中设置其unsaved-value属性为0。
另外,Integer提供了多个与整数相关的操作方法,例如,将一个字符串转换成整数,Integer中还定义了表示整数的最大值和最小值的常量。
- Boolean:(全部缓存)
- Byte:(全部缓存)
- Integer(-128 — 127缓存)
- Character(<= 127缓存)
- Short(-128 — 127缓存)
- Long(-128 — 127缓存)
- Float(没有缓存)
- Doulbe(没有缓存)
-
包装类,装箱和拆箱
-
包装类(原始类型对应的类,即Boolean, Byte, Short, Integer, Long, Float, Double, Character)的自动拆、装箱分别是指如下的过程:
装箱:是指基本类型变为相应包装类的过程,如Integer a=Integer.valueOf(100);或者int a=100;Integer b=new Integer(a);这里的Integer.valueOf(100)和new Integer(a)就是装箱,由基本数据类型构造出一个包装类的对象。
拆箱:就是装箱的逆过程。如Integer a=new Integer(100);int b=a.intValue();这里的a.intValue()就是拆箱的过程,由一个包装类对象转换到相应的基本数据类型。
自动装箱、拆箱:指编译器帮助开发人员完成包装类的装箱和拆箱过程,也就是在将*.java文件编译成*.class文件的过程中完成。本文的目的是要介绍一下,编译器在何时才进行自动装箱、拆箱。
包装类的自动装箱和拆箱规则如下:
- 遇到赋值运算符“=”(包括传参数,参数传递属于隐式赋值)时,会将包装类拆箱或装箱为相应类型
- 遇到算术运算符、位运算符和位移运算符“+, ++, --, -, *, /, %, &, |, ^, ~, <<, >>, >>>”时,对包装类进行拆箱
- 遇到关系运算符">, <, >=, <="(不包括“==”和“!=”哦)时,对包装类进行拆箱
- 对关系运算符"==, !="而言,遇到数字常量或算术表达式时,才对包装类进行拆箱
其余情况下,不进行自动的装箱和拆箱,注意啊,这里说的是自动装箱和拆箱。 -
String、StringBuilder、StringBuffer
-
- 修改字符串速度
StringBuilder>StringBuffer>String - 内容是否可变
只有String不可变。 - 线程安全
只有StringBuilder是线程不安全的。
String作为不可变类,是明显线程安全的,Java中所有不可变类都是线程安全的。
StringBuffer类是可变类,但是StringBuffer类中实现的方法都是被Sychronized关键字所修饰的,因此它靠锁实现了线程安全。
Stringbuilder类是可变类,并且方法没有被Sychronized修饰,因此它是线程不安全的。JAVA平台提供了两个类:String和StringBuffer,它们可以储存和操作字符串,即包含多个字符的字符数据。这个String类提供了数值不可改变的字符串。而这个StringBuffer类提供的字符串进行修改。当你知道字符数据要改变的时候你就可以使用StringBuffer。典型地,你可以使用StringBuffers来动态构造字符数据。另外,String实现了equals方法,new String(“abc”).equals(newString(“abc”)的结果为true,而StringBuffer没有实现equals方法,所以,new StringBuffer(“abc”).equals(new StringBuffer(“abc”)的结果为false。
接着要举一个具体的例子来说明,我们要把1到100的所有数字拼起来,组成一个串。
StringBuffer sbf = newStringBuffer();
for(int i=0;i<100;i++) {
sbf.append(i);
}
上面的代码效率很高,因为只创建了一个StringBuffer对象,而下面的代码效率很低,因为创建了101个对象。
String str = new String();
for(int i=0;i<100;i++) {
str = str + i;
}
String覆盖了equals方法和hashCode方法,而StringBuffer没有覆盖equals方法和hashCode方法,所以,将StringBuffer对象存储进Java集合类中时会出现问题。
在讲两者区别时,应把循环的次数搞成10000,然后用endTime-beginTime来比较两者执行的时间差异,最后还要讲讲StringBuilder与StringBuffer的区别。(区别如下)
StringBuffer线程安全的可变字符序列。一个类似于 String的字符串缓冲区,但不能修改。虽然在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容,可将字符串缓冲区安全地用于多个线程。可以在必要时对这些方法进行同步,因此任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致。(从 JDK 5开始,为该类补充了一个单个线程使用的等价类,即 StringBuilder。与该类相比,通常应该优先使用 StringBuilder类,因为它支持所有相同的操作,但由于它不执行同步,所以速度更快。)
StringBuilder一个可变的字符序列。此类提供一个与 StringBuffer兼容的 API,但不保证同步。该类被设计用作 StringBuffer的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。(如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer要快。但将 StringBuilder的实例用于多个线程是不安全的。如果需要这样的同步,则建议使用 StringBuffer。)
StringBuffer和Stringbuilder上的主要操作都是 append和 insert 方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串缓冲区中。append方法始终将这些字符添加到缓冲区的末端;而 insert方法则在指定的点添加字符。
例如,如果 z 引用一个当前内容是“start”的字符串缓冲区对象,则此方法调用z.append("le")会使字符串缓冲区包含“startle”,而 z.insert(4, "le")将更改字符串缓冲区,使之包含“starlet”。
通常,如果 sb 引用 StringBuilder 的一个实例,则sb.append(x)和 sb.insert(sb.length(), x)具有相同的效果。只要发生有关源序列(如在源序列中追加或插入)的操作,该类就只在执行此操作的字符串缓冲区上而不是在源上实现同步。
- 修改字符串速度
-
重载和重写的区别
-
Overload是重载的意思,Override是覆盖的意思,也就是重写。
重载Overload表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同(即参数个数或类型不同)。
重写Override表示子类中的方法可以与父类中的某个方法的名称和参数完全相同,通过子类创建的实例对象调用这个方法时,将调用子类中的定义方法,这相当于把父类中定义的那个完全相同的方法给覆盖了,这也是面向对象编程的多态性的一种表现。子类覆盖父类的方法时,只能比父类抛出更少的异常,或者是抛出父类抛出的异常的子异常,因为子类可以解决父类的一些问题,不能比父类有更多的问题。子类方法的访问权限只能比父类的更大,不能更小。如果父类的方法是private类型,那么,子类则不存在覆盖的限制,相当于子类中增加了一个全新的方法。
至于Overloaded的方法是否可以改变返回值的类型这个问题,要看你倒底想问什么呢?这个题目很模糊。如果几个Overloaded的方法的参数列表不一样,它们的返回者类型当然也可以不一样。但我估计你想问的问题是:如果两个方法的参数列表完全一样,是否可以让它们的返回值不同来实现重载Overload。这是不行的,我们可以用反证法来说明这个问题,因为我们有时候调用一个方法时也可以不定义返回结果变量,即不要关心其返回结果,例如,我们调用map.remove(key)方法时,虽然remove方法有返回值,但是我们通常都不会定义接收返回结果的变量,这时候假设该类中有两个名称和参数列表完全相同的方法,仅仅是返回类型不同,java就无法确定编程者倒底是想调用哪个方法了,因为它无法通过返回结果类型来判断。
override可以翻译为覆盖,从字面就可以知道,它是覆盖了一个方法并且对其重写,以求达到不同的作用。对我们来说最熟悉的覆盖就是对接口方法的实现,在接口中一般只是对方法进行了声明,而我们在实现时,就需要实现接口声明的所有方法。除了这个典型的用法以外,我们在继承中也可能会在子类覆盖父类中的方法。在覆盖要注意以下的几点:
1、覆盖的方法的标志必须要和被覆盖的方法的标志完全匹配,才能达到覆盖的效果;
2、覆盖的方法的返回值必须和被覆盖的方法的返回一致;
3、覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致,或者是其子类;
4、被覆盖的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。
overload对我们来说可能比较熟悉,可以翻译为重载,它是指我们可以定义一些名称相同的方法,通过定义不同的输入参数来区分这些方法,然后再调用时,JVM就会根据不同的参数样式,来选择合适的方法执行。在使用重载要注意以下的几点:
1、在使用重载时只能通过不同的参数样式。例如,不同的参数类型,不同的参数个数,不同的参数顺序(当然,同一方法内的几个参数类型必须不一样,例如可以是fun(int,float),但是不能为fun(int,int));
2、不能通过访问权限、返回类型、抛出的异常进行重载;
3、方法的异常类型和数目不会对重载造成影响;
4、对于继承来说,如果某一方法在父类中是访问权限是priavte,那么就不能在子类对其进行重载,如果定义的话,也只是定义了一个新方法,而不会达到重载的效果。
-
抽象类和接口有什么区别
-
1.abstract class表示继承关系,"is-a"关系,单继承
可以有数据成员,默认friendly
可以有非抽象方法和抽象方法,抽象方法只能是public或protected
2.interface表示like-a"关系,一个类可以实现多个接口
只能有静态数据成员(默认是public staic final)
只能有类型抽象方法,且public类型 -
接口可以继承接口。抽象类可以实现(implements)接口,抽象类可以继承具体类。抽象类中可以有静态的main方法。
-
备注:只要明白了接口和抽象类的本质和作用,这些问题都很好回答,你想想,如果你是java语言的设计者,你是否会提供这样的支持,如果不提供的话,有什么理由吗?如果你没有道理不提供,那答案就是肯定的了。
只有记住抽象类与普通类的唯一区别就是不能创建实例对象和允许有abstract方法。
-
说说反射的用途及实现
-
Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制
-
在运行时构造一个类的对象;判断一个类所具有的成员变量和方法;调用一个对象的方法;生成动态代理。反射最大的应用就是框架
-
Java反射机制主要用于实现以下功能。
(1)在运行时判断任意一个对象所属的类型。
(2)在运行时构造任意一个类的对象。
(3)在运行时判断任意一个类所具有的成员变量和方法。
(4)在运行时调用任意一个对象的方法,甚至可以调用private方法
-
说说自定义注解的场景及实现
-
垂直化编程,就是A—B—C—D…等执行下去,一个逻辑一个逻辑完了再执行下一个,但是spring 中AOP提供了一种思想,它的作用就是,当在业务不知情的情况下,对业务代码的功能的增强,这种思想使用的场景,例如事务提交、方法执行之前的权限检测、日志打印、方法调用事件等等
-
java在我们要自定义注解的时候提供了它自己的自定义语法以及元注解,元注解(负责注解其他注解): Java5.0定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。Java5.0定义的元注解:
1.@Target,
2.@Retention,
3.@Documented,
4.@Inherited
这些类型和它们所支持的类在java.lang.annotation包中可以找到。
1.@Target:用户描述注解的作用范围
取值(ElementType)有:
1.CONSTRUCTOR:用于描述构造器
2.FIELD:用于描述域
3.LOCAL_VARIABLE:用于描述局部变量
4.METHOD:用于描述方法
5.PACKAGE:用于描述包
6.PARAMETER:用于描述参数
7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
2.@Retention:表示需要在什么级别保存该注释信息
取值(RetentionPoicy)有:
1.SOURCE:在源文件中有效(即源文件保留)
2.CLASS:在class文件中有效(即class保留)
3.RUNTIME:在运行时有效(即运行时保留)(常用)
3.@Documented:Documented是一个标记注解
4.@Inherited :用于声明一个注解; -
HTTP请求的GET与POST方式的区别
-
(1) 在客户端,Get方式在通过URL提交数据,数据在URL中可以看到;POST方式,数据放置在HTML HEADER内提交。
(2) GET方式提交的数据最多只能有1024字节,而POST则没有此限制。
(3) 安全性问题。正如在(1)中提到,使用 Get的时候,参数会显示在地址栏上,而 Post不会。所以,如果这些数据是中文数据而且是非敏感数据,那么使用 get;如果用户输入的数据不是中文字符而且包含敏感数据,那么还是使用 post为好。
(4) 安全的和幂等的。所谓安全的意味着该操作用于获取信息而非修改信息。幂等的意味着对同一 URL的多个请求应该返回同样的结果。完整的定义并不像看起来那样严格。换句话说,GET请求一般不应产生副作用。从根本上讲,其目标是当用户打开一个链接时,她可以确信从自身的角度来看没有改变资源。比如,新闻站点的头版不断更新。虽然第二次请求会返回不同的一批新闻,该操作仍然被认为是安全的和幂等的,因为它总是返回当前的新闻。反之亦然。POST请求就不那么轻松了。POST表示可能改变服务器上的资源的请求。仍然以新闻站点为例,读者对文章的注解应该通过 POST请求实现,因为在注解提交之后站点已经不同了
-
Session与Cookie区别
-
Cookie保存在客户端,未设置存储时间的cookie为会话cookie保存在浏览器的进程开辟的内存中,当浏览器关闭后会话cookie也会被删除;设置了存储时间的cookie保存在用户设备的磁盘中直到过期。
session保存在服务器端,存储在IIS的进程开辟的内存中。
当服务器端生成一个session时就会向客户端发送一个cookie保存在客户端,这个cookie保存的是session的sessionId。这样才能保证客户端发起请求后客户端已经登录的用户能够与服务器端成千上万的session中准确匹配到已经保存了该用户信息的session,同时也能够确保不同页面之间传值时的正确匹配。
注:为了防止客户端禁用了cookie而无法使用session的情况可以把sessionId和其他用户信息重写到url中,每次请求都在url中附带sessionId和用户信息(不包含用户的敏感信息)
-
列出自己常用的JDK包
-
1.java.lang:语言包
2.java.util:实用包
3.java.awt:抽象窗口工具包
4.javax.swing:轻量级的窗口工具包,这是目前使用最广泛的GUI程序设计包
5.java.io:输入输出包
6.java.net:网络函数包
7.java.applet:编制applet用到的包(目前编制applet程序时,更多的是使用swing中的JApplet类)。
-
MVC设计思想
-
MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,M是指业务模型,V是指用户界面,C是指控制器,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。
MVC是软件开发过程中比较流行的设计思想。应该明确一点就是,MVC是设计模式,设计思想,不是一种编程技术。
在web开发中最典型的是JSP+servlet+javabean模式,其思想的核心概念如下:
Model:封装应用程序的数据结构和事务逻辑,集中体现应用程序的状态,当数据状态改变是,能够在试图里面体现出来。JavaBean非常适合这个角色。
View:是Model是外在表现,模型状态改变是,有所体现,JSP非常适合这个角色。
Controller:是对用户输入进行相应,将模型和试图联系在一起,负责将数据写到模型中,并调用视图。Servlet非常适合这个角色。
MVC思想如图:
MVC的步骤如下:
1.用户在表单中输入,表单提交给Servlet,Servlet验证输入,然后实例化JavaBean
2,JavaBean查询数据库,查询结果暂存在JavaBean中。
3,Servlet跳转到JSP,JSP使用JavaBean得到它里面的查询结果,并显示出来。
-
equals与==的区别
-
值类型(int,char,long,boolean等)都是用==判断相等性。对象引用的话,==判断引用所指的对象是否是同一个。equals是Object的成员函数,有些类会覆盖(override)这个方法,用于判断对象的等价性。例如String类,两个引用所指向的String都是"abc",但可能出现他们实际对应的对象并不是同一个(和jvm实现方式有关),因此用==判断他们可能不相等,但用equals判断一定是相等的。
(单独把一个东西说清楚,然后再说清楚另一个,这样,它们的区别自然就出来了,混在一起说,则很难说清楚)
==操作符专门用来比较两个变量的值是否相等,也就是用于比较变量所对应的内存中所存储的数值是否相同,要比较两个基本类型的数据或两个引用变量是否相等,只能用==操作符。
如果一个变量指向的数据是对象类型的,那么,这时候涉及了两块内存,对象本身占用一块内存(堆内存),变量也占用一块内存,例如Objet obj = new Object();变量obj是一个内存,new Object()是另一个内存,此时,变量obj所对应的内存中存储的数值就是对象占用的那块内存的首地址。对于指向对象类型的变量,如果要比较两个变量是否指向同一个对象,即要看这两个变量所对应的内存中的数值是否相等,这时候就需要用==操作符进行比较。
equals方法是用于比较两个独立对象的内容是否相同,就好比去比较两个人的长相是否相同,它比较的两个对象是独立的。例如,对于下面的代码:
String a=new String("foo");
String b=new String("foo");
两条new语句创建了两个对象,然后用a,b这两个变量分别指向了其中一个对象,这是两个不同的对象,它们的首地址是不同的,即a和b中存储的数值是不相同的,所以,表达式a==b将返回false,而这两个对象中的内容是相同的,所以,表达式a.equals(b)将返回true。
在实际开发中,我们经常要比较传递进行来的字符串内容是否等,例如,String input = …;input.equals(“quit”),许多人稍不注意就使用==进行比较了,这是错误的,随便从网上找几个项目实战的教学视频看看,里面就有大量这样的错误。记住,字符串的比较基本上都是使用equals方法。
如果一个类没有自己定义equals方法,那么它将继承Object类的equals方法,Object类的equals方法的实现代码如下:
boolean equals(Object o){
return this==o;
}
这说明,如果一个类没有自己定义equals方法,它默认的equals方法(从Object类继承的)就是使用==操作符,也是在比较两个变量指向的对象是否是同一对象,这时候使用equals和使用==会得到同样的结果,如果比较的是两个独立的对象则总返回false。如果你编写的类希望能够比较该类创建的两个实例对象的内容是否相同,那么你必须覆盖equals方法,由你自己写代码来决定在什么情况即可认为两个对象的内容是相同的。
-
hashCode和equals方法的区别与联系
-
hashCode 方法是基类Object中的 实例native方法,因此对所有继承于Object的类都会有该方法。
-
哈希相关概念
我们首先来了解一下哈希表:-
概念 : Hash 就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出(int),该输出就是散列值。这种转换是一种 压缩映射,也就是说,散列值的空间通常远小于输入的空间。不同的输入可能会散列成相同的输出,从而不可能从散列值来唯一的确定输入值。简单的说,就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
-
应用–数据结构 : 数组的特点是:寻址容易,插入和删除困难; 而链表的特点是:寻址困难,插入和删除容易。那么我们能不能综合两者的特性,做出一种寻址容易,插入和删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法——拉链法,我们可以理解为 “链表的数组”,如图:
图1 哈希表示例左边很明显是个数组,数组的每个成员是一个链表。该数据结构所容纳的所有元素均包含一个指针,用于元素间的链接。我们根据元素的自身特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。其中,将根据元素特征计算元素数组下标的方法就是散列法。
-
拉链法的适用范围 : 快速查找,删除的基本数据结构,通常需要总数据量可以放入内存。
- 要点 :
hash函数选择,针对字符串,整数,排列,具体相应的hash方法;
碰撞处理,一种是open hashing,也称为拉链法,另一种就是closed hashing,也称开地址法,opened addressing
前提: 谈到hashCode就不得不说equals方法,二者均是Object类里的方法。由于Object类是所有类的基类,所以一切类里都可以重写这两个方法。
- 原则 1 : 如果 x.equals(y) 返回 “true”,那么 x 和 y 的 hashCode() 必须相等 ;
- 原则 2 : 如果 x.equals(y) 返回 “false”,那么 x 和 y 的 hashCode() 有可能相等,也有可能不等 ;
- 原则 3 : 如果 x 和 y 的 hashCode() 不相等,那么 x.equals(y) 一定返回 “false” ;
- 原则 4 : 一般来讲,equals 这个方法是给用户调用的,而 hashcode 方法一般用户不会去调用 ;
- 原则 5 : 当一个对象类型作为集合对象的元素时,那么这个对象应该拥有自己的equals()和hashCode()设计,而且要遵守前面所说的几个原则。
-
-
什么是Java序列化和反序列化,如何实现Java序列化?或者请解释Serializable 接口的作用
-
序列化:把对象转换为字节序列的过程称为对象的序列化。
反序列化:把字节序列恢复为对象的过程称为对象的反序列化。 -
实现Serializable接口即可
-
transient 修饰的属性,是不会被序列化的 静态static的属性,不序列化
-
Object类中常见的方法,为什么wait notify会放在Object里边?
-
a、这些方法用于同步中
b、使用这些方法时必须要标识所属的同步的锁
c、锁可以是任意对象,所以任意对象调用的方法一定是定义在Object类中
-
Java的平台无关性如何体现出来的
-
Java平台无关的能力给予网络一个同构的运行环境,使得分布式系统可以围绕着“网络移动对象”开构建。比如对象序列化,RMI, Jini就是利用平台无关性。把面向对象编程从虚拟机带到了网络上。
-
JDK和JRE的区别
-
JDK,开发java程序用的开发包,JDK里面有java的运行环境(JRE),包括client和server端的。需要配置环境变量。。。。
JRE,运行java程序的环境,JVM,JRE里面只有client运行环境,安装过程中,会自动添加PATH。 -
Java 8有哪些新特性
-
1、接口的默认方法2、Lambda 表达式3、函数式接口4、方法与构造函数引用5、Lambda 作用域6、访问局部变量7、访问对象字段与静态变量8、访问接口的默认方法9、Date API10、Annotation 注解
1.2、Java常见集合
-
List 和 Set 区别
-
Set和hashCode以及equals方法的联系
-
List 和 Map 区别
-
Arraylist 与 LinkedList 区别
-
ArrayList 与 Vector 区别
-
HashMap 和 Hashtable 的区别
-
HashSet 和 HashMap 区别
-
以上问题可查看一下博客(Collection)
-
HashMap 和 ConcurrentHashMap 的区别
-
HashMap 的工作原理及代码实现,什么时候用到红黑树
-
多线程情况下HashMap死循环的问题
-
以上问题可查看一下博客(HashMap)
-
HashMap出现Hash DOS攻击的问题
-
ConcurrentHashMap 的工作原理及代码实现,如何统计所有的元素个数
-
手写简单的HashMap
-
- public interface DIYMap<K,V> {
- //Map双列集合 基本功能是 快速取
- public V put(K k,V v);
- //快速取
- public V get(K k);
- //定义一个内部接口
- public interface Entry<K,V>{
- public K getKey();
- public V getValue();
- }
- }
- import java.util.ArrayList;
- import java.util.List;
- /*
- 了解hashmap中entry实体的结构
- crc16算法
- hashmap底层=数组+链表
- 通过hash算法带来的好处, 快存快取 / 数组在存的时候是需要遍历的
- HashMap底层是怎么回事?
- */
- public class DIYHashMap<K, V> implements DIYMap<K, V>{
- //定义默认数组大小
- private int defaultLenth=16;
- //负载因子,扩容标准 useSize/数组长度>0.75扩容
- private double defaultAddSizeFactor=0.75;
- //使用数组位置的总数
- private double useSize;
- //定义Map 骨架之一数组
- private Entry<K, V>[] table;
- public DIYHashMap(int defaultLenth, double defaultAddSizeFactor) {
- if(defaultLenth<0){
- throw new IllegalArgumentException("数组长度为负数"+defaultLenth);
- }
- if(defaultAddSizeFactor<=0 || Double.isNaN(defaultAddSizeFactor)){
- throw new IllegalArgumentException("扩容标准必须大于0的数字"+defaultLenth);
- }
- this.defaultLenth = defaultLenth;
- this.defaultAddSizeFactor = defaultAddSizeFactor;
- table=new Entry[defaultLenth];
- }
- //快速存取 hash算法
- public V put(K k, V v) {
- if(useSize>defaultAddSizeFactor*defaultLenth){
- //扩容
- up2Size();
- }
- //通过key来计算出 存储的位置
- int index=getIndex(k,table.length);
- Entry<K, V> entry=table[index];
- Entry<K, V> newEntry=new Entry<K, V>(k, v, null);
- if(entry==null){
- table[index]=newEntry;
- useSize++;
- }else{//维护数组相同位置队列
- Entry<K, V> tmp;
- while((tmp=table[index])!=null){
- tmp=tmp.next;
- }
- tmp.next=newEntry;
- }
- return newEntry.getValue();
- }
- private int getIndex(K k, int length) {
- //通常hashCode 取膜法
- int m=length-1;
- int index=hash(k.hashCode()) & m;
- return index >= 0 ? index : -index;
- }
- //创建自己的hash算法,保证计算出的位置 在数组中均匀分布
- private int hash(int hashCode) {
- hashCode=hashCode^((hashCode>>>20)^(hashCode>>>12));
- return hashCode^((hashCode>>>7)^(hashCode>>>4));
- }
- //扩容数组
- private void up2Size() {
- Entry<K, V>[] newTable=new Entry[defaultLenth*2];
- //将原table中的entry重新,散列到新的table中
- againHash(newTable);
- }
- //将原table中的entry重新,散列到新的table中
- private void againHash(Entry<K, V>[] newTable) {
- //数组里面对象 封装到list中,包括同一位置 有列表结构的都解析出来
- List<Entry<K,V>> entryList=new ArrayList<Entry<K,V>>();
- for(int i=0;i<table.length;i++){
- if(table[i]==null){
- continue;
- }
- findEntryByNext(table[i],entryList);
- }
- if(entryList.size()>0){
- useSize=0;
- defaultLenth=defaultLenth*2;
- table=newTable;
- for (Entry<K, V> entry : entryList) {
- if(entry.next!=null){
- entry.next=null;
- }
- put(entry.getKey(), entry.getValue());
- }
- }
- }
- private void findEntryByNext(Entry<K, V> entry, List<Entry<K, V>> entryList) {
- if(entry!=null && entry.next!=null){
- //这个entry对象已经形成链表结构
- entryList.add(entry);
- //递归 将链表中的entry实体 都一次封装到entryList链表中
- findEntryByNext(entry.next, entryList);
- }else{
- entryList.add(entry);
- }
- }
- //快取
- public V get(K k) {
- //通过key来计算出 存储的位置
- int index=getIndex(k,table.length);
- Entry<K, V> entry=table[index];
- if(entry==null){
- throw new NullPointerException();
- }
- return findValueByKey(k,entry);
- }
- private V findValueByKey(K k, Entry<K, V> entry) {
- if(k == entry.getKey() || k.equals(entry.getKey())){
- return entry.v;
- }else if(entry.next!=null){
- return findValueByKey(k,entry.next);
- }
- return null;
- }
- class Entry<K, V> implements DIYMap.Entry<K, V>{
- K k;
- V v;
- //指向被this挤压下去的entry
- Entry<K, V> next;
- public Entry(K k, V v, Entry<K, V> next) {
- this.k = k;
- this.v = v;
- this.next = next;
- }
- @Override
- public K getKey() {
- return k;
- }
- @Override
- public V getValue() {
- return v;
- }
- }
- }
-
看过那些Java集合类的源码
1.3、进程和线程
-
线程和进程的概念、并行和并发的概念
-
进程和线程
进程是一个程序的实例。每个进程都有自己的虚拟地址空间和控制线程,线程是操作系统调度器(Schduler)分配处理器时间的基础单元。
并发:
讲并发之前,要先看一张图:
- Concurrency,是并发的意思。并发的实质是一个物理CPU(也可以多个物理CPU) 在若干道程序(或线程)之间多路复用,并发性是对有限物理资源强制行使多用户共享以提高效率。
- 微观角度:所有的并发处理都有排队等候,唤醒,执行等这样的步骤,在微观上他们都是序列被处理的,如果是同一时刻到达的请求(或线程)也会根据优先级的不同,而先后进入队列排队等候执行。
- 宏观角度:多个几乎同时到达的请求(或线程)在宏观上看就像是同时在被处理。
- 通俗点讲,并发就是只有一个CPU资源,程序(或线程)之间要竞争得到执行机会。图中的第一个阶段,在A执行的过程中B,C不会执行,因为这段时间内这个CPU资源被A竞争到了,同理,第二个阶段只有B在执行,第三个阶段只有C在执行。其实,并发过程中,A,B,C并不是同时在进行的(微观角度)。但又是同时进行的(宏观角度)。
并行:
同样,在讲并行之前,要先看一张图:
- Parallelism,即并行,指两个或两个以上事件(或线程)在同一时刻发生,是真正意义上的不同事件或线程在同一时刻,在不同CPU资源呢上(多核),同时执行。
- 并行,不存在像并发那样竞争,等待的概念。
- 图中,A,B,C都在同时运行(微观,宏观)。
通过多线程实现并发,并行:
- java中的Thread类定义了多线程,通过多线程可以实现并发或并行。
- 在CPU比较繁忙,资源不足的时候(开启了很多进程),操作系统只为一个含有多线程的进程分配仅有的CPU资源,这些线程就会为自己尽量多抢时间片,这就是通过多线程实现并发,线程之间会竞争CPU资源争取执行机会。
- 在CPU资源比较充足的时候,一个进程内的多线程,可以被分配到不同的CPU资源,这就是通过多线程实现并行。
- 至于多线程实现的是并发还是并行?上面所说,所写多线程可能被分配到一个CPU内核中执行,也可能被分配到不同CPU执行,分配过程是操作系统所为,不可人为控制。所有,如果有人问我我所写的多线程是并发还是并行的?我会说,都有可能。
- 不管并发还是并行,都提高了程序对CPU资源的利用率,最大限度地利用CPU资源。
-
创建线程的方式及实现
-
1. 继承Thread类创建线程类
- package com.thread;
- public class FirstThreadTest extends Thread{
- int i = 0;
- //重写run方法,run方法的方法体就是现场执行体
- public void run()
- {
- for(;i<100;i++){
- System.out.println(getName()+" "+i);
- }
- }
- public static void main(String[] args)
- {
- for(int i = 0;i< 100;i++)
- {
- System.out.println(Thread.currentThread().getName()+" : "+i);
- if(i==20)
- {
- new FirstThreadTest().run();
- new FirstThreadTest().run();
- }
- }
- }
- }
2. 通过Runable接口创建线程类
- package com.thread;
- public class RunnableThreadTest implements Runnable
- {
- private int i;
- public void run()
- {
- for(i = 0;i <100;i++)
- {
- System.out.println(Thread.currentThread().getName()+" "+i);
- }
- }
- public static void main(String[] args)
- {
- for(int i = 0;i < 100;i++)
- {
- System.out.println(Thread.currentThread().getName()+" "+i);
- if(i==20)
- {
- RunnableThreadTest rtt = new RunnableThreadTest();
- new Thread(rtt,"新线程1").start();
- new Thread(rtt,"新线程2").start();
- }
- }
- }
- }
3. 通过Callable和FutureTask创建线程
a. 创建Callable接口的实现类,并实现call()方法;
b. 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callback对象的call()方法的返回值;
c. 使用FutureTask对象作为Thread对象的target创建并启动新线程;
d. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
- package com.demo;
- import java.util.concurrent.Callable;
- import java.util.concurrent.ExecutionException;
- import java.util.concurrent.FutureTask;
- public class CallableThreadTest implements Callable<Integer>
- {
- public static void main(String[] args)
- {
- CallableThreadTest ctt = new CallableThreadTest();
- FutureTask<Integer> ft = new FutureTask<Integer>(ctt);
- // Thread thread = new Thread(ft,"有返回值的线程");
- // thread.start();
- for(int i = 0;i < 100;i++)
- {
- System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
- if(i==20)
- {
- new Thread(ft,"有返回值的线程").start();
- }
- }
- try
- {
- System.out.println("子线程的返回值:"+ft.get());
- } catch (InterruptedException e)
- {
- e.printStackTrace();
- } catch (ExecutionException e)
- {
- e.printStackTrace();
- }
- }
- @Override
- public Integer call() throws Exception
- {
- int i = 0;
- for(;i<100;i++)
- {
- System.out.println(Thread.currentThread().getName()+" "+i);
- }
- return i;
- }
- }
4. 通过线程池创建线程
- /**
- *
- */
- package com.demo;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- /**
- * @author Maggie
- *
- */
- public class ThreadPool
- {
- /* POOL_NUM */
- private static int POOL_NUM = 10;
- /**
- * Main function
- */
- public static void main(String[] args)
- {
- ExecutorService executorService = Executors.newFixedThreadPool(5);
- for(int i = 0; i<POOL_NUM; i++)
- {
- RunnableThread thread = new RunnableThread();
- executorService.execute(thread);
- }
- }
- }
- class RunnableThread implements Runnable
- {
- private int THREAD_NUM = 10;
- public void run()
- {
- for(int i = 0; i<THREAD_NUM; i++)
- {
- System.out.println("线程" + Thread.currentThread() + " " + i);
- }
- }
- }
-
进程间通信的方式(线程间通信)
-
1.管道(pipe):
管道可用于具有亲缘关系进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
2.信号(signal):
信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致得。
3.消息队列(message queue):
消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。
4.共享内存(shared memory):
可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
5.信号量(semaphore):
主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
6.套接字(socket);
这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。
-
1.闭锁CountDownLatch做减计数,而栅栏CyclicBarrier则是加计数。
2.CountDownLatch是一次性的,CyclicBarrier可以重用。
3.CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成。
4.鉴于上面的描述,CyclicBarrier在一些场景中可以替代CountDownLatch实现类似的功能。
另外,值得一提的是,CountDownLatch和CyclicBarrier在创建和启动线程时,都没有明确提到同时启动全部线程,事实上这在技术上是不大可能,不必要,不提倡的。 -
说说 Semaphore 原理(代码)
-
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源,就好比现在的旅游景点限流。
主要方法:
* acquire():从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。 获取一个许可(如果提供了一个)并立即返回,将可用的许可数减 1。
* release():释放一个许可,将其返回给信号量,即:将可用的许可数增加 1protected方法。 -
说说 Exchanger 原理(代码)
-
Java并发API提供了一种允许2个并发任务间相互交换数据的同步应用。更具体的说,Exchanger类允许在2个线程间定义同步点,当2个线程到达这个点,他们相互交换数据类型,使用第一个线程的数据类型变成第二个的,然后第二个线程的数据类型变成第一个的。
用于实现两个人之间的数据交换,每个人在完成一定的事务后想与对方交换数据,第一个先拿出数据的人将一直等待第二个人拿着数据到来时,才能彼此交换数据。
-
ThreadLocal类用来提供线程内部的局部变量。这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型。
总结:ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性。ThreadLocal的主要应用场景为按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。例如:同一个网站登录用户,每个用户服务器会为其开一个线程,每个线程中创建一个ThreadLocal,里面存用户基本信息等,在很多页面跳转时,会显示用户信息或者得到用户的一些信息等频繁操作,这样多线程之间并没有联系而且当前线程也可以及时获取想要的数据。
-
讲讲线程池的实现原理(代码)
-
多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。
假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。
如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
一个线程池包括以下四个基本组成部分:
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。
线程池不仅调整T1,T3产生的时间段,而且它还显著减少了创建线程的数目,看一个例子:
假设一个服务器一天要处理50000个请求,并且每个请求需要一个单独的线程完成。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目,而如果服务器不利用线程池来处理这些请求则线程总数为50000。一般线程池大小是远小于50000。所以利用线程池的服务器程序不会为了创建50000而在处理请求时浪费时间,从而提高效率。代码实现中并没有实现任务接口,而是把Runnable对象加入到线程池管理器(ThreadPool),然后剩下的事情就由线程池管理器(ThreadPool)来完成了
Java通过ThreadPoolExecutor提供线程池:
ThreadPoolExecutor作为java.util.concurrent包对外提供基础实现,以内部线程池的形式对外提供管理任务执行,线程调度,线程池管理等等服务;
线程池的参数:corePoolSize 为核心线程;maximunPoolSize为最大线程;
keepAliveTime为最长生命时间;unit是其时间单位;workQueue任务队列;
handler是过多线程之后的策略- ExecutorService singleThreadPool = new ThreadPoolExecutor(4,
- 10,
- 0L,
- TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>(1024),
- namedThreadFactory,
- new ThreadPoolExecutor.AbortPolicy());
-
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 -
线程的生命周期,状态是如何转移的
-
线程的生命周期开始:当Thread对象创建完成时
线程的生命周期结束:①当run()方法中代码正常执行完毕
②线程抛出一个未捕获的异或错误时。
线程整个生命周期的五个阶段:
新建状态,就绪状态,运行状态,阻塞状态,死亡状态。
新建状态:
当Thread对象创建完成时, 使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。但此时它还不能运行,仅仅由虚拟机为其分配了内存,它保持这个状态直到程序 start() 这个线程。
就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列(可运行池里)中,此时它具备了运行的条件,能否获得CPU的使用权开始运行,还需要等待系统的调度。
运行状态:
如果就绪状态的线程获得CPU的使用权,开始执行 run()中的线程执行体,此时线程便处于运行状态。当使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其他线程获得执行的机会。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
阻塞状态:
一个正在线程的线程在某种特殊形况下,如执行了耗时的输入/输出操作时,sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。,若想再此进入就绪状态就需要使用notify()方法唤醒该线程。
同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
其他阻塞:通过调用线程的 sleep() 、或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 方法时间到了之后,join() 新加的线程运行结束后, I/O 处理完毕,线程重新转入就绪状态。
死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。 -
可参考:《Java多线程编程核心技术》
1.4、锁机制
-
说说线程安全问题,什么是线程安全,如何保证线程安全
-
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
- 不共享线程间的变量;
- 设置属性变量为不可变变量;
- 每个共享的可变变量都使用一个确定的锁保护;
- 1使用线程安全的类
2使用synchronized同步代码块,或者用Lock锁
3多线程并发情况下,线程共享的变量改为方法局部级变量
-
所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的
*synchronized 和 ReentrantLock 都是可重入锁
*可重入锁的意义在于防止死锁
*实现原理实现是通过为每个锁关联一个请求计数和一个占有它的线程。
*当计数为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,jvm将记录锁的占有者,并且讲请求计数器置为1 。
*如果同一个线程再次请求这个锁,计数将递增;
*每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。 -
产生死锁的四个条件(互斥、请求与保持、不剥夺、循环等待)(代码)
-
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不可强行占有:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
-
如何检查死锁(通过jConsole检查死锁)
-
Jconsole
Jconsole是JDK自带的图形化界面工具。
-
使用JDK给我们的的工具JConsole,可以通过打开cmd然后输入jconsole打开。
-
连接到需要查看的进程。
-
-
Jstack
Jstack是JDK自带的命令行工具,主要用于线程Dump分析。
- 我们先用Jps来查看java进程id。 2.看一下jstack的使用。
-
volatile 实现原理(禁止指令重排、刷新内存)(代码)
-
synchronized 实现原理(对象监视器)
-
这两段话就解释了synchronized的实现原理:
monitorenter:
每个对象有一个monitor,即监视器,当且仅当monitor被占用时,这个monitor就被锁住了。线程执行monitorenter指令是为了尝试获取该monitor的所有权,过程为:
1) 如果一个monitor的进入数为0,那么该线程直接进入monitor,并且将monitor进入数置为1,该线程成为该monitor的所有者;
2) 如果该进程是已经占用该monitor,则直接进入,并且monitor进入数加1;
3)如果该进程未占有该monitor,即monitor被其他线程所占有,那么该线程会被阻塞,直到该monitor的进入数变为0,此时该线程会再次尝试获取该monitor。
monitorexit:
执行monitorexit指令的线程必须是已经拥有该monitor的线程,执行monitorexit指令后,该monitor的进入数减1,直到该monitor的进入数减为0,此时该线程不再是该monitor的所有者,其他被阻塞进入该monitor的线程可以尝试获取该monitor的所有权。 -
synchronized 与 lock 的区别(代码)
-
类别 synchronized Lock 存在层次 Java的关键字,在jvm层面上 是一个类 锁的释放 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 在finally中必须释放锁,不然容易造成线程死锁 锁的获取 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待 锁状态 无法判断 可以判断 锁类型 可重入 不可中断 非公平 可重入 可判断 可公平(两者皆可) 性能 少量同步 大量同步 -
AQS同步队列(代码)
-
队列同步器(简称:同步器)AbstractQueuedSynchronizer(英文简称:AQS,也是面试官常问的什么是AQS的AQS),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
-
CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”,CAS是项 乐观锁 技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
-
常见的原子操作类(代码)
-
- AtomicBoolean:原子更新布尔变量
- AtomicInteger:原子更新整型变量
- AtomicLong:原子更新长整型变量
-
什么是ABA问题,出现ABA问题JDK是如何解决的(代码)
-
乐观锁的业务场景及实现方式(代码)
-
Java 8并法包下常见的并发类(代码)
-
偏向锁、轻量级锁、重量级锁、自旋锁的概念(各种锁)
-
可参考:《Java多线程编程核心技术》
1.5、JVM
-
JVM运行时内存区域划分(代码)
-
程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。
如上图所示,JVM中的运行时数据区应该包括这些部分。在JVM规范中虽然规定了程序在执行期间运行时数据区应该包括这几部分,但是至于具体如何实现并没有做出规定,不同的虚拟机厂商可以有不同的实现方式
-
内存溢出OOM和堆栈溢出SOE的示例及原因、如何排查与解决(代码)
-
如何判断对象是否可以回收或存活(代码)
-
常见的GC回收算法及其含义(代码)
-
常见的JVM性能监控和故障处理工具类:jps、jstat、jmap、jinfo、jconsole等(代码)
-
JVM如何设置参数(代码)
-
JVM性能调优(代码)
-
类加载器、双亲委派模型、一个类的生命周期、类是如何加载到JVM中的(代码)
-
类加载的过程:加载、验证、准备、解析、初始化
-
- 加载(Loading);
- 验证(Verification);
- 准备 (Preparation);
- 解析(Resolution);
- 初始化(Initialization);
- 使用(Using)
- 卸载 (Unloading)
其中验证、准备和解析三部分称为连接,在Java语言中,类型的加载和连接过程都是在程序运行期间完成的(Java可以动态扩展的语言特性就是依赖运行期动态加载、动态连接这个特点实现的),这样会在类加载时稍微增加一些性能开销,但是却为Java应用程序提供高度的灵活性
加载、验证、准备、初始化和卸载这5个阶段的顺序是固定的(即:加载阶段必须在验证阶段开始之前开始,验证阶段必须在准备阶段开始之前开始等。这些阶段都是互相交叉地混合式进行的,通常会在一个阶段的执行过程中调用或激活另一个阶段),解析阶段则不一定,在某些情况下,解析阶段有可能在初始化阶段结束后开始,以支持Java的动态绑定(动态绑定是指在执行期间(非编译期)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。程序运行过程中,把函数(或过程)调用与响应调用所需要的代码相结合的过程称为动态绑定。) -
强引用、软引用、弱引用、虚引用
-
1.强引用
以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。 -
-
-
-
强引用:
-
Java内存模型JMM(代码)
1.6、设计模式
-
常见的设计模式
-
设计模式的的六大原则及其含义
-
常见的单例模式以及各种实现方式的优缺点,哪一种最好,手写常见的单例模式
-
设计模式在实际场景中的应用
-
Spring中用到了哪些设计模式
-
MyBatis中用到了哪些设计模式
-
你项目中有使用哪些设计模式
-
说说常用开源框架中设计模式使用分析
-
动态代理很重要!!!
1.7、数据结构
-
树(二叉查找树、平衡二叉树、红黑树、B树、B+树)
-
深度有限算法、广度优先算法
-
克鲁斯卡尔算法、普林母算法、迪克拉斯算法
-
什么是一致性Hash及其原理、Hash环问题
-
常见的排序算法和查找算法:快排、折半查找、堆排序等
1.8、网络/IO基础
-
BIO、NIO、AIO的概念
-
1.Java对BIO、NIO、AIO的支持:
① Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
② Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
③ Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,2.BIO、NIO、AIO适用场景分析:
① BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
② NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
③ AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。 -
3.Java NIO和IO的主要区别
IO NIO 面向流 面向缓冲 阻塞IO 非阻塞IO 无 选择器 面向流与面向缓冲
Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
阻塞与非阻塞IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
选择器(Selectors)
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。 -
什么是长连接和短连接(代码)
-
Https的基本概念(代码)
-
从浏览器中输入URL到页面加载的发生了什么?可参考《代码》
二、数据存储和消息队列
2.1、数据库
-
MySQL 索引使用的注意事项(点击打开链接)
-
1.WHERE字句的查询条件里有 NOT IN 、<>、!=,MYSQL将无法使用索引;
2.WHERE字句的查询条件里使用了函数,MYSQL将无法使用索引
3.在JOIN操作中,MYSQL只有在主键和外键的数据类型相同时才能使用索引,否则即使建立了索引也不会使用
4.使用了比较操作符LIKE和REGEXP,MYSQL只有在搜索模板的第一个字符不是通配符的情况下才能使用索引。比如说,如果查询条件是LIKE 'abc%',MYSQL将使用索引;如果条件是LIKE '%abc'或者'_abc%',MYSQL将不使用索引。
5.在ORDER BY操作中,MYSQL只有在排序条件不是一个查询条件表达式的情况下才使用索引。尽管如此,在涉及多个数据表的查询里,即使有索引可用,那些索引在加快ORDER BY操作方面也没什么作用。
使用order by特别提示:
1>mysql一次查询只能使用一个索引。如果要对多个字段使用索引,建立复合索引。
2>在ORDER BY操作中,MySQL只有在排序条件不是一个查询条件表达式的情况下才使用索引。6.如果某个数据列里包含着许多重复的值,就算为它建立了索引也不会有很好的效果。比如说,如果某个数据列里包含了净是些诸如“0/1”或“Y/N”等值,就没有必要为它创建一个索引。
7.使用短索引: 对串列进行索引,如果可以就应该指定一个前缀长度。例如,如果有一个char(255)的列,如果在前10个或20个字符内,多数值是唯一的,那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和I/O操作。(针对hash的索引方式,对每个值都做hash值存储I/O操作存储索引信息)
8.如果条件中有or(并且其中有or的条件是不带索引的),即使其中有条件带索引也不会使用(这也是为什么尽量少用or的原因)。注意:要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引
9.如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
10.对于那些定义为text、image和bit数据类型的列不应该增加索引。因为这些列的数据量要么相当大,要么取值很少。
11.只要列中包含有NULL值,都将不会被包含在索引中,复合索引中只要有一列含有NULL值,那么这一列对于此符合索引就是无效的。
12.组合索引之最左前缀:顾名思义,就是最左优先,上例中我们创建了name_age_course多列索引,相当于创建了(name)单列索引,(name,age)组合索引以及(name,age,course)组合索引。在单独使用(age)、(age,course)等条件下索引无效
-
DDL、DML、DCL分别指什么
-
DML(data manipulation language):
它们是SELECT、UPDATE、INSERT、DELETE,就象它的名字一样,这4条命令是用来对数据库里的数据进行操作的语言
DDL(data definition language):
DDL比DML要多,主要的命令有CREATE、ALTER、DROP等,DDL主要是用在定义或改变表(TABLE)的结构,数据类型,表之间的链接和约束等初始化工作上,他们大多在建立表时使用
DCL(Data Control Language):
是数据库控制功能。是用来设置或更改数据库用户或角色权限的语句,包括(grant,deny,revoke等)语句。在默认状态下,只有sysadmin,dbcreator,db_owner或db_securityadmin等人员才有权力执行DCL -
explain命令
-
在工作中,我们用于捕捉性能问题最常用的就是打开慢查询,定位执行效率差的SQL,那么当我们定位到一个SQL以后还不算完事,我们还需要知道该SQL的执行计划,比如是全表扫描,还是索引扫描,这些都需要通过EXPLAIN去完成。EXPLAIN命令是查看优化器如何决定执行查询的主要方法。可以帮助我们深入了解MySQL的基于开销的优化器,还可以获得很多可能被优化器考虑到的访问策略的细节,以及当运行SQL语句时哪种策略预计会被优化器采用。需要注意的是,生成的QEP并不确定,它可能会根据很多因素发生改变。MySQL不会将一个QEP和某个给定查询绑定,QEP将由SQL语句每次执行时的实际情况确定,即便使用存储过程也是如此。尽管在存储过程中SQL语句都是预先解析过的,但QEP仍然会在每次调用存储过程的时候才被确定。
-
left join,right join,inner join(点击打开链接)
-
数据库事物ACID(原子性、一致性、隔离性、持久性)(点击打开链接)
-
事物的隔离级别(读未提交、读以提交、可重复读、可序列化读)(点击打开链接)
-
1、TransactionDefinition接口中定义五个隔离级别(isolation):
-
脏读、幻读、不可重复读
-
脏读:一个事务读取了另一个事务改写但还未提交的数据,如果这些数据被回滚,则读到的数据是无效的。
不可重复读:在同一事务中,多次读取到同一数据的结果有所不同。也就是说,后续读取可以读到另一个事务已经提交的更新数据。
幻读:一个事务读取了几行记录后,另一个事务插入一些记录,幻读就发生了,再查询的时候,第一个事务就会发现有些原来没有的记录。 -
数据库的几大范式(点击打开链接)
-
第一范式(1NF): 强调的是列的原子性,即列不能够再分成其他几列。(属性存在子集)
第二范式(2NF): 首先是 1NF,另外包含两部分内容,一是表必须有一个主键;二是没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的一部分。
第三范式(3NF):首先是 2NF,另外非主键列必须直接依赖于主键,不能存在传递依赖。即不能存在:非主键列 A 依赖于非主键列 B,非主键列 B 依赖于主键的情况。(A->B C->A这里得出结论C->B就是传递依赖,箭头理解:B依赖A)
-
数据库常见的命令
-
说说分库与分表设计(点击打开链接)
-
分库与分表带来的分布式困境与应对之策(如何解决分布式下的分库分表,全局表?)(点击打开链接)
-
说说 SQL 优化之道(点击打开链接)
-
*当只要一行数据时使用 LIMIT 1 **MySQL数据库引擎会在找到一条数据后停止搜索,而不是继续往后查少下一条符合记录的数据
*千万不要 ORDER BY RAND()
*避免 SELECT *
*应尽量避免在 where 子句中对字段进行表达式操作
*应尽量避免在where子句中对字段进行函数操作
*很多时候用 exists 代替 in 是一个好的选择
*应尽量避免在 where 子句中使用 != 或 <> 操作符
*对于连续的数值,能用 between 就不要用 in
*应尽量避免在 where 子句中使用 or 来连接条件 **如果一个字段有索引,一个字段没有索引,将导致引擎放弃使用索引而进行全表扫描
*一般来说,预判一下过滤条件的范围。由于数据库是从后向前解析 SQL 语句的,通常建议把能过滤最多结果的条件放在后面,(不是一定的,mysql会根据索引做一些优化),尽量使过滤数据量大的条件先被执行
*Join语句的优化 **尽可能减少Join语句中的NestedLoop的循环总次数;“永远用小结果集驱动大的结果集”
*ORDER BY的实现与优化 **优化Query语句中的ORDER BY的时候,尽可能利用已有的索引来避免实际的排序计算(order字段尽量出现在条件中,并且有索引),可以很大幅度的提升ORDER BY操作的性能。
*GROUP BY的实现与优化 **由于GROUP BY实际上也同样需要进行排序操作,而且与ORDER BY相比,GROUP BY主要只是多了排序之后的分组操作。当然,如果在分组的时候还使用了其他的一些聚合函数,那么还需要一些聚合函数的计算。所以,在GROUP BY的实现过程中,与ORDER BY一样也可以利用到索引。
-
MySQL遇到的死锁问题、如何排查与解决(点击打开链接)
-
存储引擎的 InnoDB与MyISAM区别,优缺点,使用场景
-
主要区别:
- 1).MyISAM是非事务安全型的,而InnoDB是事务安全型的。
- 2).MyISAM锁的粒度是表级,而InnoDB支持行级锁定。
- 3).MyISAM支持全文类型索引,而InnoDB不支持全文索引。
- 4).MyISAM相对简单,所以在效率上要优于InnoDB,小型应用可以考虑使用MyISAM。
- 5).MyISAM表是保存成文件的形式,在跨平台的数据转移中使用MyISAM存储会省去不少的麻烦。
- 6).InnoDB表比MyISAM表更安全,可以在保证数据不会丢失的情况下,切换非事务表到事务表(alter table tablename type=innodb)。
- 1).MyISAM管理非事务表。它提供高速存储和检索,以及全文搜索能力。如果应用中需要执行大量的SELECT查询,那么MyISAM是更好的选择。
- 2).InnoDB用于事务处理应用程序,具有众多特性,包括ACID事务支持。如果应用中需要执行大量的INSERT或UPDATE操作,则应该使用InnoDB,这样可以提高多用户并发操作的性能。
-
索引类别(B+树索引、全文索引、哈希索引)、索引的原理
-
什么是自适应哈希索引(AHI)
-
为什么要用 B+tree作为MySQL索引的数据结构
-
聚集索引与非聚集索引的区别(点击打开链接)
根本区别
聚集索引和非聚集索引的根本区别是表记录的排列顺序和与索引的排列顺序是否一致。
聚集索引
聚集索引表记录的排列顺序和索引的排列顺序一致,所以查询效率快,只要找到第一个索引值记录,其余就连续性的记录在物理也一样连续存放。聚集索引对应的缺点就是修改慢,因为为了保证表中记录的物理和索引顺序一致,在记录插入的时候,会对数据页重新排序。
非聚集索引
非聚集索引制定了表中记录的逻辑顺序,但是记录的物理和索引不一定一致,两种索引都采用B+树结构,非聚集索引的叶子层并不和实际数据页相重叠,而采用叶子层包含一个指向表中的记录在数据页中的指针方式。非聚集索引层次多,不会造成数据重排。
例子对比两种索引
聚集索引就类似新华字典中的拼音排序索引,都是按顺序进行,例如找到字典中的“爱”,就里面顺序执行找到“癌”。而非聚集索引则类似于笔画排序,索引顺序和物理顺序并不是按顺序存放的。
-
遇到过索引失效的情况没,什么时候可能会出现,如何解决
-
- 如果条件中有or,即使其中有条件带索引也不会使用(这也是为什么尽量少用or的原因)
注意:要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引
2.对于多列索引,不是使用的第一部分,则不会使用索引
3.like查询是以%开头(以%结尾是可以的)
4.如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
5.如果mysql估计使用全表扫描要比使用索引快,则不使用索引
-
limit 20000 加载很慢怎么解决(点击打开链接)
-
如何选择合适的分布式主键方案(点击打开链接)
-
选择合适的数据存储方案
-
常见的几种分布式ID的设计方案(点击打开链接)
-
-
1. UUID
UUID是Universally Unique Identifier的缩写,它是在一定的范围内(从特定的名字空间到全球)唯一的机器生成的标识符,UUID是16字节128位长的数字,通常以36字节的字符串表示,比如:3F2504E0-4F89-11D3-9A0C-0305E82C3301。
UUID经由一定的算法机器生成,为了保证UUID的唯一性,规范定义了包括网卡MAC地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素,以及从这些元素生成UUID的算法。UUID的复杂特性在保证了其唯一性的同时,意味着只能由计算机生成。
优点:
- 本地生成ID,不需要进行远程调用,时延低,性能高。
缺点:
- UUID过长,16字节128位,通常以36长度的字符串表示,很多场景不适用,比如用UUID做数据库索引字段。
- 没有排序,无法保证趋势递增。
-
2. Flicker方案
这个方案是由Flickr团队提出,主要思路采用了MySQL自增长ID的机制(auto_increment + replace into)
-
优点:
- 充分借助数据库的自增ID机制,可靠性高,生成有序的ID。
缺点:
- ID生成性能依赖单台数据库读写性能。
- 依赖数据库,当数据库异常时整个系统不可用。
-
-
常见的数据库优化方案,在你的项目中数据库如何进行优化的(点击打开链接)
2.2、Redis
-
Redis 有哪些数据类型,可参考《Redis常见的5种不同的数据类型详解》String list set sortset hash(点击打开链接)
-
Redis 内部结构(点击打开链接)
-
Redis 使用场景(点击打开链接)
-
Redis 持久化机制,可参考《使用快照和AOF将Redis数据持久化到硬盘中》
-
Redis 集群方案与实现
-
Redis 为什么是单线程的?(点击打开链接)
-
redis利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销
-
总体来说快速的原因如下:
1)绝大部分请求是纯粹的内存操作(非常快速)
2)采用单线程,避免了不必要的上下文切换和竞争条件
3)非阻塞IO
-
缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级(点击打开链接)
-
使用缓存的合理性问题(点击打开链接)
-
Redis常见的回收策略
-
redis 提供 6种数据淘汰策略:
- voltile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-enviction(驱逐):禁止驱逐数据
2.3、消息队列
-
消息的重发补偿解决思路
-
1. 处理失败 指的是MessageListener的onMessage方法里抛出RuntimeException。
2. Message头里有两个相关字段:Redelivered默认为false,redeliveryCounter默认为0。
3. 消息先由broker发送给consumer,consumer调用listener,如果处理失败,本地redeliveryCounter++,给broker一个特定应答,broker端的message里redeliveryCounter++,延迟一点时间继续调用,默认1s。超过6次,则给broker另一个特定应答,broker就直接发送消息到DLQ。
4. 如果失败2次,consumer重启,则broker再推过来的消息里,redeliveryCounter=2,本地只能再重试4次即会进入DLQ。
5. 重试的特定应答发送到broker,broker即会在内存将消息的redelivered设置为true,redeliveryCounter++,但是这两个字段都没有持久化,即没有修改存储中的消息记录。所以broker重启时这两个字段会被重置为默认值
-
消息的幂等性解决思路
-
消息的堆积解决思路
-
自己如何实现消息队列
-
如何保证消息的有序性(点击打开链接)
三、开源框架和容器
3.1、SSM/Servlet
-
Servlet的生命周期(链接)
-
加载—>实例化—>服务—>销毁
-
转发与重定向的区别(链接)
-
一句话,转发是服务器行为,重定向是客户端行为。为什么这样说呢,这就要看两个动作的工作流程:
转发过程:客户浏览器发送http请求----》web服务器接受此请求--》调用内部的一个方法在容器内部完成请求处理和转发动作----》将目标资源发送给客户;在这里,转发的路径必须是同一个web容器下的url,其不能转向到其他的web路径上去,中间传递的是自己的容器内的request。在客户浏览器路径栏显示的仍然是其第一次访问的路径,也就是说客户是感觉不到服务器做了转发的。转发行为是浏览器只做了一次访问请求。
重定向过程:客户浏览器发送http请求----》web服务器接受后发送302状态码响应及对应新的location给客户浏览器--》客户浏览器发现是302响应,则自动再发送一个新的http请求,请求url是新的location地址----》服务器根据此请求寻找资源并发送给客户。在这里location可以重定向到任意URL,既然是浏览器重新发出了请求,则就没有什么request传递的概念了。在客户浏览器路径栏显示的是其重定向的路径,客户可以观察到地址的变化的。重定向行为是浏览器做了至少两次的访问请求的。
-
BeanFactory 和 ApplicationContext 有什么区别(链接)
-
,两者都是通过xml配置文件加载bean,ApplicationContext和BeanFacotry相比,提供了更多的扩展功能,但其主要区别在于后者是延迟加载,如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常;而ApplicationContext则在初始化自身是检验,这样有利于检查所依赖属性是否注入;所以通常情况下我们选择使用ApplicationContext.
-
Spring Bean 的生命周期(点击打开链接)
-
Spring IOC 如何实现(点击打开链接)
-
java程序中的每个业务逻辑至少需要两个或以上的对象来协作完成,通常,每个对象在使用他的合作对象时,自己均要使用像new object() 这样的语法来完成合作对象的申请工作。你会发现:对象间的耦合度高了。而IOC的思想是:Spring容器来实现这些相互依赖对象的创建、协调工作。对象只需要关系业务逻辑本身就可以了。从这方面来说,对象如何得到他的协作对象的责任被反转了(IOC、DI)。
这是我对Spring的IOC的体会。DI其实就是IOC的另外一种说法。DI是由Martin Fowler 在2004年初的一篇论文中首次提出的。他总结:控制的什么被反转了?就是:获得依赖对象的方式反转了。 -
Spring中Bean的作用域,默认的是哪一个
-
1.singleton
singleton,也称单例作用域。在每个 Spring IoC 容器中有且只有一个实例,而且其完整生命周期完全由 Spring 容器管理。对于所有获取该 Bean 的操作 Spring 容器将只返回同一个 Bean。
需要注意的是,若一个 Bean 未指定 scope 属性,默认也为 singleton 。
2.prototype
prototype,也称原型作用域。每次向 Spring IoC 容器请求获取 Bean 都返回一个全新的Bean。相对于 singleton 来说就是不缓存 Bean,每次都是一个根据 Bean 定义创建的全新 Bean。
Web 作用域
1.reqeust
request,表示每个请求需要容器创建一个全新Bean。
在 Spring IoC 容器,即XmlWebApplicationContext 会为每个 HTTP 请求创建一个全新的 RequestPrecessor 对象。当请求结束后,该对象的生命周期即告结束。当同时有 10 个 HTTP 请求进来的时候,容器会分别针对这 10 个请求创建 10 个全新的 RequestPrecessor 实例,且他们相互之间互不干扰,从不是很严格的意义上说,request 可以看做 prototype 的一种特例,除了场景更加具体之外,语意上差不多。
2.session
session,表示每个会话需要容器创建一个全新 Bean。比如对于每个用户一般会有一个会话,该用户的用户信息需要存储到会话中,此时可以将该 Bean 配置为 web 作用域。
3.globalSession
globalSession,类似于session 作用域,只是其用于 portlet 环境的 web 应用。如果在非portlet 环境将视为 session 作用域。
-
说说 Spring AOP、Spring AOP 实现原理(点击打开链接)
-
AOP技术则恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。AOP代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向方面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。
使用“横切”技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处都基本相似。比如权限认证、日志、事务处理。Aop 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。正如Avanade公司的高级方案构架师Adam Magee所说,AOP的核心思想就是“将应用程序中的商业逻辑同对其提供支持的通用服务进行分离。”
实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码
-
动态代理(CGLib 与 JDK)、优缺点、性能对比、如何选择(点击打开链接)
-
JDK动态代理是面向接口,在创建代理实现类时比CGLib要快,创建代理速度快。
CGLib动态代理是通过字节码底层继承要代理类来实现(如果被代理类被final关键字所修饰,那么抱歉会失败),在创建代理这一块没有JDK动态代理快,但是运行速度比JDK动态代理要快。
使用注意:
如果要被代理的对象是个实现类,那么Spring会使用JDK动态代理来完成操作(Spirng默认采用JDK动态代理实现机制)
如果要被代理的对象不是个实现类那么,Spring会强制使用CGLib来实现动态代理。
-
Spring事务机制是一种典型的策略模式,PlatformTransactionManager代表事务管理接口,但它并不知道到底如何管理事务,它只要求事务管理提供开始事务getTransaction(),提交事务commit()和回滚事务rollback()这三个方法,但具体如何实现则交给其实现类完成。编程人员只需要在配置文件中根据具体需要使用的事务类型做配置,Spring底层就自动会使用具体的事务实现类进行事务操作,而对于程序员来说,完全不需要关心底层过程,只需要面向PlatformTransactionManager接口进行编程即可。PlatformTransactionManager接口中提供了如下方法:getTransaction(..), commit(); rollback(); 这些都是与平台无关的事务操作。
-
1、TransactionDefinition接口中定义五个隔离级别(isolation):
说明:
脏读:一个事务读取了另一个事务改写但还未提交的数据,如果这些数据被回滚,则读到的数据是无效的。
不可重复读:在同一事务中,多次读取到同一数据的结果有所不同。也就是说,后续读取可以读到另一个事务已经提交的更新数据。
幻读:一个事务读取了几行记录后,另一个事务插入一些记录,幻读就发生了,再查询的时候,第一个事务就会发现有些原来没有的记录。2. 在TransactionDefinition接口中定义了七个事务传播行为(propagationBehavior):
3、TransactionDefinition接口中定义了事务超时
所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒。
默认设置为底层事务系统的超时值,如果底层数据库事务系统没有设置超时值,那么就是none,没有超时限制。
4、事务只读属性
只读事务用于客户代码只读但不修改数据的情形,只读事务用于特定情景下的优化,比如使用Hibernate的时候,默认为读写事务。 -
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
大多数的数据库默认隔离级别为 Read Commited,比如 SqlServer、Oracle
少数数据库默认隔离级别为:Repeatable Read 比如: MySQL InnoDB
-
Spring 事务底层原理(点击打开链接)
-
Spring事务失效(事务嵌套),JDK动态代理给Spring事务埋下的坑,可参考《JDK动态代理给Spring事务埋下的坑!》
-
如何自定义注解实现功能
-
Spring MVC 运行流程(点击打开链接)
-
在整个Spring MVC框架中,DispatcherServlet处于核心位置,它负责协调和组织不同组件完成请求处理并返回响应的工作。具体流程为:
1)客户端发送http请求,web应用服务器接收到这个请求,如果匹配DispatcherServlet的映射路径(在web.xml中配置),web容器将请求转交给DispatcherServlet处理;
2)DispatcherServlet根据请求的信息及HandlerMapping的配置找到处理该请求的Controller;
3)Controller完成业务逻辑处理后,返回一个ModelAndView给DispatcherServlet;
4)DispatcherServlet借由ViewResolver完成ModelAndView中逻辑视图名到真实视图对象View的解析工作;
5)DispatcherServlet根据ModelAndView中的数据模型对View对象进行视图渲染,最终客户端得到的响应消息可能是一个普通的html页面,也可能是一个xml或json串,甚至是一张图片或一个PDF文档等不同的媒体形式。 -
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
文字叙述
1 用户浏览器发起请求
2 前端控制器DispatcherServlet首先会去请求Handler(也就是Controller),
怎么请求Handler----通过查找HandlerMapping(里面有xml或者注解方式配置的Handler映射信息信息)来匹配用户请求url对应的Handler,
将查找到的请求信息,放入到执行链HandlerExecutionChain中,然后在放入该url对应的拦截器信息。
然后将执行链HandlerExecutionChain返回给前端控制器DispatcherServlet
3 前端控制器DispatcherServlet通过请求到的handler,再请求处理器适配器HandlerAdapter去执行handler,
::: 执行之前需要先请求执行链中的拦截器的preHandle方法进行某些请求校验等。
4 处理器适配器执行handler后返回给前端控制器DispatcherServlet一个ModelAndView(里面放有视图信息,模型数据信息)
::: 执行拦截器的postHandle方法
5 前端控制器DispatcherServlet请求视图解析器解析视图,根据逻辑名(xxxx/xxxx/xxxx.jsp)解析成真正的视图view(jsp,ftl等)
6 视图解析器解析完成后,返回给前端控制器DispatcherServlet一个View
7 前端控制器DispatcherServlet进行视图渲染,将模型数据填充到request中
8 响应用户请求,展示jsp等视图信息。
-
Spring MVC 启动流程
-
Spring 的单例实现原理(点击打开链接)
-
我们来看看Spring中的单例实现,当我们试图从Spring容器中取得某个类的实例时,默认情况下,Spring会才用单例模式进行创建。
<bean id="date" class="java.util.Date"/> <bean id="date" class="java.util.Date" scope="singleton"/> (仅为Spring2.0支持) <bean id="date" class="java.util.Date" singleton="true"/> 以上三种创建对象的方式是完全相同的,容器都会向客户返回Date类的单例引用。那么如果我不想使用默认的单例模式,每次请求我都希望获得一个新的对象怎么办呢?很简单,将scope属性值设置为prototype(原型)就可以了<bean id="date" class="java.util.Date" scope="prototype"/> 通过以上配置信息,Spring就会每次给客户端返回一个新的对象实例。
那么Spring对单例的底层实现,到底是饿汉式单例还是懒汉式单例呢?呵呵,都不是。Spring框架对单例的支持是采用单例注册表的方式进行实现的,源码如下: -
刚才的源码中,大家真正要记住的是Spring对bean实例的创建是采用单例注册表的方式进行实现的,而这个注册表的缓存是HashMap对象,如果配置文件中的配置信息不要求使用单例,Spring会采用新建实例的方式返回对象实例
-
Spring 框架中用到了哪些设计模式
-
1.工厂模式,这个很明显,在各种BeanFactory以及ApplicationContext创建中都用到了;
2.模版模式,这个也很明显,在各种BeanFactory以及ApplicationContext实现中也都用到了;
3.代理模式,在Aop实现中用到了JDK的动态代理;
4.策略模式,第一个地方,加载资源文件的方式,使用了不同的方法,比如:ClassPathResourece,FileSystemResource,ServletContextResource,UrlResource但他们都有共同的借口Resource;第二个地方就是在Aop的实现中,采用了两种不同的方式,JDK动态代理和CGLIB代理;
5.单例模式,这个比如在创建bean的时候
-
Spring 其他产品(Srping Boot、Spring Cloud、Spring Secuirity、Spring Data、Spring AMQP 等)
-
有没有用到Spring Boot,Spring Boot的认识、原理
-
它的核心功能是由@EnableAutoConfiguration这个注解提供的/Spring boot出现之后,得益于“习惯优于配置”这个理念,再也没有繁琐的配置、难以集成的内容(大多数流行第三方技术都被集成在内)。
-
Hibernate的原理(点击打开链接)
-
1.通过Configuration config = new Configuration().configure();//读取并解析hibernate.cfg.xml配置文件
2.由hibernate.cfg.xml中的<mapping resource="com/xx/User.hbm.xml"/>读取并解析映射信息
3.通过SessionFactory sf = config.buildSessionFactory();//创建SessionFactory
4.Session session = sf.openSession();//打开Sesssion
5.Transaction tx = session.beginTransaction();//创建并启动事务Transation
6.persistent operate操作数据,持久化操作
7.tx.commit();//提交事务
8.关闭Session
9.关闭SesstionFactory -
1. 面向对象设计的软件内部运行过程可以理解成就是在不断创建各种新对象、建立对象之间的关系,调用对象的方法来改变各个对象的状态和对象消亡的过程,不管程序运行的过程和操作怎么样,本质上都是要得到一个结果,程序上一个时刻和下一个时刻的运行结果的差异就表现在内存中的对象状态发生了变化。
2.为了在关机和内存空间不够的状况下,保持程序的运行状态,需要将内存中的对象状态保存到持久化设备和从持久化设备中恢复出对象的状态,通常都是保存到关系数据库来保存大量对象信息。从Java程序的运行功能上来讲,保存对象状态的功能相比系统运行的其他功能来说,应该是一个很不起眼的附属功能,java采用jdbc来实现这个功能,这个不起眼的功能却要编写大量的代码,而做的事情仅仅是保存对象和恢复对象,并且那些大量的jdbc代码并没有什么技术含量,基本上是采用一套例行公事的标准代码模板来编写,是一种苦活和重复性的工作。
3.通过数据库保存java程序运行时产生的对象和恢复对象,其实就是实现了java对象与关系数据库记录的映射关系,称为ORM(即Object Relation Mapping),人们可以通过封装JDBC代码来实现了这种功能,封装出来的产品称之为ORM框架,Hibernate就是其中的一种流行ORM框架。使用Hibernate框架,不用写JDBC代码,仅仅是调用一个save方法,就可以将对象保存到关系数据库中,仅仅是调用一个get方法,就可以从数据库中加载出一个对象。
4.使用Hibernate的基本流程是:配置Configuration对象、产生SessionFactory、创建session对象,启动事务,完成CRUD操作,提交事务,关闭session。
5.使用Hibernate时,先要配置hibernate.cfg.xml文件,其中配置数据库连接信息和方言等,还要为每个实体配置相应的hbm.xml文件,hibernate.cfg.xml文件中需要登记每个hbm.xml文件。
6.在应用Hibernate时,重点要了解Session的缓存原理,级联,延迟加载和hql查询。
-
Hibernate与 MyBatis的比较(点击打开链接)、
-
springmvc与struts2比较(点击打开链接)
struts2是类级别的拦截, 一个类对应一个request上下文,
springmvc是方法级别的拦截,一个方法对应一个request上下文,而方法同时又跟一个url对应
所以说从架构本身上 spring3 mvc就容易实现restful url
而struts2的架构实现起来要费劲
因为struts2 action的一个方法可以对应一个url
而其类属性却被所有方法共享,这也就无法用注解或其他方式标识其所属方法了
===================================
spring3mvc的方法之间基本上独立的,独享request response数据
请求数据通过参数获取,处理结果通过ModelMap交回给框架
方法之间不共享变量
而struts2搞的就比较乱,虽然方法之间也是独立的,但其所有Action变量是共享的
这不会影响程序运行,却给我们编码 读程序时带来麻烦
====================================
spring3 mvc的验证也是一个亮点,支持JSR303
处理ajax的请求更是方便 只需一个注解@ResponseBody ,然后直接返回响应文本即可 -
可参考《为什么会有Spring》
-
可参考《为什么会有Spring AOP》
3.2、Netty
-
为什么选择 Netty
-
说说业务中,Netty 的使用场景
-
原生的 NIO 在 JDK 1.7 版本存在 epoll bug
-
什么是TCP 粘包/拆包
-
TCP粘包/拆包的解决办法
-
Netty 线程模型
-
说说 Netty 的零拷贝
-
Netty 内部执行流程
-
Netty 重连实现
3.3、Tomcat
-
Tomcat的基础架构(Server、Service、Connector、Container)
-
Tomcat如何加载Servlet的
-
Pipeline-Valve机制
-
可参考:《四张图带你了解Tomcat系统架构!》
四、分布式
4.1、Nginx
-
请解释什么是C10K问题或者知道什么是C10K问题吗?(点击打开链接)
-
正向代理和反向代理.
-
Nginx几种常见的负载均衡策略
-
Nginx服务器上的Master和Worker进程分别是什么
-
使用“反向代理服务器”的优点是什么?
4.2、分布式其他
-
谈谈业务中使用分布式的场景
-
Session 分布式方案
-
Session 分布式处理
-
分布式锁的应用场景、分布式锁的产生原因、基本概念
-
分布是锁的常见解决方案
-
分布式事务的常见解决方案
-
集群与负载均衡的算法与实现
-
说说分库与分表设计,可参考《数据库分库分表策略的具体实现方案》
-
分库与分表带来的分布式困境与应对之策
4.3、Dubbo
-
什么是Dubbo,可参考《Dubbo入门》
-
什么是RPC、如何实现RPC、RPC 的实现原理,可参考《基于HTTP的RPC实现》
-
Dubbo中的SPI是什么概念
-
Dubbo的基本原理、执行流程
五、微服务
5.1、安全问题
-
如何防范常见的Web攻击、如何方式SQL注入
-
服务端通信安全攻防
-
HTTPS原理剖析、降级攻击、HTTP与HTTPS的对比
5.2、性能优化
-
性能指标有哪些
-
如何发现性能瓶颈
-
性能调优的常见手段
-
说说你在项目中如何进行性能调优
六、其他
6.1、设计能力
-
说说你在项目中使用过的UML图
-
你如何考虑组件化、服务化、系统拆分(点击打开链接)
-
秒杀场景如何设计
6.2、业务工程
-
说说你的开发流程、如何进行自动化部署的
-
你和团队是如何沟通的
-
你如何进行代码评审
-
说说你对技术与业务的理解
-
说说你在项目中遇到感觉最难Bug,是如何解决的
-
介绍一下工作中的一个你认为最有价值的项目,以及在这个过程中的角色、解决的问题、你觉得你们项目还有哪些不足的地方
6.3、软实力
-
说说你的优缺点、亮点
-
说说你最近在看什么书、什么博客、在研究什么新技术、再看那些开源项目的源代码
-
说说你觉得最有意义的技术书籍
-
工作之余做什么事情、平时是如何学习的,怎样提升自己的能力
-
说说个人发展方向方面的思考
-
说说你认为的服务端开发工程师应该具备哪些能力
-
说说你认为的架构师是什么样的,架构师主要做什么
-
如何看待加班的问题
-
市场经济机遇与挑战并存,企业每临重要关头,为争取主动抢占先机而组织加班,作为员工应该充分理解,主动请战保质保量加入突击。当然作为企业应该努力避免此种状况发生,突击必然紧张 紧张产生忙乱,连续不断地加班容易引起员工心理疲劳懈怠情绪,反而影响质量与效率。企业必须在管理上下功夫,努力做到高效八小时,达到紧凑有序 均衡生产的目的。