百味皆苦 java后端开发攻城狮

八股文-基础

2019-04-06
百味皆苦

面试宝典

Java特性

对java平台的理解?

一次编译、到处运行”说的是Java语言跨平台的特性,Java的跨平台特性与Java虚拟机的存在密不可分,可在不同的环境中运行。比如说Windows平台和Linux平台都有相应的JDK,安装好JDK后也就有了Java语言的运行环境。其实Java语言本身与其他的编程语言没有特别大的差异,并不是说Java语言可以跨平台,而是在不同的平台都有可以让Java语言运行的环境而已,所以才有了Java一次编译,到处运行这样的效果。

程序从源代码到运行的三个阶段:编码——编译——运行——调试。Java在编译阶段则体现了跨平台的特点。编译过程大概是这样的:首先是将Java源代码转化成.CLASS文件字节码,这是第一次编译。.class文件就是可以到处运行的文件。然后Java字节码会被转化为目标机器代码,这是是由JVM来执行的,即Java的第二次编译。

需要强调的一点是,java并不是编译机制,而是解释机制。Java字节码的设计充分考虑了JIT这一即时编译方式,可以将字节码直接转化成高性能的本地机器码,这同样是虚拟机的一个构成部分
java的特性是什么?Java是解析运行的吗?

面向对象(封装,继承,多态)
平台无关性(JVM运行.class文件)
语言(泛型,Lambda)
类库(集合,并发,网络,IO/NIO)
JRE(Java运行环境,JVM,类库)
JDK(Java开发工具,包括JRE,javac,诊断工具)

Java不是解析运行的,Java源代码经过Javac编译成.class文件;.class文件经JVM解析或编译运行
解析:.class文件经过JVM内嵌的解析器解析执行。
编译:存在JIT编译器(Just In Time Compile 即时编译器)把经常运行的代码作为"热点代码"编译与本地平台相关的机器码,并进行各种层次的优化。
AOT编译器: Java 9提供的直接将所有代码编译成机器码执行。
1.什么是 Java 虚拟机?为什么 Java 被称作是“平台无关的编程语言”?

java 虚拟机是一个可以执行 Java 字节码的虚拟机进程。Java 源文件被编译成能被 Java 虚拟机执行的字节码文件。
Java 被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java 虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。
2.JDK 和 JRE 的区别是什么?

Java 运行时环境(JRE)是将要执行 Java 程序的 Java 虚拟机。它同时也包含了执行 applet 需要的浏览器插件。Java 开发工具包(JDK)是完整的 Java 软件开发包,包含了 JRE,编译器和其他的工具(比如:JavaDoc,Java 调试器),可以让开发者开发、编译、执行 Java 应用程序。

OOP面向对象

封装继承多态抽象

  • OOP面向对象编程
Java 是一个支持并发、基于类和面向对象的计算机编程语言。下面列出了面向对象软件开发的优点:

代码开发模块化,更易维护和修改。 
代码复用。 
增强代码的可靠性和灵活性。 
增加代码的可理解性。 
面向对象编程有很多重要的特性,比如:封装,继承,多态和抽象
使用封装的一些好处:

通过隐藏对象的属性来保护对象内部的状态。 
提高了代码的可用性和可维护性,因为对象的行为可以被单独的改变或者是扩展。 
禁止对象之间的不良交互提高模块化。
抽象和封装的不同点:

抽象和封装是互补的概念。一方面,抽象关注对象的行为。另一方面,封装关注对象行为的细节。一般是通过隐藏对象内部状态信息做到封装,因此,封装可以看成是用来提供抽象的一种策略。
new一个对象的过程和clone一个对象的区别?

new 操作符的本意是分配内存。程序执行到 new 操作符时,首先去看 new 操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。
clone 在第一步是和 new 相似的,都是分配内存,调用 clone 方法时,分配的内存和原对象(即调用 clone 方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域,填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。
为什么java是单继承,但却是多实现的呢?

java是单继承是因为一个类只能有一个直接父类;如果类A有一个print方法,类B也有一个print方法,类C继承了A和B,那么当调用new C().print()方法时就不知道是调用的哪一个类的了

但是对于接口的实现,一个类却能够实现多个接口,接口是用来扩展对象的功能的,即便两个接口中存在相同的抽象函数。但在实现时,我们只能在当前类中实现一个这样的函数,所以不论是实现的哪个,另外一个同名的也就无所谓了。于是,java就是多实现的了。

对象结构

对象的内存结构是怎样的?

对象由三部分组成,对象头,对象实例,对齐填充。
其中对象头一般是十六个字节,包括两部分,第一部分有哈希码,锁状态标志,线程持有的锁,偏向线程id,gc分代年龄等。第二部分是类型指针,也就是对象指向它的类元数据指针,可以理解,对象指向它的类。
对象实例就是对象存储的真正有效信息,也是程序中定义各种类型的字段包括父类继承的和子类定义的,这部分的存储顺序会被虚拟机和代码中定义的顺序影响(这个被虚拟机影响是不是就是重排序??如果是的话,volatile定义的变量不会被重排序应该就是这里不会受虚拟机影响吧??)。
第三部分对齐填充只是一个类似占位符的作用,因为内存的使用都会被填充为八字节的倍数。

java问题

标识符

如何判断一个标识符时候合法?

标识符是以字母开头的字母数字序列:
数字是指0~9,字母指大小写英文字母、下划线(_)和美元符号($),也可以是Unicode字符集中的字符,如汉字;
字母、数字等字符的任意组合,不能包含+、- *等字符;
不能使用关键字;大小写敏感

char和string

字符型常量和字符串常量的区别?

形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的若干个字符
含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)
占内存大小: 字符常量只占2个字节; 字符串常量占若干个字节(至少一个字符结束标志) 
(注意: char在Java中占两个字节)
17、是否可以继承String类? 
答:String 类是final类,不可以被继承。简单的来说:String 类中使用 final 关键字字符数组保存字符串,private final char value[],所以 String 对象是不可变的。

17、字符串拼接原理:
答:运行时, 两个字符串str1, str2的拼接首先会调用 String.valueOf(obj),这个Obj为str1,而String.valueOf(Obj)中的实现是return obj == null ? “null” : obj.toString(), 然后产生StringBuilder,调用的StringBuilder(str1)构造方法,把StringBuilder初始化,长度str1.length()+16,并且调用append(str1)! 接下来调用StringBuilder.append(str2), 把第二个字符串拼接进去, 然后调用StringBuilder.toString返回结果!
19、String和StringBuilder、StringBuffer的区别? 默认容积是多少?

答String是只读字符串,也就意味着String引用的字符串内容是不能被改变的。StringBuffer/StringBuilder类表示的字符串对象可以直接进行修改。StringBuilder是Java 5中引入的,它和StringBuffer的方法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方面都没有被synchronized修饰,因此它的效率也比StringBuffer要高。

构建时初始字符串长度加 16(这意味着,如果没有构建对象时输入最初的字符串,那么初始值就是 16)。我们如果确定拼接会发生非常多次,而且大概是可预计的,那么就可以指定合适的大小,避免很多次扩容的开销。扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行 arraycopy。
Java9对string的改动有哪些?

在 Java 9 中,我们引入了 Compact Strings 的设计,对字符串进行了大刀阔斧的改进。将数据存储方式从 char 数组,改变为一个 byte 数组加上一个标识编码的所谓 coder,并且将相关字符串操作类都进行了修改。另外,所有相关的 Intrinsic 之类也都进行了重写,以保证没有任何性能损失。
22、char 型变量中能不能存贮一个中文汉字,为什么? 

答:char类型可以存储一个中文汉字,因为Java中使用的编码是Unicode(不选择任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法),一个char类型占2个字节(16比特),所以放一个中文是没问题的。
如何将byte 转为String

可以使用String 接收byte[] 参数的构造器来进行转换,需要注意的点是要使用的正确的编
码,否则会使用平台默认编码,这个编码可能跟原来的编码相同,也可能不同。
什么是字符串常量池?

[java常量池](https://blog.csdn.net/qq_36925536/article/details/100928298)
Java中的字符串常量池(String Pool)是存储在Java堆内存中的字符串池。我们可以使用new运算符创建String对象,也可以用双引号(”“)创建字串对象。之所以有字符串常量池,是因为String在Java中是不可变(immutable)的,它是String interning概念的实现。字符串常量池也是亨元模式(Flyweight)的实例。
当我们使用双引号创建一个字符串时,首先在字符串常量池中查找是否有相同值的字符串,如果发现则返回其引用,否则它会在池中创建一个新的字符串,然后返回新字符串的引用。
如果使用new运算符创建字符串,则会强制String类在堆空间中创建一个新的String对象。我们可以使用intern()方法将其放入字符串常量池或从字符串常量池中查找具有相同的值字符串对象并返回其引用
你对String 对象的intern()熟悉么?

intern()方法会首先从常量池中查找是否存在该常量值,如果常量池中不存在则现在常量池中创建,如果已经存在则直接返回。
比如
String s1=”aa”;
String s2=s1.intern();
System.out.print(s1==s2);//返回true
面试题----考自《深入理解Java虚拟机》
public class StringPool58Demo {
public static void main(String[] args) {

String str1 = new StringBuilder("58").append("tongcheng").toString();
System.out.println(str1);
System.out.println(str1.intern());
System.out.println(str1 == str1.intern());

System.out.println("------------");

String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2);
System.out.println(str2.intern());
System.out.println(str2 == str2.intern());
}
}

第一个为true
第二个为false
为什么?按照代码结果,Java字符串答案为false必然是两个不同的Java,那另外一个Java字符串如何加载进来的?
有一个初始化的Java字符串(jdk自带的),在加载sun.misc.Version这个类的时候进入常量池
System.initializeSystemClass()--->sun.misc.Version.init()
类加载器和rt.jar,根加载器提前部署加载rt.jar

变量

成员变量与局部变量的区别有哪些?

从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;
成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;
成员变量和局部变量都能被 final 所修饰。
从变量在内存中的存储方式来看:非静态的成员变量随对象存在于堆内存,它随着对象的创建而存在;局部变量则存在于栈内存,随着方法的调用而自动消失。
成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
阐述静态变量和实例变量的区别?

不管创建多少个对象,静态变量在内存中有且仅有一个;实例变量必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。

引用

对象实体与对象引用有何不同?

对象实例在堆内存中,对象引用存放在栈内存中(对象引用指向对象实例);
一个对象引用可以指向0个或1个对象
一个对象可以有n个引用指向它
强引用、软引用、弱引用、幻象引用有什么区别?具体使用场景是什么?

不同的引用类型,主要体现的是对象不同的可达性(reachable)状态和对垃圾收集的影响。

强引用(“Strong” Reference),就是我们最常见的普通对象引用,我们平常典型编码Object obj = new Object()中的obj就是强引用。通过关键字new创建的对象所关联的引用就是强引用。只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,当然具体回收时机还是要看垃圾收集策略。

软引用(SoftReference),是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。图片缓存框架中,“内存缓存”中的图片是以这种引用来保存,使得JVM在发生OOM之前,可以回收这部分缓存

弱引用(WeakReference)并不能使对象豁免垃圾收集,仅仅是提供一种访问在弱引用状态下对象的途径。这就可以用来构建一种没有特定约束的关系,比如,维护一种非强制性的映射关系,如果试图获取时对象还在,就使用它,否则重现实例化。它同样是很多缓存实现的选择。在静态内部类中,经常会使用虚引用。例如,一个类发送网络请求,承担callback的静态内部类,则常以虚引用的方式来保存外部类(宿主类)的引用,当外部类需要被JVM回收时,不会因为网络请求没有及时回来,导致外部类不能被回收,引起内存泄漏

对于幻象引用,有时候也翻译成虚引用,你不能通过它访问对象。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制,比如,通常用来做所谓的 Post-Mortem 清理机制,也有人利用幻象引用监控对象的创建和销毁。

强引用就像大老婆,关系很稳固。
软引用就像二老婆,随时有失宠的可能,但也有扶正的可能。
弱引用就像情人,关系不稳定,可能跟别人跑了。
幻像引用就是梦中情人,只在梦里出现过。

对象相等

对象的相等与指向他们的引用相等,两者有什么不同?

对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。
== 与 equals的区别?

== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。

equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;
使用equals时如何避免空指针问题?

Object的equals方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。
不过更推荐使用 java.util.Objects#equals
Objects.equals(null,"SnailClimb");// false
16、两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对? 
答:不对,如果两个对象x和y满足x.equals(y) == true,它们的哈希码(hash code)应当相同。Java对于eqauls方法和hashCode方法是这样规定的:(1)如果两个对象相同(equals方法返回true),那么它们的hashCode值一定要相同;(2)如果两个对象的hashCode相同,它们并不一定相同。

基本数据类型

整形包装类型值如何比较?浮点类型数据如何比较?

所有整型包装类对象值的比较必须使用equals方法。
Integer x = 3;//自动装箱,数值在-128~127之间,会把x对象缓存起来
Integer y = 3;//和x数值一样,从缓存中取出x的引用赋值给y,所以x和y引用相同
System.out.println(x == y);// true,引用相同

Integer a = new Integer(3);
Integer b = new Integer(3);
System.out.println(a == b);//false,两个独立对象
System.out.println(a.equals(b));//true,对象的内容相同

浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断。
float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999964
System.out.println(a == b);// false

使用 BigDecimal 来定义浮点数的值,再进行浮点数的运算操作。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);// 0.1
BigDecimal y = b.subtract(c);// 0.1
System.out.println(x.equals(y));// true 
a=a+b 与a+=b 有什么区别吗?

+=操作符会进行隐式自动类型转换,此处a+=b 隐式的将加操作的结果类型强制转换为持有
结果的类型,而a=a+b 则不会自动进行类型转换。如:
byte a = 127;
byte b = 127;
b = a + b; // error : cannot convert from int to byte
b += a; // ok

其实无论a+b 的值为多少,编译器都会报错,因为a+b
操作会将a、b 提升为int 类型,所以将int 类型赋值给byte 就会编译出错
5.Java 支持的数据类型有哪些?什么是自动拆装箱?

Java 语言支持的 8 中基本数据类型是: 
byte 
short 
int 
long 
float 
double 
boolean 
char 
自动装箱是 Java 编译器在基本数据类型和对应的对象包装类型之间做的一个转化。比如:把 int 转化成 Integer,double 转化成 double,等等。反之就是自动拆箱。 
4、float f=3.4;是否正确?

答:不正确。3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成float f =3.4F;。
11、switch 是否能作用在byte 上,是否能作用在long 上,是否能作用在String上? 
答:在Java 5以前,switch(expr)中,expr只能是byte、short、char、int。从Java 5开始,Java中引入了枚举类型,expr也可以是enum类型,从Java 7开始,expr还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。
12、用最有效率的方法计算2乘以8? 
答: 
2 << 3(左移3位相当于乘以2的3次方,右移3位相当于除以2的3次方)。
Java 的八种基本数据类型,每个占多少个字节?

bit --位:位是计算机中存储数据的最小单位,指二进制数中的一个位数,其值为“0”或“1”。
byte --字节:字节是计算机存储容量的基本单位,一个字节由8位二进制数组成。在计算机内部,一个字节可以表示一个数据,也可以表示一个英文字母,两个字节可以表示一个汉字。
1Byte=8bit  (1B=8bit)

byte     8bit=1byte
short   16bit=2byte
int     32bit=4byte
long    64bit=8byte
float   32bit=4byte
double   64bit=8byte
boolean 1bit
char     16bit=2byte
short s1 = 1; s1 = s1 + 1;有什么错?

short s1=1;这一句没有错误,编译器自动把1从整形处理为short
s1=s1+1; 右侧的表达式会返回一个int类型的整数,再把这个int类型的整数赋值给short类型的s1的时候,就会出现类型强制转换错误

short s1= 1; s1 += 1; 该段代码是否有错,有的话怎么改?
+=操作符会自动对右边的表达式结果强转匹配左边的数据类型,所以没错。
Math.round(11.5)等于多少? Math.round(-11.5)等于多少?

Math.round 的意思是+0.5 取整数
所以 Math.round(11.5) 即 11.5+0.5 = 12
Math.round(-11.5) 即 -11.5+0.5 = -11
java 当中使用什么类型表示价格比较好?

如果不是特别关心内存和性能的话,使用BigDecimal,否则使用预定义精度的double 类型。
可以将int 强转为byte 类型么?会产生什么问题?

我们可以做强制转换,但是Java 中int 是32 位的而byte 是8 位的,所以,如果强制转化int
类型的高24 位将会被丢弃,byte 类型的范围是从-128 到128
数据类型之间的转换?

