一、基础篇
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(没有缓存)
-
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得到它里面的查询结果,并显示出来。
-
i++和++i的区别,及其线程安全问题
i++和++i都是i=i+1的意思,但是过程有些许区别:
i++:先赋值再自加。(例如:i=1;a=1+i++;结果为a=1+1=2,语句执行完后i再进行自加为2)
++i:先自加再赋值。(例如:i=1;a=1+++i;结果为a=1+(1+1)=3,i先自加为2再进行运算)
但是在单独使用时没有区别:如for(int i=0;i<10;i++){ }和for(int i=0;i<10;++i) { }没有区别。
i++和++i的线程安全分为两种情况:
1、如果i是局部变量(在方法里定义的),那么是线程安全的。因为局部变量是线程私有的,别的线程访问不到,其实也可以说没有线程安不安全之说,因为别的线程对他造不成影响。
2、如果i是全局变量(类的成员变量),那么是线程不安全的。因为如果是全局变量的话,同一进程中的不同线程都有可能访问到。
如果有大量线程同时执行i++操作,i变量的副本拷贝到每个线程的线程栈,当同时有两个线程栈以上的线程读取线程变量,假如此时是1的话,那么同时执行i++操作,再写入到全局变量,最后两个线程执行完,i会等于3而不会是2,所以,出现不安全性。
-
1、先是寄存器从内存中读取i,
2、然后在寄存器中进行+1操作,
3、最后+1后的值回传内存,
因为没有保证原子操作,万一出现3步没有执行完而线程的CPU时间片已经用完,导致操作失败,所以并不安全。
这里有两种方法可以保证线程安全:
1.使用synchronize进行加锁操作,但是影响效率
2.使用CAS乐观锁技术,CAS是一种轻量级锁,是通过CPU硬件指令来实现的,通过总线加锁的方式,指令能保证CPU寄存器和内存交换数据是原子操作,具体使用CAS下的原子操作类AtomicInteger
-
equals与==的区别
1.==是判断两个变量或实例是不是指向同一个内存空间
equals是判断两个变量或实例所指向的内存空间的值是不是相同
2.==是指对内存地址进行比较
equals()是对字符串的内容进行比较
3.==指引用是否相同
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 的工作原理及代码实现,如何统计所有的元素个数
-
1.3、进程和线程
-
线程和进程的概念、并行和并发的概念
- java中的Thread类定义了多线程,通过多线程可以实现并发或并行。
- 在CPU比较繁忙,资源不足的时候(开启了很多进程),操作系统只为一个含有多线程的进程分配仅有的CPU资源,这些线程就会为自己尽量多抢时间片,这就是通过多线程实现并发,线程之间会竞争CPU资源争取执行机会。
- 在CPU资源比较充足的时候,一个进程内的多线程,可以被分配到不同的CPU资源,这就是通过多线程实现并行。
- 至于多线程实现的是并发还是并行?上面所说,所写多线程可能被分配到一个CPU内核中执行,也可能被分配到不同CPU执行,分配过程是操作系统所为,不可人为控制。所有,如果有人问我我所写的多线程是并发还是并行的?我会说,都有可能。
- 不管并发还是并行,都提高了程序对CPU资源的利用率,最大限度地利用CPU资源。
-
进程和线程
进程是一个程序的实例。每个进程都有自己的虚拟地址空间和控制线程,线程是操作系统调度器(Schduler)分配处理器时间的基础单元。
并发:
讲并发之前,要先看一张图:
- Concurrency,是并发的意思。并发的实质是一个物理CPU(也可以多个物理CPU) 在若干道程序(或线程)之间多路复用,并发性是对有限物理资源强制行使多用户共享以提高效率。
- 微观角度:所有的并发处理都有排队等候,唤醒,执行等这样的步骤,在微观上他们都是序列被处理的,如果是同一时刻到达的请求(或线程)也会根据优先级的不同,而先后进入队列排队等候执行。
- 宏观角度:多个几乎同时到达的请求(或线程)在宏观上看就像是同时在被处理。
- 通俗点讲,并发就是只有一个CPU资源,程序(或线程)之间要竞争得到执行机会。图中的第一个阶段,在A执行的过程中B,C不会执行,因为这段时间内这个CPU资源被A竞争到了,同理,第二个阶段只有B在执行,第三个阶段只有C在执行。其实,并发过程中,A,B,C并不是同时在进行的(微观角度)。但又是同时进行的(宏观角度)。
并行:
同样,在讲并行之前,要先看一张图:
- Parallelism,即并行,指两个或两个以上事件(或线程)在同一时刻发生,是真正意义上的不同事件或线程在同一时刻,在不同CPU资源呢上(多核),同时执行。
- 并行,不存在像并发那样竞争,等待的概念。
- 图中,A,B,C都在同时运行(微观,宏观)。
通过多线程实现并发,并行:
-
创建线程的方式及实现
-
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错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。 -
2、软引用(SoftReference)
如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。 -
3.弱引用(WeakReference)
如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 -
4.虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃 圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解
被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
特别注意,在世纪程序设计中一般很少使用弱引用与虚引用,使用软用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。 -
强引用:
String str = “abc”;
list.add(str);
软引用:
如果弱引用对象回收完之后,内存还是报警,继续回收软引用对象
弱引用:
如果虚引用对象回收完之后,内存还是报警,继续回收弱引用对象
虚引用:
虚拟机的内存不够使用,开始报警,这时候垃圾回收机制开始执行System.gc(); String s = “abc”;如果没有对象回收了, 就回收没虚引用的对象 -
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到页面加载的发生了什么?可参考《代码》