字符串如何转基本数据类型:调用基本数据类型对应的包装类中的方法 parseXXX(String)或valueOf(String)即可返回相应基本类型
基本数据类型如何转字符串:一种方法是将基本数据类型与空字符串(“”)连接(+)即可获得其所对应的字符串;另一种方法是调用 String类中的 valueOf()方法返回相应字符串。

序列化

Java序列化中如果有些字段不想进行序列化,怎么办?

对于不想进行序列化的变量,使用transient关键字修饰。
transient关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。
什么是java序列化,如何实现java序列化?

序列化是指把一个java对象,通过某种介质进行传输,比如socket输入输出流,或者保存在一个文件里
实现java序列化的手段是让该类实现Serializable接口,这个接口是一个标识性接口,没有任何方法,仅仅用于表示该类可以序列化
显示设置serialVersionUID

静态

3.”static”关键字是什么意思?Java 中是否可以覆盖(override)一个static方法?

“static”关键字表明一个成员变量或者是成员方法可以在没有所属的类的实例变量的情况下被访问。
Java 中 static 方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而 static 方法是编译时静态绑定的。static 方法跟类的任何实例都不相关,所以概念上不适用。 
4.是否可以在 static 环境中访问非 static 变量?

static 变量在 Java 中是属于类的,它在所有的实例中的值是一样的。当类被 Java 虚拟机载入的时候,会对 static 变量进行初始化。如果你的代码尝试不用实例来访问非 static 的变量,编译器会报错,因为这些变量还没有被创建出来,还没有跟任何实例关联上。
抽象方法是否可以同时是static,是否可同时是本地方法(native),是否可同时被synchronized修饰?

都不可以,抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。本地方法是由本地代码(如C代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。synchronized和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。

方法与函数

6.Java 中的方法覆盖(Overriding)和方法重载(Overloading)是什么意思?

Java 中的方法重载发生在同一个类里面两个或者是多个方法的方法名相同但是参数不同的情况。
与此相对,方法覆盖是说子类重新定义了父类的方法。方法覆盖必须有相同的方法名,参数列表和返回类型。覆盖者可能不会限制它所覆盖的方法的访问。
7.Java 中,什么是构造函数?什么是构造函数重载?什么是复制构造函数?

当新对象被创建的时候,构造函数会被调用。每一个类都有构造函数。在程序员没有给类提供构造函数的情况下,Java 编译器会为这个类创建一个默认的构造函数。
Java 中构造函数重载和方法重载很相似。可以为一个类创建多个构造函数。每一个构造函数必须有它自己唯一的参数列表。
Java 不支持像 C++中那样的复制构造函数,这个不同点是因为如果你不自己写构造函数的情况下,Java 不会创建默认的复制构造函数。 
13、数组有没有length()方法?String有没有length()方法? 
答:数组获取长度的手段是 .length 属性;String获取长度的手段是 length()方法;集合获取长度的手段是 size()方法;文件获取长度的手段是 length()方法
14、在Java中,如何跳出当前的多重嵌套循环? 
答:在最外层循环前加一个标记如A,然后用break A;可以跳出多重循环。(Java中支持带标签的break和continue语句,作用有点类似于C和C++中的goto语句,但是就像要避免使用goto一样,应该避免使用带标签的break和continue,因为它不会让你的程序变得更优雅,很多时候甚至有相反的作用,所以这种语法其实不知道更好)

public class TestBreak {
    public static void main(String[] args) {
        outfor: for (int i = 0; i < 10; i++){
            for (int j = 0; j < 10; j++){
                if (j == 5){
                    break outfor;
                }
                System.out.println("j = " + j);
            }
        }
    }
}
java在静态方法中可以调用哪些方法?

不能使用this调用本类的类方法(即静态方法),this指向的是实例对象,此时未实例化,故不能使用
在静态方法中调用本类的静态方法时可直接调用
在静态方法中,不只可以调用本类的静态方法,也可以使用【类名.静态方法名】调用其他类的静态方法
可以调用实例方法,使用【new 类名().实例方法名】调用

接口与抽象类

8.Java 支持多继承么?java8之后支持吗?

不支持,Java 不支持多继承。每个类都只能继承一个类,但是可以实现多个接口。 
如果同时出现继承和实现,则必须先继承(extends)再实现(implements)

Java 8以后,Java类依然不支持传统的多继承,即一个类不能直接继承多个类。但是,Java通过接口(interface)的改进,引入了默认方法(default methods)和静态方法(static methods),提供了一种模拟多继承效果的机制。
默认方法允许在接口中定义具有具体实现的方法,当一个类实现多个接口,而这些接口中有默认方法时,如果多个接口中定义了同名的默认方法,那么实现类需要显式地覆盖(Override)该方法以解决冲突,或者使用@Override和super.defaultMethodName()来明确指定调用哪个接口的默认方法。这种方式虽然不是真正的多继承,但可以在不修改接口的前提下,为实现类添加新的行为,类似于多重继承的效果。
总结来说,Java类本身仍然遵循单继承的原则,但通过接口的默认方法和静态方法,可以在一定程度上模拟多继承的功能,提供更灵活的类设计方式。
9.接口和抽象类的区别是什么?

接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。 类可以实现很多个接口,但是只能继承一个抽象类 
类如果要实现一个接口,它必须要实现接口声明的所有方法。但是,类可以不实现抽象类声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。 
抽象类可以在不提供接口方法实现的情况下实现接口。 
Java 接口中的属性在不提供修饰符修饰的情况下,会自动加上public static final的。抽象类可以包含非 final 的变量。
接口中属性不能用private,protected,default 修饰,因为默认是public
接口中如果属性是基本数据类型,需要赋初始值,若是引用类型,也需要初始化,因为默认有final修饰,必须赋初始值;
Java 接口中的成员函数默认是 public 的。抽象类的成员函数可以是 private,protected 或者是 public。 
接口是绝对抽象的,不可以被实例化。抽象类也不可以被实例化,但是,如果它包含 main方法的话是可以被调用的。 
接口可以继承接口?
抽象类能实现接口?
抽象类能继承实体类?

接口可以继承接口,比如list继承了Collection
抽象类可以实现接口,比如适配器实现了监听器接口
抽象类可以继承实体类,比如所有抽象类都继承了Object
java8之后对接口做了什么改动?

Java 8对接口做出了显著的修改,主要引入了两个新特性:默认方法(Default Methods)和静态方法(Static Methods)。这些修改极大地增强了接口的功能性和灵活性,使得接口不仅仅定义类型契约,还能提供具体的实现细节。

默认方法(Default Methods)
概念:默认方法允许在接口中定义具有具体实现的方法。这意味着接口可以提供一个默认的实现,而实现该接口的类可以选择保留这个默认实现,或者覆盖它提供自己的实现。
语法:默认方法使用default关键字定义,例如:
public interface MyInterface {
       default void myMethod() {
           System.out.println("Default implementation");
       }
   }
目的:默认方法的主要目的是为了接口的演进,可以在不破坏现有实现的情况下向接口中添加新方法。这对于库的维护尤为重要,因为库的接口更新不会强制所有实现它的客户端类进行修改。
冲突解决:如果一个类实现了多个接口,而这些接口中有同名的默认方法,则需要在实现类中明确覆盖该方法以解决冲突,或者使用super关键字指定调用哪个接口的默认方法。


静态方法(Static Methods)
概念:接口中的静态方法与类中的静态方法相似,它们属于接口本身而不是接口的实例。这使得接口可以提供一些工具方法,而无需实例化接口。
语法:静态方法使用static关键字定义,例如:
   public interface MyInterface {
       static void myStaticMethod() {
           System.out.println("Static method in interface");
       }
   }
用途:静态方法常用于提供与接口相关的实用函数,这些函数不依赖于接口实例的状态,可以作为辅助工具方法使用。
意义
兼容性:这些修改提高了代码的向后兼容性,使得API的升级更加平滑。
函数式编程:默认方法和静态方法的引入,配合Lambda表达式,促进了Java语言对函数式编程的支持,使得编写更加简洁、高效的代码成为可能。
设计模式:特别是默认方法,简化了设计模式如策略模式、工厂模式的实现,使得接口可以更加自然地定义算法家族,而不必为每个算法都创建一个抽象类。
综上所述,Java 8对接口的这些改动,不仅丰富了接口的表达能力,也为Java开发者带来了更现代、灵活的编程模型

值传递

10.什么是值传递和引用传递?

对象被值传递,意味着传递了对象的一个副本。因此,就算是改变了对象副本,也不会影响源对象的值。
对象被引用传递,意味着传递的并不是实际的对象,而是对象的引用。因此,外部对引用对象所做的改变会反映到所有的对象上。
在方法中,修改一个基础类型的参数不会影响原始参数值
在方法中,改变一个对象参数的引用不会影响到原始引用
在方法中,修改一个对象的属性会影响原始对象参数
在方法中,修改集合和Maps的元素会影响原始集合参数
18、当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递? 

答:JAVA中没有引用传递,是值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的内存地址。这个值(内存地址)被传递后,同一个内存地址指向堆内存当中的同一个对象,所以通过哪个引用去操作这个对象,对象的属性都是改变的。

堆栈

9、解释内存中的栈(stack)、堆(heap)和静态区(static area)的用法。 
答:
基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用内存中的栈空间;
通过new关键字和构造器创建的对象放在堆空间;
程序中的字面量(literal)如直接书写的100、"hello"和常量都是放在静态区中。
栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,理论上整个内存没有被其他进程使用的空间甚至硬盘上的虚拟内存都可以被当成堆空间来使用。

虚拟机栈和本地方法栈为什么是私有的?

虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
什么是堆和方法区?

堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

类加载

21、描述一下JVM加载class文件的原理机制? 

答:JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,它负责在运行时查找和装入类文件中的类。 
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这样可以节省内存开销
类装载方式,有两种 
      1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
      2.显式装载, 通过class.forname()等方法,显式加载需要的类

类加载过程是怎样的?

系统加载 Class 类型的文件主要三步:加载->连接->初始化。
连接过程又可分为三步:验证->准备->解析。
类加载器有哪些?什么是双亲委派模型?

JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader:
BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。
ExtensionClassLoader(扩展类加载器) :主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。
AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

每一个类都有一个对应它的类加载器。系统中的 ClassLoader 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派给父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为 null 时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。

对象

String s = new String("xyz");创建了几个String Object?

首先构造方法 new String("xyz"); 中的"xyz" 这本身就是一个字符串对象
然后 new 关键字一定会创建一个对象
所以总共创建了两个String对象
怎样创建一个immutable类?

将 class 自身声明为 final,这样别人就不能扩展来绕过限制了
将所有成员变量定义为 private 和 final,并且不要实现 setter 方法
通常构造对象时,成员变量使用深度拷贝来初始化,而不是直接赋值,这是一种防御措施,因为你无法确定输入对象不被其他人修改
如果确实需要实现 getter 方法,或者其他可能会返回内部状态的方法,使用 copy-on-write 原则,创建私有的 copy
String s = "Hello";s = s + " world!";这两行代码执行后,原始的 String 对象中的内容变了没有?

没有。因为 String被设计成不可变类,所以它的所有对象都是不可变对象。
s原先指向一个 String 对象,内容是 "Hello",然后我们对 s 进行了“+”操作,这时s不指向原来那个对象了,而指向了另一个 String 对象,内容为"Hello world!",原来那个对象还存在于内存之中,只是 s 这个引用变量不再指向它了。
String str1 = "hello";//这样创建字符串是存在于常量池中
String str2 = "he" + new String("llo");// str2存在于堆中
String str3 = "he" + "llo";//存在于常量池中
System.err.println(str1 == str2);//false,==是验证两个对象是否是一个(内存地址是否相同)
System.err.println(str1 == str3);//true,

String s1=”ab”, String s2=”a”+”b”, String s3=”a”, String s4=”b”, s5=s3+s4 请问s5==s2 返回什么?
返回false。在编译过程中,编译器会将s2 直接优化为”ab”,会将其放置在常量池当中,s5则是被创建在堆区,相当于s5=new String(“ab”);

运算

介绍一下java中位运算^,&,<<,>>,<<<,>>>

1.^(亦或运算) ,针对二进制,相同的为0,不同的为1
2.&(与运算) 针对二进制,只要有一个为0,就为0
3.<<(向左位移) 针对二进制,转换成二进制后向左移动3位,后面用0补齐
4.>>(向右位移) 针对二进制,转换成二进制后向右移动3位
5.>>>(无符号右移)  无符号右移,忽略符号位,空位都以0补齐

"|"与"||"的区别?

用法:condition 1 | condition 2、condition 1 || condition 2
"|"是按位或:先判断条件1,不管条件1是否可以决定结果(这里决定结果为true),都会执行条件2
"||"是逻辑或:先判断条件1,如果条件1可以决定结果(这里决定结果为true),那么就不会执行条件2
int型除以double型,结果是什么型?

int型除以double型,结果是double型
自动转换遵循以下规则:
1) 若参与运算量的类型不同,则先转换成同一类型,然后进行运算。
2) 转换按数据长度增加的方向进行,以保证精度不降低。如int型和long型运算时,先把int量转成long型后再进行运算。
a.若两种类型的字节数不同,转换成字节数高的类型
b.若两种类型的字节数相同,且一种有符号,一种无符号,则转换成无符号类型
3) 所有的浮点运算都是以双精度进行的,即使仅含float单精度量运算的表达式,也要先转换成double型,再作运算。
4) char型和short型参与运算时,必须先转换成int型。
5) 在赋值运算中,赋值号两边量的数据类型不同时,赋值号右边量的类型将转换为左边量的类型。
6) 如果右边量的数据类型长度左边长时,将丢失一部分数据,这样会降低精度,丢失的部分按四舍五入向前舍入。
三目运算符的空指针问题是怎么回事?

boolean flag = true; //设置成TRUE,保证表达式二一定可以执行
boolean simpleBoolean = false; //基本数据类型的boolean变量
Boolean nullBoolean = null; // 包装类的类型的Boolean变量
boolean x = flag ? nullBoolean : simpleBoolean; //使用三目运算符,并把结果赋值给基本类型的boolean
以上三目运算会抛出NPE,
反编译后的代码
boolean x = flag ? nullBoolean.booleanValue() : simpleBoolean;
编译器帮我们做了一次自动拆箱,导致了NPE

原因:当第二位,第三位操作数的类型相同时,则三目运算的结果和这两个操作数的类型相同;当第二,第三位操作数的类型分别为基本类型和包装类型,那么三目运算的结果类型要求是基本类型,如果结果不符合预期,编译器会进行自动拆箱


Map<String,Boolean> map = new HashMap<>();
Boolean b = (map != null ? map.get("hello") : false);
上面表达式在Java8以前是NPE,Java8和以后版本执行结果是null,因为Java8可以进行类型推断

拷贝

深拷贝和浅拷贝的区别是什么?

浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。
深拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深拷贝把要复制的对象所引用的对象都复制了一遍。
解析XML文档有哪几种方式?

主要是两种,SAX和DOM
SAX 就是逐行读取,直到找到目标数据为止
DOM 是先全文档加载,然后读取

关键字

final关键字除了修饰类之外,还有哪些用法呢?

final修饰的类,表示这个类不可被继承,这可以有效避免 API 使用者更改基础功能,某种程度上,这是保证平台安全的必要手段。
final修饰的变量,一旦赋值,不可重新赋值;可以清楚地避免意外赋值导致的编程错误
final修饰的方法无法被覆盖;
final修饰的实例变量,必须手动赋值,不能采用系统默认值;
final修饰的实例变量,一般和static联用,用来声明常量;
final不能和abstract关键字联合使用。
final是不是immutable?

注意,final 并不等同于 immutable

final List<String> strList = new ArrayList<>();
strList.add("Hello");
strList.add("world");  
List<String> unmodifiableStrList = List.of("hello", "world");
unmodifiableStrList.add("again");

final 只能约束 strList 这个引用不可以被赋值,但是 strList 对象行为不被 final 影响,添加元素等操作是完全正常的。如果我们真的希望对象本身是不可变的,那么需要相应的类支持不可变的行为。
在上面这个例子中,List.of 方法创建的本身就是不可变 List,最后那句 add 是会在运行时抛出异常的。

java8

如何取得年月日、小时分钟秒?

import java.time.LocalDateTime;
import java.util.Calendar;

class DateTimeTest {
    public static void main(String[] args) {
        Calendar cal = Calendar.getInstance();
        System.out.println(cal.get(Calendar.YEAR));
        System.out.println(cal.get(Calendar.MONTH)); // 0 - 11
        System.out.println(cal.get(Calendar.DATE));
        System.out.println(cal.get(Calendar.HOUR_OF_DAY));
        System.out.println(cal.get(Calendar.MINUTE));
        System.out.println(cal.get(Calendar.SECOND));
        // Java 8
        LocalDateTime dt = LocalDateTime.now();
        System.out.println(dt.getYear());
        System.out.println(dt.getMonthValue()); // 1 - 12
        System.out.println(dt.getDayOfMonth());
        System.out.println(dt.getHour());
        System.out.println(dt.getMinute());
        System.out.println(dt.getSecond());
    }
}
如何格式化日期?

import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Date;

class DateFormatTest {
    public static void main(String[] args) {
        SimpleDateFormat oldFormatter = new SimpleDateFormat("yyyy/MM/dd");
        Date date1 = new Date();
        System.out.println(oldFormatter.format(date1));
        // Java 8
        DateTimeFormatter newFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
        LocalDate date2 = LocalDate.now();
        System.out.println(date2.format(newFormatter));
    }
}
打印昨天的当前时刻?

import java.util.Calendar;

class YesterdayCurrent {
    public static void main(String[] args) {
        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.DATE, -1);
        System.out.println(cal.getTime());
    }
}

//java-8
import java.time.LocalDateTime;

class YesterdayCurrent {
    public static void main(String[] args) {
        LocalDateTime today = LocalDateTime.now();
        LocalDateTime yesterday = today.minusDays(1);
        System.out.println(yesterday);
    }
}

java线程

11.进程和线程的区别是什么?

进程是执行着的应用程序,而线程是进程内部的一个执行序列。一个进程可以有多个线程。线程又叫做轻量级进程。
并发优势与并发风险分别有哪些?

优势
速度:同时处理多个请求,响应更快;复杂的操作可以分成多个进程同时进行
设计:程序设计在某些情况下更简单,也可以有更多的选择。
资源利用:cup能够在等待io的时候做一些其他的事情

风险
安全性:多个线程共享数据时可能会产生与期望不符的结果
活跃性:某个操作无法继续进行下去时,就会发生活跃性问题。比如死锁,饥饿等问题
性能:线程过多时会使得CPU频繁切换,调度时间增多,同步机制,消耗过多内存
12.创建线程有几种不同的方式?你喜欢哪一种?为什么?

总结来说,创建线程只有一种方式,那就是构造Thread类,而实现线程的执行单元有两种方式。
方式一:实现Runnable接口的run方法,并把Runnable实例传给Thread类
方式二:重写Thread的run方法(继承Thread类)
通过Callable和FutureTask创建线程,也算是一种创建线程的方式

实现 Runnable 接口这种方式更受欢迎,因为这不需要继承 Thread 类。在应用设计中已经继承了别的对象的情况下,这需要多继承(而 Java 不支持多继承),只能实现接口。同时,线程池也是非常高效的,很容易实现和使用。
如何正确的停止线程?

使用interrupt来通知,而不是强制

不要使用stop,suspend,和resume方法
实现Runnable接口和Callable接口的区别?

Runnable 接口不会返回结果或抛出检查异常,但是Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。

工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。(Executors.callable(Runnable task)或 Executors.callable(Runnable task,Object resule))。
13.概括的解释下线程的几种可用状态。

就绪(Runnable):线程准备运行,不一定立马就能开始执行。 
运行中(Running):进程正在执行线程的代码。 
等待中(Waiting):线程处于阻塞的状态,等待外部的处理结束。 
睡眠中(Sleeping):线程被强制睡眠。 
I/O 阻塞(Blocked on I/O):等待 I/O 操作完成。 
同步阻塞(Blocked on Synchronization):等待获取锁。 
死亡(Dead):线程完成了执行。 
14.同步方法和同步代码块的区别是什么?

同步方法默认用this或者当前类class对象作为锁。
同步代码可以选择以什么来加锁,比同步方法更细颗粒化,同步代码可以同步有同步问题的部分代码而不是整个方法。
同步方法用关键字synchronized修饰方法,同步代码主要修饰需要进行同步的代码块,用synchronized(object){代码内容}进行修饰。
15.在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?

监视器和锁在 Java 虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。
程序计数器为什么是私有的?

程序计数器主要有下面两个作用:
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
并发编程的三个重要特性?

原子性 : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。synchronized 可以保证代码片段的原子性。
可见性 :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。
有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。
常见的线程安全类有哪些?

通过synchronized 关键字给方法加上内置锁来实现线程安全
Timer,TimerTask,Vector,Stack,HashTable,StringBuffer

原子类Atomicxxx—包装类的线程安全类
如AtomicLong,AtomicInteger等等
Atomicxxx 是通过Unsafe 类的native方法实现线程安全的

阻塞队列 BlockingQueue 和BlockingDeque
BlockingDeque接口继承了BlockingQueue接口,
BlockingQueue 接口的实现类有ArrayBlockingQueue ,LinkedBlockingQueue ,PriorityBlockingQueue 而BlockingDeque接口的实现类有LinkedBlockingDeque
BlockingQueue和BlockingDeque 都是通过使用定义为final的ReentrantLock作为类属性显式加锁实现同步的

CopyOnWriteArrayList和 CopyOnWriteArraySet
CopyOnWriteArraySet的内部实现是在其类内部声明一个final的CopyOnWriteArrayList属性,并在调用其构造函数时实例化该CopyOnWriteArrayList,CopyOnWriteArrayList采用的是显式地加上ReentrantLock实现同步,而CopyOnWriteArrayList容器的线程安全性在于在每次修改时都会创建并重新发布一个新的容器副本,从而实现可变性。

Concurrentxxx
最常用的就是ConcurrentHashMap,当然还有ConcurrentSkipListSet和ConcurrentSkipListMap等等。
ConcurrentHashMap使用了一种完全不同的加锁策略来提供更高的并发性和伸缩性。ConcurrentHashMap并不是将每个方法都在同一个锁上同步并使得每次只能有一个线程访问容器,而是使用一种粒度更细的加锁机制——分段锁来实现更大程度的共享
在这种机制中,任意数量的读取线程可以并发访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改Map,这使得在并发环境下吞吐量更高,而在单线程环境中只损失非常小的性能

ThreadPoolExecutor
ThreadPoolExecutor也是使用了ReentrantLock显式加锁同步

Collections中的synchronizedCollection(Collection c)方法可将一个集合变为线程安全,其内部通过synchronized关键字加锁同步

死锁

16.什么是死锁(deadlock)?

两个进程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是两个进程都陷入了无限的等待中。 
//手写一个死锁
public class DeadlockTest {
	//定义2个资源
    private static final Integer a = 0;
    private static final Integer b = 1;

    public static void main(String[] args) {
        //启动2个线程,分别调用getA()和getB()
        new Thread(DeadlockTest::getA).start();
        new Thread(DeadlockTest::getB).start();
    }
	static void getA() {
		//用synchronized 对a对象加锁
        synchronized (a) {
            System.out.println(Thread.currentThread().getName() + "获取到A锁");
            try {
                //等待500ms,再去获取B资源锁,让另一个线程有时间去独占b
                Thread.sleep(500);
                getB();
                System.out.println(Thread.currentThread().getName() + "获取到B锁");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static void getB() {
        synchronized (b) {
            System.out.println(Thread.currentThread().getName() + "获取到B锁");
             try {
                //等待500ms,再去获取A资源锁,让另一个线程有时间去独占a
                Thread.sleep(500);
            	getA();
            	System.out.println(Thread.currentThread().getName() + "获取到A锁");
             } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
/*
Thread-0获取到A锁
Thread-1获取到B锁

Thread-0在拿到a对象的监视器锁(后文简称“A锁”)之后,又要去拿b对象的监视器锁(简称“B锁”),而此时B锁在Thread-1手中,于是Thread-0只能阻塞,等待B锁被Thread-1释放。对Thread-1而言,亦是如此,死锁产生。

互斥条件:在上面代码中就是通过synchronized加锁,该锁是独占的、排它的。一个线程获取到之后,不允许第二个线程同时获取。

请求和保持条件:Thread-0拿到A锁的同时,又要请求B锁,但B锁被 Thread-1占有,所以要阻塞自己,等待B资源。

不剥夺条件:Thread-0不能抢占Thread-1已拥有的资源,只能等待其主动释放。

环路等待条件:hread-0等待Thread-1占用的资源B,Thread-1等待Thread-0占用的资源A,形成环路等待条件。
*/
17.如何确保 N 个线程可以访问 N 个资源同时又不导致死锁?

使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。

破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
破坏请求与保持条件 :一次性申请所有的资源。
破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
操作系统中死锁的四个必要条件?

互斥条件:进程对分配给它的资源进行排它性使用,即在一段时间内某一个资源只能由一个进程使用。如果其他进程申请使用该资源则必须等待,直到拥有者释放该资源;

请求和保持条件:进程已经至少保持了一个资源,但是又提出了新的资源请求,该资源又被其他进程占用,此进程被阻塞,但是并没有释放自己拥有的资源

不可抢占条件:分配给进程的资源,除非进程自己释放,否则其他进程不可抢占

循环等待:发生死锁时,必然存在一个进程-资源的循环链
只要破坏四个条件中的一个,就能阻止死锁情况的发生

同步与异步

线程同步的机制?

线程同步有4种机制:临界区,互斥量,事件,信号量
临界区:临界区是一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。 
PS:私人浴室(没有管理员)只有一间淋浴房,我想洗澡,我时不时来看下淋浴房空了没,空了我就去洗。

互斥量:功能上跟临界区类似,不过可用于不同进程间的线程同步。
PS:公共浴室(有管理员)只有一间淋浴房,我想洗澡,问了下管理员,有空的淋浴房么,如果有,管理员就让我洗,否则管理员就让我先去休息室睡一觉,等有空的淋浴房了叫醒我去洗澡。

事件:触发重置事件对象,那么等待的所有线程中将只有一个线程能唤醒,并同时自动的将此事件对象设置为无信号的;它能够确保一个线程独占对一个资源的访问。和互斥量的区别在于多了一个前置条件判定。
PS:公共浴室(有管理员)只有一间淋浴房,我想洗澡,问了下管理员,有空的淋浴房么,如果淋浴房没人洗而且打扫完了(等待的事件),管理员就让我洗,否则管理员就让我先去休息室睡一觉,等没人洗而且打扫完了叫醒我去洗澡。

信号量:信号量用于限制对临界资源的访问数量,保证了消费数量不会大于生产数量。
PS:公共浴室(有管理员)有N间(资源数量限制)淋浴房,我想洗澡,问了下管理员,有空的淋浴房么,如果有,管理员就让我洗,否则管理员就让我先去休息室睡一觉,等有空的淋浴房了叫醒我去洗澡。
同步和异步的区别?

同步(Sync):所谓同步,就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。

异步(Async):异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。 
阻塞和非阻塞的区别?

阻塞就是干不完不准回来,阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。
非阻塞就是你先干,我现看看有其他事没有,完了告诉我一声,

唤醒与阻塞

sleep() 方法和 wait() 方法区别和共同点?

两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁 。
两者都可以暂停线程的执行。
Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。
启动线程为什么调用start方法而不是直接调用run方法?

new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态(Runnable),当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
线程阻塞是调用的哪个方法?解除阻塞是调用的哪个方法?

sleep(毫秒),指定以毫秒为单位的时间,使线程在该时间内进入线程阻塞状态,期间得不到cpu的时间片,等到时间过去或调用interrupt方法强行中断,线程重新进入可执行状态。(暂停线程,不会释放锁)

yield()方法会使的线程放弃当前分得的cpu时间片,但此时线程任然处于可执行状态,随时可以再次分得cpu时间片。yield()方法只能使同优先级的线程有执行的机会。调用 yield()的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。(暂停当前正在执行的线程,并执行其他线程,且让出的时间不可知)

wait() 和 notify() 两个方法搭配使用,wait()使线程进入阻塞状态,调用notify()时,线程进入可执行状态。wait()内可加或不加参数,加参数时是以毫秒为单位,当到了指定时间或调用notify()方法时,进入可执行状态。(属于Object类,而不属于Thread类,wait()会先释放锁住的对象,然后再执行等待的动作。由于wait()所等待的对象必须先锁住,因此,它只能用在同步化程序段或者同步化方法内,否则,会抛出异常IllegalMonitorStateException.)

join()方法也叫线程加入。是当前线程A调用另一个线程B的join()方法,当前线程转A入阻塞状态,直到线程B运行结束,线程A才由阻塞状态转为可执行状态。

如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统

什么是上下文切换?

当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

线程池

为什么使用线程池?

降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池执行execute()方法和submit()方法的区别是什么呢?

两个方法都是线程池中提供的,都可以用来执行线程的调度任务
execute()只能执行实现Runnable接口类型的任务;而submit()不仅可以执实现Runnable类型接口的任务,也可以执行实现Callable接口类型的任务
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
如果线程执行发生异常,submit可以通过Future.get()方法抛出异常,方便我们自定义异常处理;而execute()会终止异常,没有返回值
如何优雅的终止线程池?

线程池提供了两个方法来终止线程:shutdown()和shutdownNow()。

shutdown() 方法是一种很保守的关闭线程池的方法。线程池执行 shutdown() 后,就会拒绝接收新的任务,但是会等待线程池中正在执行的任务和已经进入阻塞队列的任务都执行完之后才最终关闭线程池。

而 shutdownNow() 方法,相对就激进一些了,线程池执行 shutdownNow() 后,会拒绝接收新的任务,同时还会中断线程池中正在执行的任务,已经进入阻塞队列的任务也被剥夺了执行的机会,不过这些被剥夺执行机会的任务会作为 shutdownNow() 方法的返回值返回。因为 shutdownNow() 方法会中断正在执行的线程,所以提交到线程池的任务,如果需要优雅地结束,就需要正确地处理线程中断。

如果提交到线程池的任务不允许取消,那就不能使用 shutdownNow() 方法终止线程池。不过,如果提交到线程池的任务允许后续以补偿的方式重新执行,也是可以使用 shutdownNow() 方法终止线程池的。
如何创建线程池?

《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

Executors 返回线程池对象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
ThreadPoolExecutor构造函数重要参数分析?

corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
unit : keepAliveTime 参数的时间单位。
threadFactory :executor 创建新线程的时候会用到。
handler :饱和策略,也可以叫拒绝策略。
ThreadPoolExecutor拒绝策略有哪些?

AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
DiscardPolicy: 不处理新任务,直接丢弃掉。
DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
如何合理设置线程池参数?

CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。

CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。

I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

CPU核数 = Runtime.getRuntime().availableProcessors()


美团思路:把线程池参数改成可配置的
corePoolSize,maximumPoolSize,workQueue
格外需要注意的是corePoolSize, 程序运行期间的时候,我们调用 setCorePoolSize() 这个方法的话,线程池会首先判断当前工作线程数是否大于corePoolSize,如果大于的话就会回收工作线程。
另外,你也看到了上面并没有动态指定队列长度的方法,美团的方式是自定义了一个叫做 ResizableCapacityLinkedBlockIngQueue 的队列(主要就是把LinkedBlockingQueue的capacity 字段的final关键字修饰给去掉了,让它变为可变的)。
线程池中的阻塞队列有哪些?

ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
1:ArrayBlockingQueue是一个用数组实现的有界阻塞队列。
2:队列慢时插入操作被阻塞,队列空时,移除操作被阻塞。
3:按照先进先出(FIFO)原则对元素进行排序。
4:默认不保证线程公平的访问队列。
5:公平访问队列:按照阻塞的先后顺序访问队列,即先阻塞的线程先访问队列。
6:非公平性是对先等待的线程是非公平的,当队列可用时,阻塞的线程都可以争夺访问队列的资格。有可能先阻塞的线程最后才访问访问队列。
7:公平性会降低吞吐量。

LinkedBlockingQueue
一个基于链表结构的阻塞队列,此队列按 FIFO 排序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法 Executors.newFixedThreadPool() 使用了这个队列。(newFixedThreadPool 用于创建固定线程数)
1:LinkedBlockingQueue具有单链表和有界阻塞队列的功能。
2:队列慢时插入操作被阻塞,队列空时,移除操作被阻塞。
3:默认和最大长度为Integer.MAX_VALUE,相当于无界(值非常大:2^31-1)。

SynchronousQueue
一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue,静态工厂方法 Executors.newCachedThreadPool 使用这个队列。(newCachedThreadPool 用于根据需要创建新线程)
1:我称SynchronousQueue为”传球好手“。想象一下这个场景:小明抱着一个篮球想传给小花,如果小花没有将球拿走,则小明是不能再拿其他球的。
2:SynchronousQueue负责把生产者产生的数据传递给消费者线程。
3:SynchronousQueue本身不存储数据,调用了put方法后,队列里面也是空的。
4:每一个put操作必须等待一个take操作完成,否则不能添加元素。
5:适合传递性场景。
6:性能高于ArrayBlockingQueue 和 LinkedBlockingQueue。

PriorityBlockingQueue
一个具有优先级的无限阻塞队列。
1:PriorityBlockQueue = PriorityQueue + BlockingQueue
2:之前我们也讲到了PriorityQueue的原理,支持对元素排序。
3:元素默认自然排序。
4:可以自定义CompareTo()方法来指定元素排序规则。
5:可以通过构造函数构造参数Comparator来对元素进行排序。

原子类CAS

介绍一下Atomic原子类?

Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
基本类型:
AtomicInteger:整形原子类
AtomicLong:长整型原子类
AtomicBoolean:布尔型原子类
数组类型:
AtomicIntegerArray:整形数组原子类
AtomicLongArray:长整形数组原子类
AtomicReferenceArray:引用类型数组原子类
引用类型:
AtomicReference:引用类型原子类
AtomicStampedReference:原子更新引用类型里的字段原子类
AtomicMarkableReference :原子更新带有标记位的引用类型
对象的属性修改类型:
AtomicIntegerFieldUpdater:原子更新整形字段的更新器
AtomicLongFieldUpdater:原子更新长整形字段的更新器
AtomicStampedReference:原子更新带有版本号的引用类型。
介绍下AtomicInteger的使用?

AtomicInteger 类常用方法:
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

class AtomicIntegerTest {
        private AtomicInteger count = new AtomicInteger();
      //使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全。
        public void increment() {
                  count.incrementAndGet();
        }

       public int getCount() {
                return count.get();
        }
}

AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
原子整型类 AtomicInteger 的 getAndIncrement 方法用到 CAS,原理是什么?

atomicInteger.compareAndSet(10, 20);
调用 atomicInteger 的 CAS 方法,先比较当前变量 atomicInteger 的值是否是10,如果是,则将变量的值设置为20。

CAS 的全称:Compare-And-Swap(比较并交换)。比较变量的现在值与之前的值是否一致,若一致则替换,否则不替换。
CAS 的作用:原子性更新变量值,保证线程安全。
CAS 指令底层代码:需要有三个操作数,变量的当前值(V),旧的预期值(A),准备设置的新值(B)。
CAS 指令执行条件:当且仅当 V=A 时,处理器才会设置 V=B,否则不执行更新。
CAS 的返回值:V 的之前值。
CAS 处理过程:原子操作,执行期间不会被其他线程中断,线程安全。
CAS 并发原语:体现在 Java 语言中 sun.misc.Unsafe 类的各个方法。调用 UnSafe 类中的 CAS 方法,JVM 会帮我们实现出 CAS 汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作。由于 CAS 是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,所以 CAS 是一条 CPU 的原子指令,不会造成所谓的数据不一致的问题,所以 CAS 是线程安全的。
CAS会带来什么问题?

1:频繁出现自旋,循环时间长,开销大(因为执行的是do while,如果比较不成功一直在循环,最差的情况,就是某个线程一直取到的值和预期值都不一样,这样就会无限循环)
2:只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以通过循环CAS的方式来保证原子操作
但是对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候只能用锁来保证原子性。
什么事ABA问题?怎样解决?

因为 CAS 需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。

ABA 问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加 1,那么 A→B→A 就会变成 1A→2B→3A。

从Java 1.5开始,JDK 的 Atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。这个类的compareAndSet 方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

// 传递两个值,一个是初始值,一个是初始版本号
 AtomicStampedReference<BuildingBlock> atomicStampedReference = new AtomicStampedReference<>(A, 1);
// 创建一个线程“乙”执行ABA操作
new Thread(() -> {
    // 获取版本号
    int stamp = atomicStampedReference.getStamp();
    System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);
    // 暂停线程“乙”1秒钟,使线程“甲”可以获取到原子引用的版本号
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    /*
    * 乙线程开始ABA替换
    * */
    // 1.比较并替换,传入4个值,期望值A,更新值B,期望版本号,更新版本号
    atomicStampedReference.compareAndSet(A, B, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
    System.out.println(Thread.currentThread().getName() + "\t 第二次版本号" + atomicStampedReference.getStamp()); //乙     第一次版本号1
    // 2.比较并替换,传入4个值,期望值B,更新值A,期望版本号,更新版本号
    atomicStampedReference.compareAndSet(B, A, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); // 乙     第二次版本号2
    System.out.println(Thread.currentThread().getName() + "\t 第三次版本号" + atomicStampedReference.getStamp()); // 乙     第三次版本号3
}, "乙").start();

//创建一个线程“甲”执行D替换A操作
new Thread(() -> {
     // 获取版本号
     int stamp = atomicStampedReference.getStamp();
     System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp); // 甲   第一次版本号1
     // 暂停线程“甲”3秒钟,使线程“乙”进行一次ABA替换操作
     try {
     TimeUnit.SECONDS.sleep(3);
     } catch (InterruptedException e) {
         e.printStackTrace();
     }
     boolean result = atomicStampedReference.compareAndSet(A,D,stamp,stamp + 1);
     System.out.println(Thread.currentThread().getName() + "\t 修改成功否" + result + "\t 当前最新实际版本号:" + atomicStampedReference.getStamp()); // 甲     修改成功否false     当前最新实际版本号:3
     System.out.println(Thread.currentThread().getName() + "\t 当前实际最新值:" + atomicStampedReference.getReference()); // 甲     当前实际最新值:BuildingBlock{shape='三角形}

}, "甲").start();

volatile

volatile同步机制特性?

保证可见性
不保证原子性
禁止指令重排

volatile 保证了可见性:当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。
volatile 保证了单线程下指令不重排:通过插入内存屏障保证指令执行顺序。
volatitle不保证原子性,如a++这种自增操作是有并发风险的,比如扣减库存、发放优惠券的场景。
volatile 类型的64位的long型和double型变量,对该变量的读/写具有原子性。
volatile 可以用在双重检锁的单例模式种,比synchronized性能更好。
volatile 可以用在检查某个状态标记以判断是否退出循环。
怎样用volatile实现双重检查锁?

class VolatileSingleton {
    private static VolatileSingleton instance = null;
    private VolatileSingleton() {
        System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
    }
    public static VolatileSingleton getInstance() {
        // 第一重检测
        if(instance == null) {
            // 锁定代码块
            synchronized (VolatileSingleton.class) {
                // 第二重检测
                if(instance == null) {
                    // 实例化对象
                    instance = new VolatileSingleton();
                    //可被看作三条伪代码
                    //memory = allocate(); // 1、分配对象内存空间
                    //instance(memory); // 2、初始化对象
                    //instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null
                }
            }
        }
        return instance;
    }
}

如果另外一个线程执行:if(instance == null) 时,则返回刚刚分配的内存地址,但是对象还没有初始化完成,拿到的instance是个假的。
解决方案:定义instance为volatile变量
private static volatile VolatileSingleton instance = null;

可见性

举例验证volatile可见性

class ShareData {
    int number = 0;

    public void setNumberTo100() {
        this.number = 100;
    }
}


public class volatileVisibility {
    public static void main(String[] args) {
        // 资源类
        ShareData shareData = new ShareData();

        // 子线程 实现了Runnable接口的,lambda表达式
        new Thread(() -> {

            System.out.println(Thread.currentThread().getName() + "\t come in");

            // 线程睡眠3秒,假设在进行运算
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 修改number的值
            myData.setNumberTo100();

            // 输出修改后的值
            System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);

        }, "子线程").start();

        while(myData.number == 0) {
            // main线程就一直在这里等待循环,直到number的值不等于零
        }

        // 按道理这个值是不可能打印出来的,因为主线程运行的时候,number的值为0,所以一直在循环
        // 如果能输出这句话,说明子线程在睡眠3秒后,更新的number的值,重新写入到主内存,并被main线程感知到了
        System.out.println(Thread.currentThread().getName() + "\t 主线程感知到了 number 不等于 0");

        
      
    }
}
最后输出结果:
子线程     come in
子线程     update number value:100
最后线程没有停止,并行没有输出"主线程知道了 number 不等于0"这句话,说明没有用volatile修饰的变量,变量的更新是不可见的

// 我们用volatile修饰变量number
class ShareData {
    //volatile 修饰的关键字,是为了增加多个线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
    volatile int number = 0;

    public void setNumberTo100() {
        this.number = 100;
    }
}

输出结果:
子线程     come in
子线程     update number value:100
main     主线程知道了 number 不等于 0

小结:说明用volatile修饰的变量,当某线程更新变量后,其他线程也能感知到。

非原子性

验证volatile不支持原子性

public class VolatileAtomicity {
    public static volatile int number = 0;

    public static void increase() {
        number++;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    increase();
                }
            }, String.valueOf(i)).start();
        }

        // 当所有累加线程都结束
        while(Thread.activeCount() > 2) {
            Thread.yield();
        }

        System.out.println(number);
    }
}
执行结果:第一次19144,第二次20000,第三次19378。

分析一下increase()方法,通过反编译工具javap得到如下汇编代码:
public static void increase();
    Code:
       0: getstatic     #2                  // Field number:I
       3: iconst_1
       4: iadd
       5: putstatic     #2                  // Field number:I
       8: return
number++其实执行了3条指令:
执行了getstatic指令number的值取到操作栈顶时,volatile关键字保证了number的值在此时是正确的,但是在执行iconst_1、iadd这些指令的时候,其他线程可能已经把number的值改变了,而操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的number值同步回主内存之中。

解决方案:
我们可以通过使用synchronized同步代码块来保证原子性。
但是使用synchronized太重了,会造成阻塞,只有一个线程能进入到这个方法。我们可以使用Java并发包(JUC)中的AtomicInterger工具包。
可以使用AtomicInterger原子类

禁止指令重排

origin_img_v2_a7d1bd52-e11b-439e-addd-6b2d1711906g

volatile怎样实现的禁止指令重排?

计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。
1.编译器优化重排:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2.指令级的并行重排:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3.内存系统的重排:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

====================================================
定义了变量num=0和变量flag=false,线程1调用初始化函数init()执行后,线程调用add()方法,当另外线程判断flag=true后,执行num+100操作,那么我们预期的结果是num会等于101,但因为有指令重排的可能,num=1和flag=true执行顺序可能会颠倒,以至于num可能等于100
public class VolatileResort {
    static int num = 0;
    static boolean flag = false;
    public static void init() {
        num= 1;
        flag = true;
    }
    public static void add() {
        if (flag) {
            num = num + 5;
            System.out.println("num:" + num);
        }
    }
    public static void main(String[] args) {
        init();
        new Thread(() -> {
            add();
        },"子线程").start();
    }
}
线程1中指令重排:num= 1;flag = true; 的执行顺序变为 flag=true;num = 1;
如果线程2 num=num+5 在线程1设置num=1之前执行,那么线程2的num变量值为5

修改为:static volatile boolean flag = false;
原理:在volatile生成的指令序列前后插入内存屏障(Memory Barries)来禁止处理器重排序。
volatile写的场景如何插入内存屏障:
在每个volatile写操作的前面插入一个StoreStore屏障(写-写 屏障)。
在每个volatile写操作的后面插入一个StoreLoad屏障(写-读 屏障)。
StoreStore屏障可以保证在volatile写(flag赋值操作flag=true)之前,其前面的所有普通写(num的赋值操作num=1) 操作已经对任意处理器可见了,保障所有普通写在volatile写之前刷新到主内存。

volatile读场景如何插入内存屏障:
在每个volatile读操作的后面插入一个LoadLoad屏障(读-读 屏障)。
在每个volatile读操作的后面插入一个LoadStore屏障(读-写 屏障)。
LoadStore屏障可以保证其后面的所有普通写(num的赋值操作num=num+5) 操作必须在volatile读(if(flag))之后执行。
volatile都不保证原子性,为啥我们还要用它?

volatile是轻量级的同步机制,对性能的影响比synchronized小。
典型的用法:检查某个状态标记以判断是否退出循环。
比如线程试图通过类似于数绵羊的传统方法进入休眠状态,为了使这个示例能正确执行,asleep必须为volatile变量。否则,当asleep被另一个线程修改时,执行判断的线程却发现不了。

那为什么我们不直接用synchorized,lock锁?它们既可以保证可见性,又可以保证原子性为何不用呢?
因为synchorized和lock是排他锁(悲观锁),如果有多个线程需要访问这个变量,将会发生竞争,只有一个线程可以访问这个变量,其他线程被阻塞了,会影响程序的性能。

注意:当且仅当满足以下所有条件时,才应该用volatile变量
对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
该变量不会与其他的状态一起纳入不变性条件中。
在访问变量时不需要加锁。


AQS

介绍一下AQS?

AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。
说一下对AQS原理的理解?

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

AQS 内部数据和方法,可以简单拆分为:
1:一个 volatile 的整数成员表示同步状态,同时提供了 setState 和 getState 方法。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
2:一个先入先出(FIFO)的等待线程队列,以实现多线程间竞争和等待,这是 AQS 机制的核心之一。CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
3:各种基于 CAS 的基础操作方法,以及各种期望具体同步结构去实现的 acquire/release 方法。

利用 AQS 实现一个同步结构,至少要实现两个基本类型的方法,分别是 acquire 操作,获取资源的独占权;还有就是 release 操作,释放对某个资源的独占。
AQS组件有哪几个?

Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。

CountDownLatch (倒计时器): CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。

CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
1. 面试官:Java线程池底层是怎么实现的?大概说下
(1)既然说“大概说下”,那大概能回答 “工作线程队列”和“任务队列”应该就阔以了!
(2)回答:在Java中,所谓的线程池中的“线程”,其实是被抽象为一个静态内部类Worker,即“工作线程”,它基于AQS(抽象队列同步器)实现、存放在线程池一个成员变量中,其名为:“工作线程队列” HashSet<Worker> workers,而将等待被执行的任务存放在成员变量 “任务队列” workQueue(BlockingQueue<Runnable> workQueue)中;
这样一来,整个线程池实现的基本思想大概就是:从任务队列workQueue中不断取出需要执行的任务,放在工作线程队列Workers中进行处理;


2.面试官:嗯,不错,说一说创建线程池的几个核心构造参数?
(2)回答: Java中创建线程池其实非常灵活,我们可以通过配置不同的参数,创建出行为不同的线程池,这几个参数包括:
A. corePoolSize:线程池的核心线程数;
B. maximumPoolSize:线程池允许的最大线程数;
C. keepAliveTime:超过核心线程数时闲置线程的存活时间;
D. workQueue:任务执行前保存任务的队列,保存着execute方法待提交的Runnable任务;
E. handler :饱和策略,也可以叫拒绝策略。


3.面试官:那线程池中的线程是怎么创建的?是一开始就随着线程池的启动就创建好的吗?
(1)画外音:看过ThreadPoolExecutor的创建、executor下的执行方法API 即execute()方法的应该可以回答上!
(2)回答:不是;线程池在创建后执行初始化策略时默认是不启动工作线程Worker的,而是等待有请求到来时才启动,每当我们调用execute()方法添加一个任务时,线程池会做如下判断:
A.如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列workQueue;
如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出一个拒绝执行的异常RejectExecutionException;只有当一个线程完成任务时,它会从队列中取下一个任务来执行;
而当一个线程无事可做(也就是空闲) 且 超过一定的时间(keepAliveTime)时,线程池会判断如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉(销毁线程回收资源的过程),所以当线程池的所有任务完成后,它最终会收缩到corePoolSize的大小;


4.面试官:你刚刚提到可以通过配置不同的参数创建出不同的线程池,那么Java中默认实现好的线程池又有哪些呢?请比较它们的异同?
2)回答:
A.SingleThreadExecutor线程池:这种线程池只有一个核心线程在工作,也就是相当于单线程串行执行所有任务;如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,此线程池保证所有任务的执行顺序按照任务的提交顺序执行;其中涉及到的参数含义为:
Executors.newSingleThreadExecutor();
corePoolSize:1,只有一个核心线程在工作;
maximumPoolSize:1;
keepAliveTime:0L;
workQueue:newLinkedBlockingQueue<Runnable>(),其缓冲队列是无界的;

B.FixedThreadPool线程池:这种线程池是固定大小的线程池,只有核心线程;每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小;线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程;FixedThreadPool多数是针对一些很稳定很固定的正规并发线程;
Executors.newFixedThreadPool(N);  N是根据实际情况自定义设置的线程数
corePoolSize:nThreads
maximumPoolSize:nThreads
keepAliveTime:0L
workQueue:newLinkedBlockingQueue<Runnable>(),其缓冲队列是无界的。

C.CachedThreadPool线程池:这种线程池是无界线程池,如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务;
线程池的大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小,SynchronousQueue是一个是缓冲区为1的阻塞队列;
缓存型池子通常用于执行一些生存期很短的异步型任务,因此在一些面向连接的daemon型Server中用得不多,但对于生存期短的异步任务,它是Executor的首选;
Executors.newCachedThreadPool();
corePoolSize:0
maximumPoolSize:Integer.MAX_VALUE
keepAliveTime:60L
workQueue:newSynchronousQueue<Runnable>(), 一个缓冲区为1的阻塞队列。

D.ScheduledThreadPool线程池:一种核心线程数固定、大小无限制的线程池;此线程池适合 定时以及周期性执行任务需求的场景(定时任务);如果闲置,非核心线程池会在DEFAULT_KEEPALIVEMILLIS时间内回收;
Executors.newScheduledThreadPool(10);
corePoolSize:corePoolSize
maximumPoolSize:Integer.MAX_VALUE
keepAliveTime:DEFAULT_KEEPALIVE_MILLIS
workQueue:newDelayedWorkQueue()


5.面试官:如何在Java线程池中提交线程?
(1)画外音:这个只要能回答上execute()、submit()就阔以了
(2)回答:线程池最常用的提交任务的方法有两种:
A.execute():ExecutorService.execute方法接收一个Runable实例,它用来执行一个任务:
B.submit():ExecutorService.submit()方法返回的是Future对象;可以用isDone()来查询Future是否已经完成,当任务完成时,可以通过调用get()方法来获取结果;也可以不用isDone()进行检查就直接调用get(),在这种情况下,get()将阻塞,直至结果准备就绪


6.面试官:什么是Java的内存模型,Java中各个线程是怎么彼此看到对方的变量的?
(1)画外音:莫非是想聊主存和线程工作内存……
(2)回答:Java的内存模型定义了程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出这样的底层细节。此处的变量包括实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为这些是线程私有的,不会被共享,所以不存在竞争问题 (并发安全的源头)
那么Java中各个线程是怎么彼此看到对方的变量的呢?:Java中定义了主内存与工作内存的概念:所有的变量都存储在主内存,每条线程还有自己的工作内存,保存了被该线程使用到的变量的主内存副本拷贝;
线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接读写主内存的变量,不同的线程之间也无法直接访问对方工作内存的变量,线程间变量值的传递需要通过主内存。


7.面试官: 请谈谈volatile有什么特点,为什么它能保证变量对所有线程的可见性?
(2)回答:关键字volatile是Java虚拟机提供的最轻量级的同步机制,当一个变量被定义成volatile之后,具备两种特性:
A.保证此变量对所有线程的可见性:当一条线程修改了这个变量的值,新值对于其他线程是可以立即得知的,而普通变量做不到这一点。
B.禁止指令重排序优化:普通变量仅仅能保证在该方法执行过程中,得到正确结果,但是不保证程序代码的执行顺序;
Java的内存模型定义了8种内存间操作:lock和unlock把一个变量标识为一条线程独占的状态,把一个处于锁定状态的变量释放出来,释放之后的变量才能被其他线程锁定;
read和write把一个变量值从主内存传输到线程的工作内存,以便load把store操作从工作内存得到的变量的值,放入主内存的变量中;
load和store把read操作从主内存得到的变量值放入工作内存的变量副本中,把工作内存的变量值传送到主内存,以便write;
use和assgin把工作内存变量值传递给执行引擎,将执行引擎值传递给工作内存变量值;
volatile的实现基于这8种内存间操作,保证了一个线程对某个volatile变量的修改,一定会被另一个线程看见,即保证了可见性。


8.面试官:既然volatile能够保证线程间变量的可见性,是不是就意味着基于volatile变量的运算就是并发安全的?
(2)回答:不是,基于volatile变量的运算在并发下不一定是安全的;volatile修饰的变量在各个线程的工作内存,不存在一致性问题(各个线程的工作内存中volatile变量,每次使用前都要刷新到主内存);
但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的(其实就是时间先后问题:ThreadA刷新到主内存之前,ThreadB已经读取了主内存变量最新值,导致不一致)


9.面试官:请简单对比下volatile对比Synchronized的异同?
(2)回答:Synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性;
ThreadLocal和Synchonized都可用于解决多线程并发访问共享资源时产生冲突;
但是ThreadLocal与Synchronized有本质的区别;Synchronized用于实现同步机制,是利用锁的机制使变量或代码块在某一时该只能被一个线程访问,是一种“以时间换空间”的方式;而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象(除了对变量的共享),是一种“以空间换时间”的方式。


10.面试官:既然谈到了ThreadLocal,那你说一说它是怎么解决并发安全的?
(1)画外音:若能提到“线程私有内存”、“变量副本”那基本上就阔以了
(2)回答:它是Java提供的一种保存线程私有信息的机制,因为其在整个线程生命周期内有效,所以可以方便地在一个线程关联的不同业务模块之间传递信息,比如事务ID、Cookie等上下文相关信息。
ThreadLocal为每一个线程维护变量的副本,把共享数据的可见范围限制在同一个线程之内,其实现原理是,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。


11.面试官:很多人都说要慎用ThreadLocal,谈谈你的理解,使用ThreadLocal需要注意些什么?
(1)画外音:应该是想说remove操作吧.
(2)回答:使用ThreadLocal要注意remove;因为ThreadLocal的实现是其实基于一个ThreadLocalMap,在ThreadLocalMap中,它的key是一个弱引用;
而通常弱引用都会和引用队列配合清理机制使用,但是ThreadLocal是个例外,它并没有这么做;这意味着,废弃项目的回收依赖于显式地触发,否则就要等待线程结束,进而回收相应ThreadLocalMap,这就是很多OOM的来源;
所以通常都会建议,应用一定要自己负责remove,并且尽量不要和线程池一起使用!

synchronized

谈谈synchronized关键字以及使用?底层原理是什么?

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。
synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!

synchronized 关键字底层原理属于 JVM 层面。
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
//双重检查锁
public class Singleton {
	//uniqueInstance 采用 volatile 关键字修饰也是很有必要的,可以禁止JVM指令重排序
    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}
synchronized和ReentrantLock 的区别?

两者都是可重入锁:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

ReentrantLock 比 synchronized 增加了一些高级功能:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
synchronized和volatile的区别?

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在:
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。
volatile只能修饰实例变量和类变量,synchronized可以修饰方法和代码块。
多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。
1. 面试官:Synchronized有用过吗?谈谈你对它的理解
(2)回答:Synchronized是Java的关键词,JVM实现的一种可以实现并发产生的多个线程互斥同步访问共享资源的方式,也可以说是一种 “同步互斥锁”,在实际代码中可用于修饰代码块、方法、静态方法以及类;适用于单体应用系统架构



2.面试官:嗯,说一说它的原理?
(2)回答: 通过查看被Synchronized 修饰过的代码块编译后的字节码,会发现编译器会在被Synchronized修饰过的代码块的前、后生成两个字节码指令:monitorenter、monitorexit;
这两个字节码指令的含义:当JVM执行到monitorenter指令时,首先会尝试着先获取对象(共享资源)的锁,如果该对象没有被锁定、又或者当前线程已经拥有了这个对象的锁时,则锁的计数器count加1,即执行 +1 操作;当JVM执行monitorexit指令时,则将锁的计数器count减一,即执行 -1 操作;当计数器count为0时 ,该对象的锁就被释放了!!
如果当前线程获取该对象的锁失败了,则进入堵塞等待状态,直到该对象的锁被另外一个线程释放为止;即Java中的Synchronize底层其实是通过对象(共享资源)头、尾设置标记,从而实现锁的获取和释放。



3.面试官:你刚才提到获取对象的锁,说一说“锁”到底是什么,如何确定对象的锁?
(2)回答: “锁” 可以理解为monitorenter和monitorexit字节码指令之间的一个 Reference类型的参数,即要锁定Lock和解锁UnLock的对象
众所周知,使用Synchronized可以修饰不同的对象,因此,对应的对象的锁可以这么确定:
A.如果Synchronized 明确指定了“锁”的对象,比如Synchronized变量、 Synchronized(this) 等,说明加、解锁的即为该变量、当前对象;
B.若 Synchronized 修饰的方法为非静态方法,表示此方法对应的对象为“锁”对象; 若 Synchronized 修饰的方法为静态方法,则表示此方法对应的类对象为“锁”对象;
注意:当一个对象被锁住时,对象里面所有用Synchronized 修饰的方法都将产生堵塞, 而对象里非Synchronized 修饰的方法可正常被调用,不受锁的影响;




4.面试官:什么叫可重入锁,为什么说Synchronized是可重入锁?
(2)回答:通俗地讲,“可重入”指的是:当 当前线程获取到了当前对象的锁之后,如果后续的操作仍然需要获取获取该对象的锁时,可以不用再次重新获取,即可以直接操作该对象(共享资源);
可重入性是锁的一个基本要求,是为了解决自己锁死自己的情况,比如一个类的同步方法调用另一个同步方法时,假如Synchronized不支持重入,进入method2方法时当前线程已经获得锁,而在method2方法里面执行method1时当前线程又要去尝试获取锁,这时如果不支持重入,它就要等待释放,把自己阻塞,导致很有可能自己锁死自己!
对Synchronized来说,可重入性是显而易见的,刚才提到,在执行monitorenter指令时,如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,就把锁的计数器+1,其实本质上就是通过这种方式实现了可重入性(而不是已拥有了锁则不能继续获取)。




5.面试官:说一说JVM底层对Java的原生锁做了哪些优化?
(2)回答:在Java 6以前,Monitor的实现完全依赖底层操作系统的互斥锁来实现,也就是上面在问题2中所阐述的获取、释放锁的逻辑;由于Java的线程与操作系统的原生线程有映射关系,如果要将一个线程进行阻塞或唤起 都需要操作系统的协助,这就需要从用户态切换到内核态来执行,这种切换代价十分昂贵,很耗处理器时间,现代JDK中做了大量的优化;
一种优化是使用自旋锁,即在线程进行阻塞操作之前先让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时就无需再让线程执行阻塞操作,避免了用户态到内核态的切换。
而现代JDK中还提供了三种不同的Monitor实现,也就是三种不同的锁:偏向锁、轻量级锁、重量级锁
这三种锁使得JDK得以优化Synchronized的运行,当JVM检测到不同的竞争状况时,会自动切换到适合的锁实现,这就是锁的升级、降级。
当没有竞争出现时,默认使用偏向锁,JVM会利用CAS操作,在对象头上的MarkWord部分设置线程ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁,因为在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
如果有另一线程试图锁定某个被偏向锁锁过的对象,JVM就会自动撤销偏向锁,切换到轻量级锁实现;轻量级锁依赖CAS操作MarkWord来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁;




6.面试官:嗯,不错,Synchronized是公平锁还是非公平锁,为什么?
(2)回答:非公平;非公平主要表现在获取锁的行为上:并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象;




7.面试官: 为什么说Synchronized是悲观锁?
(2)回答:因为Synchronized的并发策略是悲观的:即不管是否会产生竞争,任何的数据操作都必须要加锁,包括“从用户态切换到核心态”、“维护锁计数器”和“检查被阻塞的线程是否需要被唤醒”等操作;
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略,即先进行操作,如果没有其他线程征用数据,那操作就成功了;
如果共享数据有征用,产生了冲突,那就再进行其他的补偿措施,这种乐观的并发策略的许多实现不需要线程挂起,所以被称为非阻塞同步。




8.面试官: 那你了解乐观锁吗,它的实现原理又是什么,能讲讲吗?
(2)回答:乐观锁,顾名思义表示系统总是认为当前的并发情况是乐观的,而不需要通过加各种锁进行控制;
乐观锁的实现原理是CAS机制(Compare And Swap,比较并交换),一种在JUC中广泛使用的算法;它涉及到三个操作数:内存值V、预期值A、新值B,当且仅当预期值A和内存值V相等时才将内存值V修改为新值B;
其底层实现逻辑:首先检查某块内存的值是否跟之前我读取的是一样的,如果不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存,即间接意味着获取锁成功!
CAS具有原子性,它的原子性是由CPU硬件指令实现保证的,即通过JNI调用Native方法,从而调用由C++编写的硬件级别指令,JDK中提供了Unsafe类来执行这些操作(查看JUC很多类的底层源码会发现 Unsafe.compareAndSwapxxx() 的调用无处不在,很牛逼!!!)



9.面试官:那乐观锁就一定是好的吗?
(2)回答:乐观锁可以避免 悲观锁独占对象这一现象 的出现,同时也提高了并发性能,但它也有一些缺点:
A.  乐观锁只能保证一个共享变量的原子操作:如果多一个或几个变量,乐观锁将变得力不从心,但互斥锁能轻易解决,不管对象数量多少及对象颗粒度大小;
B.  长时间自旋可能导致开销大。假如CAS长时间不成功而一直自旋,会给CPU带来很大的开销;
C.  ABA问题:CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不够严谨;
假如内存值原来是A,后来被一线程改为B,最后又被改回了A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。




10.面试官:刚提到ABA的问题,那有什么办法解决吗?
(2)回答:解决的思路是引入版本号,每次变量更新时都把版本号加1,同时如果条件允许,还需要额外建立数据更新历史表,并同时维护好版本号version 和 数据变更记录的映射关系!




11.面试官:跟Synchronized相比,可重入锁ReentrantLock的实现原理有什么不同?
(2)回答:其实,几乎所有锁的实现原理都是为了达到同个目的:让所有的线程都能看到某种标记,同一时刻只能有一个线程获取到锁;
Synchronized通过在对象头中设置标记MarkWord实现了这一目的,是一种JVM原生的锁实现方式;
而ReentrantLock以及所有的基于Lock接口的实现类,则是通过一个volitile关键字修饰的int类型变量,并保证每个线程都能拥有对该int变量的可见性和原子性,其本质是基于所谓的AQS框架;




12.面试官:你刚刚提到了AQS,那你说说AQS的实现原理?
(2)回答:AQS,即 AbstractQueuedSynchronizer  抽象队列同步器,是一个用来构建锁和同步器的类,JUC  Lock包下的锁(常用的有ReentrantLock、ReadWriteLock),以及其他的像Semaphore、CountDownLatch,甚至是早期的FutureTask等,都是基于AQS来构建的;
A:AQS在内部定义了一个变量:volatile int state,用于表示同步状态:当线程调用lock方法时,如果state=0,说明没有任何线程占有共享资源的锁,可以获得锁并将state=1;如果state=1,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待。
B:AQS内部是通过Node实体类来表示一个双向链表结构的同步队列,完成线程获取锁的排队工作,当有线程获取锁失败后,就被添加到队列末尾。
Node类是对要访问同步代码的线程的封装,包含了线程本身及其状态waitStatus(它有五种不同的取值,分别表示是否被阻塞、是否等待唤醒、是否已经被取消等),每个Node结点关联其prev结点和next结点(指针),方便线程释放锁后快速唤醒下一个在等待的线程,是一个FIFO的过程;
Node类有两个常量,SHARED和EXCLUSIVE,分别代表共享模式和独占模式,所谓共享模式是一个锁允许多条线程同时操作(信号量Semaphore就是基于AQS的共享模式实现的),独占模式指的是同一个时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待(如ReentranLock);
C:AQS通过内部类Condition Object构建等待队列(可有多个),当Condition调用wait()方法后,线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移到同步队列中竞争锁。
D:AQS和Condition各自维护了不同的队列,在使用Lock和Condition的时候,其实就是两个队列的互相移动。




13.面试官:请对比下Synchronized 和 ReentrantLock的异同?
(2)回答:ReentrantLock是Lock的实现类,是一个互斥的同步锁;
A:从功能角度上看,ReentrantLock比Synchronized的同步操作更精细(因为可以像普通对象一样使用),甚至实现了Synchronized没有的高级功能,如:
等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,对处理执行时间非常长的同步块很有用。
带超时的获取锁尝试:在指定的时间范围内获取锁,如果时间到了仍然无法获取则返回,可以判断是否有线程在排队等待获取锁。
可以响应中断请求:与Synchronized不同,当获取到锁的线程被中断时,能够响应中断,中断异常将会被抛出,同时锁会被释放。
可以实现公平锁:从锁的释放角度上看,Synchronized在JVM层面上实现的,不但可以通过一些监控工具监控Synchronized的锁定,而且在代码执行出现异常时,JVM会自动释放锁定;但是使用Lock则不行,Lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中。
B:从性能角度上看,Synchronized早期实现比较低效,对比ReentrantLock,大多数场景性能都相差较大,但是在Java6中对其进行了非常多的改进,在竞争不激烈时,Synchronized的性能要优于ReetrantLock;在高竞争情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。




14.面试官:上面提到ReentrantLock也是一种可重入锁,那它的底层又是如何实现的?
(2)回答:ReentrantLock内部自定义了同步器Sync(Sync既实现了AQS,又实现了AOS,而AOS提供了一种持有互斥锁的方式),其实就是加锁的时候通过CAS算法,将线程对象放到一个双向链表中,每次获取锁的时候,看下当前维护的那个线程ID和当前请求的线程ID是否一样,一样就可重入了。



15.面试官:除了Synchronized 和 ReentrantLock,你还接触过JUC下中的哪些并发工具?
(2)回答:通常所说的并发包JUC其实就是java.util.concurrent包及其子包下集合了Java并发的各种基础工具类,具体主要包括几个方面:
A:提供了CountDownLatch、CyclicBarrier、Semaphore等,比Synchronized更加高级,可以实现更加丰富多线程操作的同步结构;
B:提供了ConcurrentHashMap、有序的ConcunrrentSkipListMap,或者通过类似快照机制实现线程安全的动态数组CopyOnWriteArrayList等,各种线程安全的容器;
C:提供了ArrayBlockingQueue、SynchorousQueue或针对特定场景的PriorityBlockingQueue等,各种并发队列的实现;
D:强大的Executor框架,可以创建各种不同类型的线程池,调度任务运行等。



16.面试官:简单说一说ReadWriteLock 和StampedLock 吧?
(2)回答:虽然ReentrantLock和Synchronized简单实用,但是行为上有一定的局限性,要么不占,要么独占;在实际应用场景中,有时候不需要大量竞争的写操作,而是以并发读为主,为了进一步优化并发操作的粒度,Java提供了读写锁;
读写锁基于的原理是:多个读操作不需要互斥,如果读锁试图锁定时,写锁却被某个线程持有时,读锁将无法获得,而只好等待对方操作结束,这样就可以自动保证不会读取到脏数据;
ReadWriteLock代表了一对锁,它在数据量大 且 并发读多、写少的时候,能够比纯同步版本凸显出优势;
读写锁看起来比Synchronized的粒度似乎细一些,但在实际应用中,其表现也并不尽人意,主要还是因为相对比较大的开销;
所以,JDK在后期引入了StampedLock,在提供类似读写锁的同时,还支持优化读模式,优化读是基于这样的假设:大多数情况下读操作并不会和写操作冲突,其逻辑是先试着修改,然后通过validate方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获取读锁。



17.面试官:如何让Java的线程彼此同步?你了解过哪些同步器?
(1)画外音:应该是想聊JUC 同步器的三个成员 : CountDownLatch、 CyclicBarrier和 Semaphore

ThreadLocal

对ThreadLocal了解多少?

ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据
如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

JVM

JVM如何理解泛型概念?

1:虚拟机中没有泛型,只有普通类和普通方法
2:所有泛型类的类型参数在编译时都会被擦除
3:创建泛型对象时请指明类型,让编译器尽早的做参数检查
事实上,JVM并不知道泛型,所有的泛型在编译阶段就已经被处理成了普通类和方法。 处理方法很简单,我们叫做类型变量T的擦除(erased) 。在编译阶段,所有泛型类的类型参数都会被Object或者它们的限定边界来替换。

JMM

Java内存模型

为什么需要Java内存模型?特性是啥?

屏蔽各种硬件和操作系统的内存访问差异
JMM是Java内存模型,也就是Java Memory Model,简称JMM,本身是一种抽象的概念,实际上并不存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

可见性(当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改)
原子性(一个操作或一系列操作是不可分割的,要么同时成功,要么同时失败)
有序性(变量赋值操作的顺序与程序代码中的执行顺序一致)
关于有序性:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内似表现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

两大内存

并发条件下,变量的同步操作与同步规则有哪些?

同步操作:
lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态
unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便以后的load动作使用。
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量。
store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便以后的write操作。
write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

同步规则:
1:如果要把一个变量从主内存中复制到工作内存,就需要按顺序地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
2:不允许read和load、store和write操作之一单独出现
3:不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中
4:不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
5:一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
6:一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。
7:如果一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
8:如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
9:对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

java集合类

collection

动力节点集合类

18.Java 集合类框架的基本接口有哪些?

Java 集合类提供了一套设计良好的支持对一组对象进行操作的接口和类。Java 集合类里面最基本的接口有: 
Collection:代表一组对象,每一个对象都是它的子元素。 
Set:不包含重复元素的 Collection。
List:有顺序的 collection,并且可以包含重复元素。 
Map:可以把键(key)映射到值(value)的对象,键不能重复。 
19.为什么集合类没有实现 Cloneable 和 Serializable 接口?

集合类接口指定了一组叫做元素的对象。集合类接口的每一种具体的实现类都可以选择以它自己的方式对元素进行保存和排序。有的集合类允许重复的键,有些不允许。 
Java 集合类框架的最佳实践有哪些?

根据应用的需要正确选择要使用的集合的类型对性能非常重要,比如:假如元素的大小是固定的,而且能事先知道,我们就应该用 Array 而不是 ArrayList。 
有些集合类允许指定初始容量。因此,如果我们能估计出存储的元素的数目,我们可以设置初始容量来避免重新计算 hash 值或者是扩容。 
为了类型安全,可读性和健壮性的原因总是要使用泛型。同时,使用泛型还可以避免运行时的 ClassCastException。 
使用 JDK 提供的不变类(immutable class)作为 Map 的键可以避免为我们自己的类实现hashCode()和 equals()方法。 
编程的时候接口优于实现。 
底层的集合实际上是空的情况下,返回长度是 0 的集合或者是数组,不要返回 null。
判断一个集合类是否为线程安全的机制是什么?

看多个线程同时访问该类中的一个成员变量,是否需要枷锁,可以说,没有线程安全的类,即多线程访问的时候,几乎都需要加锁

迭代器

20.什么是迭代器(Iterator)?

Iterator 接口提供了很多对集合元素进行迭代的方法。每一个集合类都包含了可以返回迭代器实例的 
迭代方法。迭代器可以在迭代的过程中删除底层集合的元素。 
21.Iterator 和 ListIterator 的区别是什么?

Iterator 可用来遍历 Set 和 List 集合,但是 ListIterator 只能用来遍历 List。 
Iterator 对集合只能是前向遍历,ListIterator 既可以前向也可以后向。 
ListIterator 实现了 Iterator 接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。
33.Enumeration 接口和 Iterator 接口的区别有哪些?

Enumeration 速度是 Iterator 的 2 倍,同时占用更少的内存。但是,Iterator 远远比 Enumeration安全,因为其他线程不能够修改正在被 iterator 遍历的集合里面的对象。同时,Iterator 允许调用者删除底层集合里面的元素,这对 Enumeration 来说是不可能的。

安全失败与快速失败

22.快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?

Iterator 的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util.concurrent 包下面的所有的类都是安全失败的。

java.util包下面的所有的集合类都是快速失败的,迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。 

安全失败与快速失败

HashMap

23.Java 中的 HashMap 的工作原理是什么?

Java 中的 HashMap 是以键值对(key-value)的形式存储元素的。
HashMap 的一些重要的特性是它的容量(capacity),负载因子(load factor)和扩容极限(threshold resizing)。
HashMap主要由数组和链表组成,他不是线程安全的。核心的点就是put插入数据的过程,get查询数据以及扩容的方式。JDK1.7和1.8的主要区别在于头插和尾插方式的修改,头插容易导致HashMap链表死循环,并且1.8之后加入红黑树对性能有提升。

put插入数据流程
往map插入元素的时候首先通过对key hash然后与数组长度-1进行与运算((n-1)&hash),都是2的次幂所以等同于取模,但是位运算的效率更高。找到数组中的位置之后,如果数组中没有元素直接存入,反之则判断key是否相同,key相同就覆盖,否则就会插入到链表的尾部,如果链表的长度超过8,则会转换成红黑树,最后判断数组长度是否超过默认的长度*负载因子也就是12,超过则进行扩容。

get查询数据
查询数据相对来说就比较简单了,首先计算出hash值,然后去数组查询,是红黑树就去红黑树查,链表就遍历链表查询就可以了。

resize扩容过程
扩容的过程就是对key重新计算hash,然后把数据拷贝到新的数组。

hashmap

24.hashCode()和 equals()方法的重要性体现在什么地方?

Java 中的 HashMap 使用 hashCode()和 equals()方法来确定键值对的索引,当根据键获取值的时候也会用到这两个方法。如果没有正确的实现这两个方法,两个不同的键可能会有相同的hash 值,因此,可能会被集合认为是相等的。而且,这两个方法也用来发现重复元素。所以这两个方法的实现对 HashMap 的精确性和正确性是至关重要的。

hashCode()是Object 类的一个方法,返回一个哈希值。如果两个对象根据equal()方法比较相等,那么调用这两个对象中任意一个对象的hashCode()方法必须产生相同的哈希值。
如果两个对象根据eqaul()方法比较不相等,那么产生的哈希值不一定相等(碰撞的情况下还是会相等的。)

深入理解

put方法

25.HashMap 和 Hashtable 有什么区别?

HashMap 允许键和值是 null,而 Hashtable 不允许键或者值是 null。 
Hashtable 是同步的,而 HashMap 不是。因此,HashMap 更适合于单线程环境,而 Hashtable适合于多线程环境。 
HashMap 提供了可供应用迭代的键的集合,因此,HashMap 是快速失败的。另一方面, Hashtable 提供了对键的列举(Enumeration)。 
一般认为 Hashtable 是一个遗留的类。
HashMap 和 HashSet区别?

HashSet 底层就是基于 HashMap 实现的。
HashMap实现Map接口;HashSet实现Set接口
除了 clone()、writeObject()、readObject()是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。
HashMap使用键(Key)计算Hashcode
HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性
HashMap 的长度为什么是2的幂次方?

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。
数组下标的计算方法是“ (n - 1) & hash”。(n代表数组长度)。
取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作
也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;
采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
为什么 JDK 1.7 是头插法,JDK 1.8 是尾插法?

头插法是操作速度最快的,找到数组位置就直接找到插入位置了,jdk8之前hashmap这种插入方法在并发场景下如果多个线程同时扩容会出现循环列表。
jdk8开始hashmap链表在节点长度达到8之后会变成红黑树,这样一来在数组后节点长度不断增加时,遍历一次的次数就会少很多很多(否则每次要遍历所有),相比头插法而言,尾插法操作额外的遍历消耗已经小很多了,也可以避免之前的循环列表问题。
怎么避免或者减少哈希碰撞 ?

1:开放地址法;当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中
2:再哈希法:当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突时
3:链表法:将所有关键字为同义词的记录存储在同一线性链表中
4:建立一个公共溢出区;将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表

解决哈希冲突

数组array与列表list

26.数组(Array)和列表(ArrayList)有什么区别?什么时候应该使用 Array 而不是 ArrayList?

Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型。 
Array 大小是固定的,ArrayList 的大小是动态变化的。 
ArrayList 提供了更多的方法和特性,比如:addAll(),removeAll(),iterator()等等。 
对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。 
27.vector,ArrayList 和 LinkedList 有什么区别?

Vector 是 Java 早期提供的线程安全的动态数组,如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector 内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。Vector 在扩容时会提高 1 倍
ArrayList 和 LinkedList 都实现了 List 接口
ArrayList 是基于索引的数据接口,它的底层是数组。它可以以 O(1)时间复杂度对元素进行随机访问。与此对应,LinkedList 是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是 O(n)。 在扩容时ArrayList 则是增加 50%。
相对于 ArrayList,LinkedList 的插入,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。 
LinkedList 比 ArrayList 更占内存,因为 LinkedList 为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。
28.Comparable 和 Comparator 接口是干什么的?列出它们的区别。

Java 提供了只包含一个 compareTo()方法的 Comparable 接口。这个方法可以个给两个对象排序。具体来说,它返回负数,0,正数来表明输入对象小于,等于,大于已经存在的对象。
Java 提供了包含 compare()和 equals()两个方法的 Comparator 接口。compare()方法用来给两个输入参数排序,返回负数,0,正数表明第一个参数是小于,等于,大于第二个参数。equals()方法需要一个对象作为参数,它用来决定输入参数是否和 comparator 相等。只有当输入参数也是一个 comparator 并且输入参数和当前 comparator 的排序结果是相同的时候,这个方法才返回 true。 
29.什么是 Java 优先级队列(Priority Queue)?

PriorityQueue 是一个基于优先级堆的无界队列,它的元素是按照自然顺序(natural order)排序的。在创建的时候,我们可以给它提供一个负责给元素排序的比较器。PriorityQueue 不允许null 值,因为他们没有自然顺序,或者说他们没有任何的相关联的比较器。最后,PriorityQueue不是线程安全的,入队和出队的时间复杂度是 O(log(n))。
31.如何权衡是使用无序的数组还是有序的数组?

有序数组最大的好处在于查找的时间复杂度是 O(log n),而无序数组是 O(n)。有序数组的缺点是插入操作的时间复杂度是 O(n),因为值大的元素需要往后移动来给新元素腾位置。相反,无序数组的插入时间复杂度是常量 O(1)。
ArrayList是否是线程安全的?

是线程不安全的。ArrayList的实现主要就是用了一个Object的数组,用来保存所有的元素,以及一个size变量用来保存当前数组中已经添加了多少元素。
在多个线程进行add操作时可能会导致elementData数组越界
List a=new ArrayList()和ArrayList a =new ArrayList()的区别?

List list = new ArrayList();这句创建了一个 ArrayList 的对象后赋给了List。此时它是一个 List 对象了,有些ArrayList 有但是 List 没有的属性和方法,它就不能再用了。而ArrayList list=new ArrayList();创建一对象则保留了ArrayList 的所有属性。 所以需要用到 ArrayList 独有的方法的时候不能用前者。

List list = new ArrayList();
ArrayList arrayList = new ArrayList();
list.trimToSize(); //错误,没有该方法。
arrayList.trimToSize();    //ArrayList 里有该方法。

set

34.HashSet 和 TreeSet 有什么区别?

HashSet 是由一个 hash 表来实现的,因此,它的元素是无序的。add(),remove(),contains()方法的时间复杂度是 O(1)。 
另一方面,TreeSet 是由一个树形的结构来实现的,它里面的元素是有序的。因此,add(), remove(),contains()方法的时间复杂度是 O(logn)。
Set里的元素是不能重复的,那么用什么方法来区分重复与否呢?

1:首先看hashcode是否相同,如果不同,就是不重复的
2:如果hashcode相同,再比较equals,如果不同,就是不重复的,否则就是重复的

hashcode原理

/*
已知一个 HashMap<Integer,User>集合, User 有 name(String)和 age(int)属性。请写一个方法实现对HashMap 的排序功能,该方法接收 HashMap<Integer,User>为形参,返回类型为 HashMap<Integer,User>,要求对 HashMap 中的 User 的 age 倒序进行排序。排序时 key=value 键值对不得拆散。
*/
class HashMapTest {
    public static void main(String[] args) {
        HashMap<Integer, User> users = new HashMap<>();
        users.put(1, new User("张三", 25));
        users.put(3,new User("李四",22));
        users.put(2, new User("王五", 28));
        System.out.println(users);
        HashMap<Integer, User> sortHashMap = sortHashMap(users);
        System.out.println(sortHashMap);
        /**
         * 控制台输出内容
         * {1=User [name=张三, age=25], 2=User [name=王五,age=28], 3=User [name=李四, age=22]}
         * {2=User [name=王五, age=28], 1=User [name=张三, age=25], 3=User [name=李四, age=22]}
         */
    }

    public static HashMap<Integer, User> sortHashMap(HashMap<Integer, User> map) {
        // 首先拿到 map 的键值对集合
        Set<Map.Entry<Integer, User>> entrySet = map.entrySet();
        // 将 set 集合转为 List 集合,为什么,为了使用工具类的排序方法
        List<Map.Entry<Integer,User>> list = new ArrayList<Map.Entry<Integer, User>>(entrySet);
        // 使用 Collections 集合工具类对 list 进行排序,排序规则使用匿名内部类来实现
        Collections.sort(list, new Comparator<Map.Entry<Integer, User>>() {
            @Override
            public int compare(Map.Entry<Integer, User> o1, Map.Entry<Integer, User> o2) {
                //按照要求根据 User 的 age 的倒序进行排
                return o2.getValue().getAge() - o1.getValue().getAge();
            }
        });
        //创建一个新的有序的 HashMap 子类的集合
        LinkedHashMap<Integer, User> linkedHashMap = new LinkedHashMap<Integer, User>();
        //将 List 中的数据存储在 LinkedHashMap 中
        for (Map.Entry<Integer,User> entry : list) {
            linkedHashMap.put(entry.getKey(), entry.getValue());
        }
        return linkedHashMap;
    }
}
Java默认排序算法是什么?

对于原始数据类型,目前使用的是所谓双轴快速排序(Dual-Pivot QuickSort),是一种改进的快速排序算法,早期版本是相对传统的快速排序
而对于对象数据类型,目前则是使用TimSort,思想上也是一种归并和二分插入排序(binarySort)结合的优化排序算法。TimSort 并不是 Java 的独创,简单说它的思路是查找数据集中已经排好序的分区(这里叫 run),然后合并这些分区来达到排序的目的。
Java 8 引入了并行排序算法(直接使用 parallelSort 方法),这是为了充分利用现代多核处理器的计算能力,底层实现基于 fork-join 框架,处理的数据集比较小的时候,差距不明显,甚至还表现差一点;但是,当数据集增长到数万或百万以上时,提高就非常大了,具体还是取决于处理器和系统环境。

垃圾回收

35.Java 中垃圾回收有什么目的?什么时候进行垃圾回收?

垃圾回收的目的是识别并且丢弃应用不再使用的对象来释放和重用资源。
36.System.gc()和 Runtime.gc()会做什么事情?

System.gc() 在内部调用 Runtime.gc()。
硬要说区别的话 Runtime.gc() 是 native method。
而 System.gc() 是非 native method,它依次调用 Runtime.gc();调用gc方法在默认情况下,会显示触发full gc,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
system.gc 调用附带一个免责声明,无法保证垃圾收集器的调用。即gc()函数的作用只是提醒虚拟机,程序员希望进行一次垃圾回收。但是这次回收不能保证一定进行,具体什么时候回收取决于jvm。如果每次调用gc方法后想让gc必须执行,可以追加调用system. runFinalization方法。
37.finalize()方法什么时候被调用?析构函数(finalization)的目的是什么?

在释放对象占用的内存之前,垃圾收集器会调用对象的 finalize()方法。一般建议在该方法中释放对象持有的资源。
38.如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占用的内存?

不会,在下一个垃圾回收周期中,这个对象将是可被回收的。
39.Java 堆的结构是什么样子的?什么是堆中的永久代(Perm Gen space)?

JVM 的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在 JVM 启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。 

堆内存是由存活和死亡的对象组成的。存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些对象回收掉之前,他们会一直占据堆内存空间。

java堆从GC角度可分为老年代和新生代。其中新生代又分为Eden区和两个Survivor 区(以下简称S0区和S1区)

因为JAVA对象90%以上的对象都是朝生夕死的,其中GC回收的成本很高,为了提高性能所以将新生成的对象放在Eden区,将扛过多次GC的“老家伙”放在老年代

因为Eden区的绝大部分对象寿命很短,那么Eden每次满了清理垃圾,存活的对象被迁移到老年区,老年区满了,就会触发Full GC,Full GC是非常耗时的,设立s区的一个目的就是在Eden区和老年代中增加一个缓冲池,放一些“年纪不够老”的对象,增加垃圾回收性能

Survivor 区也会进行垃圾回收,但是并非主动进行的垃圾回收,是Eden区在进行垃圾回收的时候顺带回收、默认Eden区和 s0 ,s1 区的比例是 8:1:1。

(复制算法)设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入S1区(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生),接着新对象继续分配在Eden区和另外那块开始被使用的Survivor区,然后始终保持一块Survivor区是空着的,就这样一直循环使用这三块内存区域
JVM 的垃圾回收算法有哪些?

1:可达性分析算法(标记阶段)
原理: 可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。 虚拟机栈、本地方法栈、方法区、字符串常量池 等地方对堆空间进行引用的,都可以作为GC Roots进行可达性分析。

2:标记-清除算法(年轻代清除阶段)
原理: 当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除 标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收
缺点:
标记清除算法的效率不算高。
在进行GC的时候,需要停止整个应用程序,用户体验较差。
这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲列表。

3:复制算法(年轻代清除阶段)
因为标记-清除算法的缺点,由此发明了复制算法。 原理: 将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点: 复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点: 需要多余的内存空间。

4:标记整理算法(老年代清除阶段)
背景: 复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
原理: 第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象。 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。
优点:
消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。 消除了复制算法当中,内存减半的高额代价。
缺点: 从效率上来说,标记-整理算法要低于复制算法。 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址 移动过程中,需要全程暂停用户应用程序。即:STW。

5:分代收集算法
背景: 不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
年轻代:复制算法 老年代:由标记-清除或者是标记-清除与标记-整理的混合实现。

6:增量收集算法
原理: 如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
缺点: 使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

7:分区算法(G1 收集器)
原理: 分区算法将整个堆空间划分成连续的不同小区间。 每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
JVM 的垃圾回收器有哪些?

新生代收集器
Serial Serial 是一款用于新生代的单线程收集器,采用复制算法进行垃圾收集。Serial 进行垃圾收集时,不仅只用一条线程执行垃圾收集工作,它在收集的同时,所有的用户线程必须暂停(Stop The World)。 ParNew
ParNew 就是一个 Serial 的多线程版本,其它与Serial并无区别。ParNew 在单核 CPU 环境并不会比 Serial 收集器达到更好的效果,它默认开启的收集线程数和 CPU 数量一致,可以通过 -XX:ParallelGCThreads 来设置垃圾收集的线程数。
如下是 ParNew 收集器和 Serial Old 收集器结合进行垃圾收集的示意图,当用户线程都执行到安全点时,所有线程暂停执行,ParNew 收集器以多线程,采用复制算法进行垃圾收集工作,收集完之后,用户线程继续开始执行。
ParallelScavenge Parallel Scavenge 也是一款用于新生代的多线程收集器,与 ParNew 的不同之处是ParNew 的目标是尽可能缩短垃圾收集时用户线程的停顿时间,Parallel Scavenge 的目标是达到一个可控制的吞吐量。

老年代收集器
SerialOld
Serial Old 收集器是 Serial 的老年代版本,同样是一个单线程收集器,采用标记-整理算法。
ParallelOld
Parallel Old 收集器是 Parallel Scavenge 的老年代版本,是一个多线程收集器,采用标记-整理算法。可以与 Parallel Scavenge 收集器搭配,可以充分利用多核 CPU 的计算能力。
CMS(ConcurrentMarkSweep)
CMS 收集器是一种以最短回收停顿时间为目标的收集器,以 “ 最短用户线程停顿时间 ” 著称。整个垃圾收集过程分为 4 个步骤:
① 初始标记:标记一下 GC Roots 能直接关联到的对象,速度较快。
② 并发标记:进行 GC Roots Tracing,标记出全部的垃圾对象,耗时较长。
③ 重新标记:修正并发标记阶段引用户程序继续运行而导致变化的对象的标记记录,耗时较短。
④ 并发清除:用标记-清除算法清除垃圾对象,耗时较长。
整个过程耗时最长的并发标记和并发清除都是和用户线程一起工作,所以从总体上来说,CMS 收集器垃圾收集可以看做是和用户线程并发执行的。

堆内存收集器
G1
G1 收集器是 jdk1.7 才正式引用的商用收集器,现在已经成为 jdk9 默认的收集器。前面几款收集器收集的范围都是新生代或者老年代,G1 进行垃圾收集的范围是整个堆内存,它采用 “ 化整为零 ” 的思路,把整个堆内存划分为多个大小相等的独立区域(Region),在 G1 收集器中还保留着新生代和老年代的概念。

CMS与G1的区别
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收 集器一起使用;G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用; 
G1收集器可预测垃圾回收的停顿时间CMS收集器是使用“标记-清除”算 法进行的垃圾回收,容易产生内存碎片G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
41.在 Java 中,对象什么时候可以被垃圾回收?

当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。
42.JVM 的永久代中会发生垃圾回收么?

垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免 Full GC 是非常重要的原因。
分析下对象可达性状态流转

强可达(Strongly Reachable),就是当一个对象可以有一个或多个线程可以不通过各种引用访问到的情况。比如,我们新创建一个对象,那么创建它的线程对它就是强可达。

软可达(Softly Reachable),就是当我们只能通过软引用才能访问到对象的状态。

弱可达(Weakly Reachable),类似前面提到的,就是无法通过强引用或者软引用访问,只能通过弱引用访问时的状态。这是十分临近 finalize 状态的时机,当弱引用被清除的时候,就符合 finalize 的条件了。

幻象可达(Phantom Reachable),上面流程图已经很直观了,就是没有强、软、弱引用关联,并且 finalize 过了,只有幻象引用指向这个对象的时候。

不可达(unreachable),意味着对象可以被清除了。

所有引用类型,都是抽象类 java.lang.ref.Reference 的子类,它提供了 get() 方法,除了幻象引用(因为 get 永远返回 null),如果对象还没有被销毁,都可以通过 get 方法获取原有对象。这意味着,利用软引用和弱引用,我们可以将访问到的对象,重新指向强引用,也就是人为的改变了对象的可达性状态!所以,对于软引用、弱引用之类,垃圾收集器可能会存在二次确认的问题,以保证处于弱引用状态的对象,没有改变为强引用。如果我们错误的保持了强引用(比如,赋值给了 static 变量),那么对象可能就没有机会变回类似弱引用的可达性状态了,就会产生内存泄漏。

说一下 JVM 的垃圾回收器 CMS和G1?优缺点是什么?

CMS:以获取最短回收停顿时间为目标的收集器,基于并发“标记清理”实现;优点是:并发,低停顿;缺点是:对CPU非常敏感,无法处理浮动垃圾,会产生大量空间碎片

G1:是一款面向服务端应用的垃圾收集器;

垃圾回收器CMS和G1

回收的机制是什么?凭什么判断一个对象会被回收?

垃圾回收(Garbage Collection,GC),垃圾回收就是释放垃圾占用的空间

平常遇到的比较常见的将对象判定为可回收对象的情况:
1)显示地将某个引用赋值为null或者将已经指向某个对象的引用指向新的对象
Object obj = new Object();
obj = null; obj被回收
Object obj1 = new Object();
Object obj2 = new Object();
obj1 = obj2; obj1被回收

2)局部引用所指向的对象
for(int i=0;i<10;i++) {
        Object obj = new Object();
        System.out.println(obj.getClass());
    }
循环每执行完一次,生成的Object对象都会成为可回收的对象。

3)只有弱引用与其关联的对象
WeakReference<String> wr = new WeakReference<String>(new String("world"));

垃圾回收机制

说一下 GC Roots 包含哪些内容?

1.虚拟机栈中引用的对象 2.本地方法栈中native方法引用的对象 3.方法区中静态属性引用的变量 4.方法区中常量引用的对象 
什么情况下会发生新生代 gc?

指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 MinorGC 非常频繁,一般回收速度也比较快。对象优先在新生代 Eden 区中分配,如果 Eden 区没有足够的空间时,就会触发一次 Young GC 

异常处理

43.Java 中的两种异常类型是什么?他们有什么区别?

Java 中有两种异常:受检查的(checked)异常和不受检查的(unchecked)异常。不受检查的异常不需要在方法或者是构造函数上声明,就算方法或者是构造函数的执行可能会抛出这样的异常,并且不受检查的异常可以传播到方法或者是构造函数的外面。相反,受检查的异常必须要用 throws 语句在方法或者是构造函数上声明。
44.Java 中 Exception 和 Error 有什么区别?运行时异常和一般异常有什么区别?

Exception 和 Error 都是 Throwable 的子类。
Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。
Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。不检查异常就是所谓的运行时异常,类似 NullPointerException、ArrayIndexOutOfBoundsException 之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。
Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如 OutOfMemoryError 之类,都是 Error 的子类。
ClassNotFoundException和NoClassDefFoundError的区别和产生原因是什么?

ClassNotFoundException的产生原因:Java支持使用Class.forName方法来动态地加载类,任意一个类的类名如果被作为参数传递给这个方法都将导致该类被加载到JVM内存中,如果这个类在类路径中没有被找到,那么此时就会在运行时抛出ClassNotFoundException异常。

NoClassDefFoundError产生的原因在于:如果JVM或者ClassLoader实例尝试加载(可以通过正常的方法调用,也可能是使用new来创建新的对象)类的时候却找不到类的定义。要查找的类在编译的时候是存在的,运行的时候却找不到了。这个时候就会导致NoClassDefFoundError.造成该问题的原因可能是打包过程漏掉了部分类,或者jar包出现损坏或者篡改。解决这个问题的办法是查找那些在开发期间存在于类路径下但在运行期间却不在类路径下的类。
异常处理的方式有哪几种?

如 try-catch-finally 块,throw、throws 关键字等
比如 try-with-resources 和 multiple catch
在编译时期,会自动生成相应的处理逻辑,比如,自动按照约定俗成 close 那些扩展了 AutoCloseable 或者 Closeable 的对象。
try (BufferedReader br = new BufferedReader(…);
     BufferedWriter writer = new BufferedWriter(…)) {// Try-with-resources
// do something
catch ( IOException | XEception e) {// Multiple catch
   // Handle it
} 
异常处理的原则有哪些?

第一,尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常。
第二,不要生吞(swallow)异常。否则会加大排查问题的难度
第三,生产环境中不要打印堆栈信息,而是要把异常记录到日志中
第四,对于有可能出现NPE的地方,要提早做处理,否则不容易定位空指针的位置
Objects. requireNonNull(filename);
第五,函数返回值有两种类型:值类型与对象引用。对于对象引用,要特别小心,如果在finally代码块中对函数返回的对象成员属性进行了修改,即使不在finally块中显式调用return语句,这个修改也会作用于返回值上。
第六,继承某个异常时,重写方法时,要么不抛出异常,要么抛出一模一样的异常
第七,当一个try后跟了很多个catch时,必须先捕获小的异常再捕获大的异常。
为什么说Java 语言的 Checked Exception 也许是个设计错误?

Checked Exception 的假设是我们捕获了异常,然后恢复程序。但是,其实我们大多数情况下,根本就不可能恢复。Checked Exception 的使用,已经大大偏离了最初的设计目的。
Checked Exception 不兼容 functional 编程,如果你写过 Lambda/Stream 代码,相信深有体会。
Java的异常处理机制可能存在什么性能问题?

try-catch 代码段会产生额外的性能开销,或者换个角度说,它往往会影响 JVM 对代码进行优化,所以建议仅捕获有必要的代码段,尽量不要一个大的 try 包住整段的代码;与此同时,利用异常控制代码流程,也不是一个好主意,远比我们通常意义上的条件语句(if/else、switch)要低效。

Java 每实例化一个 Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个开销可就不能被忽略了。

当我们的服务出现反应变慢、吞吐量下降的时候,检查发生最频繁的 Exception 也是一种思路。
45.1 throw 和 throws 有什么区别?

throw 关键字用来在程序中明确的抛出异常,相反,throws 语句用来表明方法不能处理的异常。每一个方法都必须要指定哪些异常不能处理,所以方法的调用者才能够确保处理可能发生的异常,多个异常是用逗号分隔的。
45.2 异常处理的时候,finally 代码块的重要性是什么?

无论是否抛出异常,finally 代码块总是会被执行。就算是没有 catch 语句同时又抛出异常的情况下,finally 代码块仍然会被执行。最后要说的是,finally 代码块主要用来释放资源,比如:I/O 缓冲区,数据库连接。

有一种特例,finally块中的代码不会被执行
try {
  // do something
  System.exit(1);
} finally{
  //代码不会被执行
  System.out.println(“Print from finally”);
}
46.异常处理完成以后,Exception 对象会发生什么变化?

Exception 对象会在下一个垃圾回收过程中被回收掉。 
47.finally 代码块和 finalize()方法有什么区别?有什么机制可以替换 finalize 吗?

无论是否抛出异常,finally 代码块都会执行,它主要是用来释放应用占用的资源。
finalize()方法是 Object 类的一个 protected 方法,它是在对象被垃圾回收之前由 Java 虚拟机来调用的。它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。如果没有特别的原因,不要实现 finalize 方法,也不要指望利用它来进行资源回收。你无法保证 finalize 什么时候执行,执行的是否符合预期。使用不当会影响性能,导致程序死锁、挂起等。finalize 的执行是和垃圾收集关联在一起的,一旦实现了非空的 finalize 方法,就会导致相应对象回收呈现数量级上的变慢

Java 平台目前在逐步使用 java.lang.ref.Cleaner 来替换掉原有的 finalize 实现。Cleaner 的实现利用了幻象引用(PhantomReference),这是一种常见的所谓 post-mortem 清理机制。利用幻象引用和引用队列,我们可以保证对象被彻底销毁前做一些类似资源回收的工作,比如关闭文件描述符(操作系统有限的资源),它比 finalize 更加轻量、更加可靠。
吸取了 finalize 里的教训,每个 Cleaner 的操作都是独立的,它有自己的运行线程,所以可以避免意外死锁等问题。
try {}里有一个return语句,那么紧跟在这个try后的finally {}里的code会不会被执行,什么时候被执行,在return前还是后?

try里的return 和 finally里的return 都会执行,但是当前方法只会采纳finally中return的值
请列举五个最常见的runtime exception?

NullPointerException 空指针异常
ArithmeticException 算术异常,比如除数为零
ClassCastException 类型转换异常
ConcurrentModificationException 同步修改异常,遍历一个集合的时候,删除集合的元素,就会抛出该异常 
IndexOutOfBoundsException 数组下标越界异常
NegativeArraySizeException 为数组分配的空间是负数异常

RMI

78.什么是 RMI?

Java 远程方法调用(Java RMI)是 Java API 对远程过程调用(RPC)提供的面向对象的等价形式,支持直接传输序列化的 Java 对象和分布式垃圾回收。远程方法调用可以看做是激活远程正在运行的对象上的方法的步骤。RMI 对调用者是位置透明的,因为调用者感觉方法是执行在本地运行的对象上的
79.RMI 体系结构的基本原则是什么?

RMI 体系结构是基于一个非常重要的行为定义和行为实现相分离的原则。RMI 允许定义行为的代码和实现行为的代码相分离,并且运行在不同的 JVM 上。 
80.RMI 体系结构分哪几层?

存根和骨架层(Stub and Skeleton layer):这一层对程序员是透明的,它主要负责拦截客户端发出的方法调用请求,然后把请求重定向给远程的 RMI 服务。 
远程引用层(Remote Reference Layer):RMI 体系结构的第二层用来解析客户端对服务端远程对象的引用。这一层解析并管理客户端对服务端远程对象的引用。连接是点到点的。 
传输层(Transport layer):这一层负责连接参与服务的两个 JVM。这一层是建立在网络上机器间的 TCP/IP 连接之上的。它提供了基本的连接服务,还有一些防火墙穿透策略。
81.RMI 中的远程接口(Remote Interface)扮演了什么样的角色?

远程接口用来标识哪些方法是可以被非本地虚拟机调用的接口。远程对象必须要直接或者是间接实现远程接口。实现了远程接口的类应该声明被实现的远程接口,给每一个远程对象定义构造函数,给所有远程接口的方法提供实现。
82.java.rmi.Naming 类扮演了什么样的角色?

java.rmi.Naming 类用来存储和获取在远程对象注册表里面的远程对象的引用。Naming 类的每一个方法接收一个 URL 格式的 String 对象作为它的参数。 
83.RMI 的绑定(Binding)是什么意思?

绑定是为了查询找远程对象而给远程对象关联或者是注册以后会用到的名称的过程。远程对象可以使用 Naming 类的 bind()或者 rebind()方法跟名称相关联。 
84.Naming 类的 bind()和 rebind()方法有什么区别?

bind()方法负责把指定名称绑定给远程对象,rebind()方法负责把指定名称重新绑定到一个新的远程对象。如果那个名称已经绑定过了,先前的绑定会被替换掉。
85.让 RMI 程序能正确运行有哪些步骤?

编译所有的源文件。 
使用 rmic 生成 stub。 
启动 rmiregistry。 
启动 RMI 服务器。 
运行客户端程序。
86.RMI 的 stub 扮演了什么样的角色?

远程对象的 stub 扮演了远程对象的代表或者代理的角色。调用者在本地 stub 上调用方法,它负责在远程对象上执行方法。当 stub 的方法被调用的时候,会经历以下几个步骤: 
初始化到包含了远程对象的 JVM 的连接。 
序列化参数到远程的 JVM。 
等待方法调用和执行的结果。 
反序列化返回的值或者是方法没有执行成功情况下的异常。 
把值返回给调用者。
87.什么是分布式垃圾回收(DGC)?它是如何工作的?

DGC 叫做分布式垃圾回收。RMI 使用 DGC 来做自动垃圾回收。因为 RMI 包含了跨虚拟机的远程对象的引用,垃圾回收是很困难的。DGC 使用引用计数算法来给远程对象提供自动内存管理。
90.解释下 Serialization 和 Deserialization。

Java 提供了一种叫做对象序列化的机制,他把对象表示成一连串的字节,里面包含了对象的数据,对象的类型信息,对象内部的数据的类型信息等等。因此,序列化可以看成是为了把对象存储在磁盘上或者是从磁盘上读出来并重建对象而把对象扁平化的一种方式。反序列化是把对象从扁平状态转化成活动对象的相反的步骤。 

数据结构

java中的数据结构?

枚举(Enumeration)
位集合(BitSet)
向量(Vector)
栈(Stack)
字典(Dictionary)
哈希表(Hashtable)
属性(Properties)

java数据结构

IO流

java中有几种类型的流?

Java中所有的流都是基于字节流,所以最基本的流是
输入输出字节流
InputStream
OutputStream
在字节流的基础上,封装了字符流
Reader
Writer
进一步,又封装了缓存流
BufferedReader
PrintWriter
以及数据流
DataInputStream
DataOutputStream
对象流
ObjectInputStream
ObjectOutputStream
获取用键盘输入常用的两种方法?

方法1:通过 Scanner
Scanner input = new Scanner(System.in);
String s  = input.nextLine();
input.close();

方法2:通过 BufferedReader
BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); 
String s = input.readLine();
什么是IO流?

它是一种数据的流从源头流到目的地。比如文件拷贝,输入流和输出流都包括了。输入流从文件中读取数据存储到进程(process)中,输出流从进程中读取数据然后写入到目标文件。
字节流如何转为字符流?

字节输入流转字符输入流通过 InputStreamReader 实现,该类的构造函数可以传入 InputStream 对象。
字节输出流转字符输出流通过 OutputStreamWriter 实现,该类的构造函数可以传入 OutputStream 对象。
如何将一个 java 对象序列化到文件里?

在 java 中能够被序列化的类必须先实现 Serializable 接口,该接口没有任何抽象方法只是起到一个标记作用。

public class Test {
    public static void main(String[] args) throws Exception {
        //对象输出流
        ObjectOutputStream objectOutputStream =
                new ObjectOutputStream(new FileOutputStream(new File("D://obj")));
        objectOutputStream.writeObject(new User("zhangsan", 100));
        objectOutputStream.close();
        //对象输入流
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("D://obj")));
        User user = (User) objectInputStream.readObject();
        System.out.println(user);
        objectInputStream.close();
    }
}
字节流和字符流的区别?

字节流读取的时候,读到一个字节就返回一个字节;

字符流使用了字节流读到一个或多个字节(中文对应的字节数是两个,在 UTF-8 码表中是 3 个字节)时。先去查指定的编码表,将查到的字符返回。这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。

字节流可以处理所有类型数据,如:图片,MP3,AVI视频文件,而字符流只能处理字符数据。
只要是处理纯文本数据,就要优先考虑使用字符流,除此之外都用字节流。

字节流主要是操作 byte 类型数据,以 byte 数组为准,主要操作类就是 OutputStream、InputStream;
字符流处理的单元为 2 个字节的 Unicode 字符,分别操作字符、字符数组或字符串;
字节流处理单元为 1 个字节,操作字节和字节数组。
如何实现对象克隆?

1:实现 Cloneable 接口并重写 Object 类中的 clone()方法;
2:实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆
注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用 Object 类的 clone 方法克隆对象。让问题在编译的时候暴露出来总是好过把问题留到运行时。
浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。
深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。
什么是 java 序列化,如何实现 java 序列化?

序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。
可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。
序列化是为了解决在对对象流进行读写操作时所引发的问题。
序 列 化 的 实 现 : 将 需 要 被 序 列 化 的 类 实 现 Serializable 接 口 , 该 接 口 没 有 需 要 实 现 的 方 法 , implements Serializable 只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个 ObjectOutputStream(对象流)对象,接着,使用 ObjectOutputStream 对象的 writeObject(Object obj)方法就可以将参数为 obj 的对象写出(即保存其状态),要恢复的话则用输入流。
BIO,NIO,AIO 有什么区别?

BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。

NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发

AIO (Asynchronous I/O):它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

反射与动态代理

什么是反射机制?什么是动态代理?

反射机制是 Java 语言提供的一种基础功能,赋予程序在运行时自省(introspect,官方用语)的能力。通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。

动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制
什么是setAccessible?

反射提供的 AccessibleObject.setAccessible(boolean flag)。它的子类也大都重写了这个方法,这里的所谓 accessible 可以理解成修饰成员的 public、protected、private,这意味着我们可以在运行时修改成员访问限制!
对比一下JDK Proxy 和 cglib ?

JDK Proxy 的优势:
最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 cglib 更加可靠。
平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版 Java 上能够使用。
代码实现简单。

cglib 框架的优势:
有的时候调用目标可能不便实现额外接口,从某种角度看,限定调用者实现接口是有些侵入性的实践,类似 cglib 动态代理就没有这种限制。
只操作我们关心的类,而不必为其他相关类增加工作量。
高性能。

网络

网络体系结构

网络协议是什么?为什么对网络协议进行分层?

在计算机网络要做到有条不紊地交换数据,就必须遵守一些事先约定好的规则, 比如交换数据的格式、是否需要发送一个应答信息。这些规则被称为网络协议。

简化问题难度和复杂度。由于各层之间独立,我们可以分割大问题为小问题。
灵活性好。当其中一层的技术变化时,只要层间接口关系保持不变,其他层不受 影响。
易于实现和维护。
促进标准化工作。分开后,每层功能可以相对简单地被描述。

网络协议分层的缺点: 功能可能出现在多个层里,产生了额外开销。 为了使不同体系结构的计算机网络都能互联,国际标准化组织 ISO 于1977年提 出了一个试图使各种计算机在世界范围内互联成网的标准框架,即著名的开放系统互联基本参考模型 OSI/RM,简称为OSI。

TCP/IP 四层体系结构是怎样的?

应用层
应用层( application-layer )的任务是通过应用进程间的交互来完成特定网络应 用。应用层协议定义的是应用进程(进程:主机中正在运行的程序)间的通信和 交互的规则。对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如域 名系统 DNS,支持万维网应用的 HTTP 协议,支持电子邮件的 SMTP 协议等 等。

运输层
运输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通 用的数据传输服务。应用进程利用该服务传送应用层报文。 运输层主要使用一下两种协议
1. 传输控制协议-TCP:提供面向连接的,可靠的数据传输服务。
2. 用户数据协议-UDP:提供无连接的,尽大努力的数据传输服务(不 保证数据传输的可靠性)。

网络层
网络层的任务就是选择合适的网间路由和交换结点,确保计算机通信的数据及时 传送。在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和 包进行传送。在TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报 ,简称数据报。

数据链路层
数据链路层(data link layer)通常简称为链路层。两台主机之间的数据传输,总 是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。
在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装 成帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息 (如同步信息,地址信息,差错控制等)。
在接收数据时,控制信息使接收端能够知道一个帧从哪个比特开始和到哪个比特结束。

物理层
在物理层上所传送的数据单位是比特。 物理层(physical layer)的作用是实现相 邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的 差异。使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送 比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说, 这个电路好像是看不见的。

介绍下三次挥手和四次握手流程?

三次挥手
第一次握手:客户端要向服务端发起连接请求,首先客户端随机生成一个起始序列号ISN(比如是100),那客户端向服务端发送的报文段包含SYN标志位(也就是SYN=1),序列号seq=100。
第二次握手:服务端收到客户端发过来的报文后,发现SYN=1,知道这是一个连接请求,于是将客户端的起始序列号100存起来,并且随机生成一个服务端的起始序列号(比如是300)。然后给客户端回复一段报文,回复报文包含SYN和ACK标志(也就是SYN=1,ACK=1)、序列号seq=300、确认号ack=101(客户端发过来的序列号+1)。
第三次握手:客户端收到服务端的回复后发现ACK=1并且ack=101,于是知道服务端已经收到了序列号为100的那段报文;同时发现SYN=1,知道了服务端同意了这次连接,于是就将服务端的序列号300给存下来。然后客户端再回复一段报文给服务端,报文包含ACK标志位(ACK=1)ack=301(服务端序列号+1)、seq=101(第一次握手时发送报文是占据一个序列号的,所以这次seq就从101开始,需要注意的是不携带数据的ACK报文是不占据序列号的,所以后面第一次正式发送数据时seq还是101)。当服务端收到报文后发现ACK=1并且ack=301,就知道客户端收到序列号为300的报文了,就这样客户端和服务端通过TCP建立了连接。

四次挥手
第一次挥手:当客户端的数据都传输完成后,客户端向服务端发出连接释放报文(当然数据没发完时也可以发送连接释放报文并停止发送数据),释放连接报文包含FIN标志位(FIN=1)、序列号seq=1101(100+1+1000,其中的1是建立连接时占的一个序列号)。需要注意的是客户端发出FIN报文段后只是不能发数据了,但是还可以正常收数据;另外FIN报文段即使不携带数据也要占据一个序列号。
第二次挥手:服务端收到客户端发的FIN报文后给客户端回复确认报文,确认报文包含ACK标志位(ACK=1)、确认号ack=1102(客户端FIN报文序列号1101+1)、序列号seq=2300(300+2000)。此时服务端处于关闭等待状态,而不是立马给客户端发FIN报文,这个状态还要持续一段时间,因为服务端可能还有数据没发完。
第三次挥手:服务端将最后数据(比如50个字节)发送完毕后就向客户端发出连接释放报文,报文包含FIN和ACK标志位(FIN=1,ACK=1)、确认号和第二次挥手一样ack=1102、序列号seq=2350(2300+50)。
第四次挥手:客户端收到服务端发的FIN报文后,向服务端发出确认报文,确认报文包含ACK标志位(ACK=1)、确认号ack=2351、序列号seq=1102。注意客户端发出确认报文后不是立马释放TCP连接,而是要经过2MSL(最长报文段寿命的2倍时长)后才释放TCP连接。而服务端一旦收到客户端发出的确认报文就会立马释放TCP连接,所以服务端结束TCP连接的时间要比客户端早一些。

HTTP状态码

get请求和post请求的区别是什么?

1. Get是不安全的,因为在传输过程,数据被放在请求的URL中;Post的所有操作对用户来说都是不可见的。 但是这种做法也不时绝对的,大部分人的做法也是按照上面的说法来的,但是也可以在get请求加上 request body,给 post请求带上 URL 参数。
2. Get请求提交的url中的数据 多只能是2048字节,这个限制是浏览器或者服务器给添加的,http协议并没有对url长度进行限制,目的是为了保证服务器和浏览器能够正常运行,防止有人恶意发送请求。Post请求则没有大小限制。
3. Get限制Form表单的数据集的值必须为ASCII字符;而Post支持整个ISO10646字符集。
4. Get执行效率却比Post方法好。Get是form提交的默认方法。
5. GET产生一个TCP数据包;POST产生两个TCP数据包。对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

HTTP与HTTPS

http报文组成有那几部分?

一个HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据4个部分组成

1.请求行
请求行由请求方法字段、URL字段和HTTP协议版本字段3个字段组成,它们用空格分隔。例如,GET /index.html HTTP/1.1。
HTTP协议的请求方法有GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT。

2.请求头部
请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔。请求头部通知服务器有关于客户端请求的信息,典型的请求头有:
User-Agent:产生请求的浏览器类型。
Accept:客户端可识别的内容类型列表。
Host:请求的主机名,允许多个域名同处一个IP地址,即虚拟主机。

3.空行
最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头。

4.请求数据
请求数据不在GET方法中使用,而是在POST方法中使用。POST方法适用于需要客户填写表单的场合。与请求数据相关的最常使用的请求头是Content-Type和Content-Length。
http响应报文组成?

HTTP响应也由三个部分组成,分别是:状态行、消息报头、响应正文。
http与https的区别?

1) https协议需要申请证书。
2) http是超文本传输协议,明文传输;https使用的是具有安全性的SSL加密传输协议。
3) http端口80,;https端口443。
4) http连接简单无状态;https由SSL+HTTP协议构件的可进行加密传输、身份验证的网络协议。

网址解析

输入一个网址后,解析过程是怎样的?

1:首先是查找浏览器缓存,浏览器会保存一段时间你之前访问过的一些网址的DNS信息,不同浏览器保存的时常不等。
2:如果没有找到对应的记录,这个时候浏览器会尝试调用系统缓存来继续查找这个网址的对应DNS信息。
3:如果还是没找到对应的IP,那么接着会发送一个请求到路由器上,然后路由器在自己的路由器缓存上查找记录,路由器一般也存有DNS信息。
4:如果还是没有,这个请求就会被发送到ISP(注:Internet Service Provider,互联网服务提供商,就是那些拉网线到你家里的运营商,中国电信中国移动什么的)
5:如果还是没有的话, 你的ISP的DNS服务器会将请求发向根域名服务器进行搜索。
6:浏览器终于得到了IP以后,浏览器接着给这个IP的服务器发送了一个http请求,方式为get
7:接收请求响应,渲染页面

下一篇 八股文-web

Comments

Content