面试考点
Java面试
一、Java 基础
1.JDK 和 JRE 有什么区别?
JDK
是Java开发工具包,是Sun Microsystems针对Java开发员的产品。
JDK中包含JRE,在JDK的安装目录下有一个名为jre的目录,里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib和起来就称为jre。
JDK是整个JAVA的核心,包括了Java运行环境JRE(Java Runtime Envirnment)、一堆Java工具(javac/java/jdb等)和Java基础的类库(即Java API 包括rt.jar)。
①SE(J2SE),standard edition,标准版,是我们通常用的一个版本,从JDK 5.0开始,改名为Java SE。
②EE(J2EE),enterprise edition,企业版,使用这种JDK开发J2EE应用程序,从JDK 5.0开始,改名为Java EE。
③ME(J2ME),micro edition,主要用于移动设备、嵌入式设备上的java应用程序,从JDK 5.0开始,改名为Java ME。
JRE
是运行基于Java语言编写的程序所不可缺少的运行环境。也是通过它,Java的开发者才得以将自己开发的程序发布到用户手中,让用户使用。
JRE中包含了Java virtual machine(JVM),runtime class libraries和Java application launcher,这些是运行Java程序的必要组件。
与大家熟知的JDK不同,JRE是Java运行环境,并不是一个开发环境,所以没有包含任何开发工具(如编译器和调试器),只是针对于使用Java程序的用户。
2.== 和 equals 的区别是什么?
- ==是判断两个变量或实例是不是指向同一个内存空间,equals是判断两个变量或实例所指向的内存空间的值是不是相同
- ==是指对内存地址进行比较 , equals()是对字符串的内容进行比较
- ==指引用是否相同, equals()指的是值是否相同
3.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?
- 在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
- 两个对象的equals()相等,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。
- 两个对象的equals()不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不要求一定生成不同的整数结果。但是,为不相等的对象生成不同整数结果可以提高哈希表的性能。
4.final 在 java 中有什么作用?
在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)
-
修饰类
当用final修饰一个类时,表明这个类不能被继承。
在使用final修饰类的时候,要注意谨慎选择,除非这个类真的在以后不会用来继承或者出于安全的考虑,尽量不要将类设计为final类。
-
修饰方法
只有在想明确禁止 该方法在子类中被覆盖的情况下才将方法设置为final的。即父类的final方法是不能被子类所覆盖的,也就是说子类是不能够存在和父类一模一样的方法的。
-
修饰变量
final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
-
当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;
-
如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。
-
本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。
-
5.java 中的 Math.round(-1.5) 等于多少?
Math.round()四舍五入的原理,小数:
-
大于0.5,舍去小数,绝对值+1;
-
小于0.5,仅舍去小数;
-
等于0.5,取原数字+0.5
Math.floor() 求一个最接近它的整数,它的值小于或等于这个浮点数。(正数去掉小数,负数去小数+1)
6.String 属于基础的数据类型吗?
String不是基本的数据类型,是final修饰的java类,java中的基本类型一共有8个,它们分别为:
- 字符类型:byte,char
- 基本整型:short,int,long
- 浮点型:float,double
- 布尔类型:boolean
7.java 中操作字符串都有哪些类?它们之间有什么区别?
String、StringBuffer、StringBuilder
相同(StringBuffer、StringBuilder):
- 都是字符串的缓冲区、可变的字符序列;具有相同的构造和方法。
区别(String、StringBuffer、StringBuilder):
内存
- String 是不可变的对象,每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象,
- StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。
出现版本
- StringBuffer是 Jdk 1.1
- StringBuilder是 Jdk 1.5
线程安全
- StringBuffer线程安全,同步锁(synchronized),多线程仍可以保证数据安全
- StringBuilder线程不安全,多线程无法保证数据安全
效率
- StringBuilder > StringBuffer > String
总结
- 不频繁增改字符,就用String;否则用StringBuffer或StringBuilder
StringBuffer的构造方法
- public StringBuffer():无参构造方法
- public StringBuffer(int capacity):指定容量的字符串缓冲区对象
- public StringBuffer(String str):指定字符串内容的字符串缓冲区对象
8.String str="i"与 String str=new String(“i”)一样吗?
不一样。
因为内存的分配方式不一样。
String str="i"
的方式,Java 虚拟机会将其分配到常量池中;而String str=new String(“i”)
方式,则会被分到堆内存中。
String str1 = "i";
String str2 = "i";
String str3 = new String("i");
System.out.println(str1 == str2);//ture
System.out.println(str2 == str3);//false
解释:
Java 虚拟机会将其分配到常量池中:常量池不会重复创建对象。
- 在String str1="i"中,把i值存在常量池,地址赋给str1。假设再写一个String str2=“i”,则会把i的地址赋给str2,但是i对象不会重新创建,他们引用的是同一个地址值,共享同一个i内存。
分到堆内存中:堆内存会创建新的对象。
- 假设再写一个String str3=new String(“i”),则会创建一个新的i对象,然后将新对象的地址值赋给str3。虽然str3和str1的值相同但是地址值不同。
拓展:
- 堆内存用来存放由new创建的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。
- 常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。
- == :引用数据类型比较地址值;
- equals:引用类型,重写前比较两个对象地址值,重写后比较属性值。
9.如何将字符串反转?
StringBuilder的reverse()方法,最简单
public static String reverse4(String s) {
return new StringBuffer(s).reverse().toString();
}
使用字符串数组,实现从尾部开始逐个逆序放入字符串
public static String reverse3(String s) {
char[] array = s.toCharArray();
String reverse = "";
for(int i = array.length - 1; i >= 0; i--)
reverse += array[i];
return reverse;
}
使用String的CharAt方法,使用String的CharAt方法取出字符串中的各个字符,然后插入到字符串中,调用StringBuilder的insert()方法进行操作。
public static String reverse2(String s) {
int length = s.length();
String reverse = "";
for(int i = 0; i < length; i++)
reverse = s.charAt(i) + reverse;
return reverse;
}
使用递归的方法,实现字符串反转
public static String reverse1(String s) {
int length = s.length();
if(length <= 1){
return s;
}
String left = s.substring(0, length / 2);
String right = s.substring(length / 2, length);
return reverse1(right) + reverse1(left);
}
10.String 类的常用方法都有那些?
1、和长度有关:
-
int length()
得到一个字符串的字符个数
2、和数组有关:
-
byte[] getByte() )
将一个字符串转换成字节数组 -
char[] toCharArray()
将一个字符串转换成字符数组 -
String split(String
) 将一个字符串按照指定内容劈开
3、和判断有关:
-
boolean equals()
判断两个字符串的内容是否一样 -
boolean equalsIgnoreCase(String)
忽略太小写的比较两个字符串的内容是否一样 -
boolean contains(String)
判断一个字符串里面是否包含指定的内容 -
boolean startsWith(String)
判断一个字符串是否以指定的内容开头 -
boolean endsWith(String)
判断一个字符串是否以指定的内容结尾
4、和改变内容有关:
-
String toUpperCase()
将一个字符串全部转换成大写 -
String toLowerCase()
将一个字符串全部转换成小写 -
String replace(String,String)
将某个内容全部替换成指定内容 -
String replaceAll(String,String)
将某个内容全部替换成指定内容,支持正则 -
String repalceFirst(String,String)
将第一次出现的某个内容替换成指定的内容 -
String substring(int)
从指定下标开始一直截取到字符串的最后 -
String substring(x,y)
从下标x开始,向后截取y个字符, -
String trim()
去除一个字符串的前后空格
5、和位置有关:
-
char charAt(int)
得到指定下标位置对应的字符 -
int indexOf(String)
得到指定内容第一次出现的下标 -
int lastIndexOf(String)
得到指定内容最后一次出现的下标
11.抽象类必须要有抽象方法吗?
不需要
抽象类不一定有抽象方法;但是包含一个抽象方法的类一定是抽象类。(有抽象方法就是抽象类,是抽象类可以没有抽象方法)
解释:
抽象方法
java中的抽象方法就是以abstract修饰的方法,这种方法只声明返回的数据类型、方法名称和所需的参数,没有方法体,也就是说抽象方法只需要声明而不需要实现。
抽象方法与抽象类
当一个方法为抽象方法时,意味着这个方法必须被子类的方法所重写,否则其子类的该方法仍然是abstract的,而这个子类也必须是抽象的,即声明为abstract。abstract抽象类不能用new实例化对象,abstract方法只允许声明不能实现。如果一个类中含有abstract方法,那么这个类必须用abstract来修饰,当然abstract类也可以没有abstract方法。 一个抽象类里面没有一个抽象方法可用来禁止产生这种类的对象。
Java中的抽象类
abstract class 在 Java 语言中表示的是一种继承关系**,一个类只能使用一次继承关系。但是,一个类却可以实现多个interface。**
在abstract class 中可以有自己的数据成员,也可以有非abstarct的成员方法,而在interface中,只能够有静态的不能被修改的数据成员(也就是必须是static final的,不过在 interface中一般不定义数据成员),所有的成员方法都是abstract的。
12.普通类和抽象类有哪些区别?
- 抽象类不能被实例化
- 抽象类可以有抽象方法,抽象方法只需申明,无需实现
- 含有抽象方法的类必须申明为抽象类
- 如果没有实现抽象基类中所有的抽象方法,则子类成为一个抽象子类;如果实现抽象类中所有抽象方法,他就是非抽象子类;
- 抽象方法不能被声明为静态static
- 抽象方法不能用private修饰
- 抽象方法不能用final修饰
13.抽象类能使用 final 修饰吗?
不能,抽象类是被用于继承的,而用final修饰的类,无法被继承。
14.接口和抽象类有什么区别?
- 抽象类要被子类继承,接口要被类实现。
- 接口只能做方法声明,抽象类中可以作方法声明,也可以做方法实现。
- 接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。
- 4.接口是设计的结果,抽象类是重构的结果。
- 5.抽象类和接口都是用来抽象具体对象的,但是接口的抽象级别最高。
- 6.抽象类可以有具体的方法和属性,接口只能有抽象方法和不可变常量。
- 7.抽象类主要用来抽象类别,接口主要用来抽象功能。
15.java 中 IO 流分为几种?
字节输入流(InputStream)
常用的字节输入流主要有:
-
InputStream
-
FileInputStream
-
BufferedInputStream
【BufferedInputStream不是InputStream的直接实现子类,是FilterInputStream的子类】InputStream是字节输入流的抽象基类 ,InputStream作为基类,给它的基类定义了几个通用的函数:
-
read(byte[] b)
:从流中读取b的长度个字节的数据存储到b中,返回结果是读取的字节个数(当再次读时,如果返回-1说明到了结尾,没有了数据) -
read(byte[] b, int off, int len)
:从流中从off的位置开始读取len个字节的数据存储到b中,返回结果是实际读取到的字节个数(当再次读时,如果返回-1说明到了结尾,没有了数据) -
close()
:关闭流,释放资源。
-
-
FileInputStream主要用来操作文件输入流,它除了可以使用基类定义的函数外,它还实现了基类的read()函数(无参的)
-
read()
:从流中读取1个字节的数据,返回结果是一个int,(如果编码是以一个字节一个字符的,可以尝试转成char,用来查看数据)。
-
-
BufferedInputStream带有缓冲的意思,普通的读是从硬盘里面读,而带有缓冲区之后,BufferedInputStream已经提前将数据封装到内存中,内存中操作数据要快,所以它的效率要要非缓冲的要高。它除了可以使用基类定义的函数外,它还实现了基类的read()函数(无参的)
-
read()
:从流中读取1个字节的数据,返回结果是一个int,(如果编码是以一个字节一个字符的,可以尝试转成char,用来查看数据)。
使用
-
InputStream是抽象基类,所以它不可以创建对象,但它可以用来“接口化编程”,因为大部分子类的函数基类都有定义,所以利用基类来调用函数。
-
FileInputStream是用来读文件数据的流,所以它需要一个文件对象用来实例化,这个文件可以是一个File对象,也可以是文件名路径字符串.【这里文件不存在会抛错】
-
BufferedInputStream是一种封装别的流以提高效率的流,所以它的初始化需要一个的InputStream流对象。
-
字节输出流(OutputStream)
常用的字节输入流主要有:
-
OutputStream
-
FileOutputStream
-
BufferedOutputStream
【BufferedOutputStream 不是OutputStream的直接实现子类,是FileOutputStream的子类】OutputStream是字节输出流的抽象基类 ,OutputStream作为基类,给它的基类定义了几个通用的函数:
-
write(byte[] b)
:将b的长度个字节数据写到输出流中。 -
write(byte[] b,int off,int len)
:从b的off位置开始,获取len个字节数据,写到输出流中。 -
flush()
:刷新输出流,把数据马上写到输出流中。 -
close()
:关闭流,释放系统资源。
FileOutputStream是用于写文件的输出流,它除了可以使用基类定义的函数外,还实现了OutputStream的抽象函数write(int b)
-
write(int b)
:将b转成一个字节数据,写到输出流中。
BufferedOutputStream像上面那个BufferedInputStream一样,都可以提高效率。它除了可以使用基类定义的函数外,它还实现了OutputStream的抽象函数write(int b):
-
write(int b)
:将b转成一个字节数据,写到输出流中。
使用
-
OutputStream是抽象基类,所以它不能实例化,但它可以用于接口化编程。
-
FileOutputStream是用于写文件的输出流,所以它需要一个文件作为实例化参数,这个文件可以是File对象,也可以是文件路径字符串。【如果文件不存在,那么将自动创建。】【FileOutputStream实例化时可以给第二个参数,第二个参数是是否使用追加写入默认,为true时代表在原有文件内容后面追加写入数据,默认为false】
-
BufferedOutputStream需要一个输出流作为实例化参数。
-
字符输入流(Reader)
常见的字符输入流有:
-
Reader
-
InputStreamReader
-
FileReader
-
BufferedReader
Reader是字符输入流的抽象基类 ,它定义了以下几个函数
-
read()
:读取单个字符,返回结果是一个int,需要转成char;到达流的末尾时,返回-1 -
read(char[] cbuf)
:读取cbuf的长度个字符到cbuf这种,返回结果是读取的字符数,到达流的末尾时,返回-1 -
close()
:关闭流,释放占用的系统资源。
InputStreamReader 可以把InputStream中的字节数据流根据字符编码方式转成字符数据流。它除了可以使用基类定义的函数,它自己还实现了以下函数
-
read(char[] cbuf, int offset, int length)
:从offset位置开始,读取length个字符到cbuf中,返回结果是实际读取的字符数,到达流的末尾时,返回-1
FileReader 可以把FileInputStream中的字节数据转成根据字符编码方式转成字符数据流。
BufferedReader可以把字符输入流进行封装,将数据进行缓冲,提高读取效率。它除了可以使用基类定义的函数,它自己还实现了以下函数
-
read(char[] cbuf, int offset, int length)
:从offset位置开始,读取length个字符到cbuf中,返回结果是实际读取的字符数,到达流的末尾时,返回-1 -
readLine()
:读取一个文本行,以行结束符作为末尾,返回结果是读取的字符串。如果已到达流末尾,则返回 null
使用
-
Reader 是一个抽象基类,不能实例化,但可以用于接口化编程。
-
InputStreamReader需要一个字节输入流对象作为实例化参数。还可以指定第二个参数,第二个参数是字符编码方式,可以是编码方式的字符串形式,也可以是一个字符集对象。
-
FileReader 需要一个文件对象作为实例化参数,可以是File类对象,也可以是文件的路径字符串。
-
BufferReader需要一个字符输入流对象作为实例化参数。
-
字符输出流(Writer)
常见的字符输出流有:
-
Writer
-
OutputStreamWriter
-
FileWriter
-
BufferedWriter
Writer是字符输出流的抽象基类, ,它定义了以下几个函数
-
-
write(char[] cbuf)
:往输出流写入一个字符数组。 -
write(int c)
:往输出流写入一个字符。 -
write(String str)
:往输出流写入一串字符串。 -
write(String str, int off, int len)
:往输出流写入字符串的一部分。 -
close()
:关闭流,释放资源。 【这个还是抽象的,写出来是说明有这个关闭功能】 -
flush()
:刷新输出流,把数据马上写到输出流中。 【这个还是抽象的,写出来是说明有这个关闭功能】
-
-
OutputStreamWriter可以使我们直接往流中写字符串数据,它里面会帮我们根据字符编码方式来把字符数据转成字节数据再写给输出流,它相当于一个中介\桥梁。
-
FileWriter与OutputStreamWriter功能类似,我们可以直接往流中写字符串数据,FileWriter内部会根据字符编码方式来把字符数据转成字节数据再写给输出流。
-
BufferedWriter比FileWriter还高级一点,它利用了缓冲区来提高写的效率。它还多出了一个函数:
-
-
newLine()
:写入一个换行符。
-
-
使用
-
-
Writer 是一个抽象基类,不能实例化,但可以用于接口化编程。
-
OutputStreamWriter 需要一个输入流对象作为实例化参数。
-
FileWriter 需要一个文件对象来实例化,可以是File类对象,也可以是文件的路径字符串。
-
BufferWriter
-
16.BIO、NIO、AIO 有什么区别?
Java共支持3种网络编程模型/IO模式:BIO、NIO、AIO。
Java BIO :同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
Java NIO :同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理 。
Java AIO(NIO.2) :异步非阻塞,AIO 引入异步通道的概念,采用了 Reactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
17.Files的常用方法都有哪些?
-
Files. exists()
:检测文件路径是否存在。 -
Files. createFile()
:创建文件。 -
Files. createDirectory()
:创建文件夹。 -
Files. delete()
:删除一个文件或目录。 -
Files. copy()
:复制文件。 -
Files. move()
:移动文件。 -
Files. size()
:查看文件个数。 -
Files. read()
:读取文件。 -
Files. write()
:写入文件。
二、容器
18.java 容器都有哪些?
数组,String,java.util下的集合容器
数组长度限制为: Integer.Integer.MAX_VALUE;
String的长度限制为:底层是char 数组 长度 Integer.MAX_VALUE 线程安全的
java.util下的集合容器
Set下各种实现类对比
**HashSet
**基于哈希表实现,有以下特点:
- 不允许重复
- 允许值为null,但是只能有一个
- 无序的。
- 没有索引,所以不包含索引操作的方法
**LinkedHashSet
**跟HashSet一样都是基于哈希表实现。只不过linkedHashSet在hashSet的基础上多了一个链表,这个链表就是用来维护容器中每个元素的顺序的。有以下特点:
- 不允许重复
- 允许值为null,但是只能有一个
- 有序的。
- 没有索引,所以不包含索引操作的方法
**TreeSet
**是SortedSet接口的唯一实现类,是基于二叉树实现的。TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序 和定制排序,其中自然排序为默认的排序方式。向TreeSet中加入的应该是同一个类的对象。有以下特点:
- 不允许重复
- 不允许null值
- 没有索引,所以不包含索引操作的方法
List下各种实现类对比。(这几个类都是有序的,允许重复的)
**ArrayList
**是基于数组实现的,其特点是查询快,增删慢。
查询快是因为数组的空间是连续的,查询时只要通过首地址和下标很快就能找到元素。
增删慢是因为数组是不能扩容的,一旦增加或者删除元素,内部操作就是新开辟一个数组把元素copy到新的数组,老的数组等待被垃圾回收。
**LinkedList
**是基于链表实现的。相比于ArrayList其特点是查询慢,增删快
查询慢:因为链表在内存中开辟的空间不一定是连续的(基本上不可能是连续的)所以链表实现的方式是每个元素节点都会存放自己的地址,数据以及下一个节点的地址,这样把所有的元素连接起来。所以当要查询元素时只能一个一个的往下找,相比于数组的首地址加下标会慢上不少。
**Vector
**也是基于数组实现的,相比于arrayList它是线程安全的。如果不考虑线程安全,ArrayList性能更优。
Map是双列集合的超类。也就是键值对形式。
HashMap
和Hashtable
都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。
-
HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。
-
HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了**
ConcurrentHashMap
**,它是HashTable的替代,比HashTable的扩展性更好 -
另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。
-
由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。
-
HashMap不能保证随着时间的推移Map中的元素次序是不变的。
**LinkedHashMap
**和hashMap的区别在于多维护了一个链表,用来存储每一个元素的顺序,就跟HashSet和LinkedHashSet差不多。
**HashMap
**通常比TreeMap快一点(树和哈希表的数据结构使然),建议多使用HashMap,在需要排序的Map时候才用TreeMap。
19.Collection 和 Collections 有什么区别?
Collection是单列集合的*接口,其派生了两个子接口 Set 和 List。定义的是所有单列集合*有的功能。
Collections则是集合类的一个工具类/帮助类,其中提供了很多静态方法。用于对集合中元素进行排序、搜索以及线程安全等操作。
总之:Collection是一个接口,而Collections是个类。
20.List、Set、Map 之间的区别是什么?
List:
- 可以允许重复对象
- 可以索引(可用普通的for循环遍历)
- 是一个有序容器
Set:
- 数据不重复(使用equals()方法保证数据不重复)
- 无序,存取的顺序不同
- 无索引
Map:
- Map 是双列集合的顶层接口
- Map 有两个对象:键(是唯一的)、值(是不唯一的)。键值对,是映射关系
- 键(key):有规律,容易记,简单的数据
- 值(value):无规律,难记,复杂的数据
21.HashMap 和 Hashtable 有什么区别?
1.线程安全性不同
- Hashtable是线程安全的,它的每个方法中都加入了Synchronize方法。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步
- HashMap不是线程安全的,在多线程并发的环境下,可能会产生死锁等问题。
- 虽然HashMap不是线程安全的,但是它的效率会比Hashtable要好很多。在我们的日常使用当中,大部分时间是单线程操作的。
2.继承的父类不同
- HashTable是继承自Dictionary类,而HashMap是继承自AbstractMap类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。
3.是否可以储存null
- HashTable不允许储存null值(key和value都不可以),HashMap允许使用null值(key和value)都可以。
4.遍历方法不同
- HashTable使用Enumeration遍历,HashMap使用Iterator进行遍历。
5.初始化和扩容方式不同
- Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
- 创建时,如果给定了容量初始值,那么Hashtable会直接使用你给定的大小,而HashMap会将其扩充为2的幂次方大小。也就是说Hashtable会尽量使用素数、奇数。而HashMap则总是使用2的幂作为哈希表的大小。
22.如何决定使用 HashMap 还是 TreeMap?
TreeMap
- TreeMap<K,V>的Key值是要求实现java.lang.Comparable,所以迭代的时候TreeMap默认是按照Key值升序排序的;
- TreeMap的实现是基于红黑树结构。适用于按自然顺序或自定义顺序遍历键(key)。
HashMap
- HashMap<K,V>的Key值实现散列hashCode(),分布是散列的、均匀的,不支持排序;
- 数据结构主要是桶(数组),链表或红黑树。适用于在Map中插入、删除和定位元素。
结论
- 如果你需要得到一个有序的结果,就应该使用TreeMap(因为HashMap中元素的排列顺序是不固定的)。
- 不需要排序就使用HashMap(HashMap有更好的性能)。
25.ArrayList 和 LinkedList 的区别是什么?
区别:
数据结构:
- ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
随机访问方式:
- LinkedList 是线性的数据存储方式,需要移动指针从前往后依次查找。
- 所以 ArrayList 比 LinkedList 在随机访问的时候效率要高,
增加和删除:
- ArrayList 增删操作要影响数组内的其他数据的下标。
- 所以在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,
综合来说:
- 在需要频繁读取集合中的元素时,更推荐使用 ArrayList,
- 而在插入和删除操作较多时,更推荐使用 LinkedList。
共性:
- ArrayList 与 LinkedList 都是单列集合中 List 接口的实现类,他们都是存取有序,有索引,可重复的。
26.如何实现数组和 List 之间的转换?
- 数组转 List ,使用 JDK 中 java.util.Arrays 工具类的 asList 方法
import java.util.Arrays;
import java.util.List;
public class test26 {
public static void main(String[] args) {
String[] strs = new String[] {"aaa", "bbb", "ccc"}; //数组
List<String> list = Arrays.asList(strs); //list
for (String s : list) {
System.out.println(s); //循环输出
}
}
}
- List 转数组,使用 List 的toArray方法。
- 无参toArray方法返回Object数组,传入初始化长度的数组对象,返回该对象数组
import java.util.Arrays;
import java.util.List;
public class test266 {
public static void main(String[] args) {
List<String> list = Arrays.asList("aaa", "bbb", "ccc"); //list
String[] array = list.toArray(new String[list.size()]); //数组
for (String s : array) {
System.out.println(s); //循环输出
}
}
}
27.ArrayList 和 Vector 的区别是什么?
- List接口一共有三个实现类,分别是ArrayList、Vector和LinkedList。
- List用于存放多个元素,能够维护元素的次序,并且允许元素的重复。
主要区别:
- 同步性:Vector是线程安全的,用synchronized实现线程安全,而ArrayList是线程不安全的。(实现同步需要很高的花费,所以访问Vector比访问ArrayList慢)
- 数据容量增长:二者都有一个初始容量大小,采用线性连续存储空间,当存储的元素的个数超过了容量时,就需要增加二者的存储空间,Vector增长原来的一倍,ArrayList增加原来的0.5倍。
总结:
- LinkedList:增删改快
- ArrayList:查询快(有索引的存在)
- 如果只有一个线程会访问到集合,那最好使用ArrayList,因为它不考虑线程安全,效率会高些;如果有多个线程会访问到集合,那最好是使用Vector,因为不需要我们再去考虑和编写线程安全的代码。
拓展:
- LinkedList和ArrayList都是通过数组实现。缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。
- LinkedList是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。
28.Array 和 ArrayList 有何区别?
根本区别:Array 是数组;ArrayList 是集合
- 存储内容比较: Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。
- 空间大小比较: array 是数组,arraylist 是集合,集合可以根据自身变化扩大,而数组创建后不可以变化。
- 方法上的比较: ArrayList提供了更多的方法和特性,比如:addAll(),removeAll(),iterator()等等。
- Array类型的变量在声明的同时必须进行实例化(至少得初始化数组的大小),而ArrayList可以只是先声明。
- 对于基本数据类型,集合(ArrayList)使用自动装箱来减少编码工作量;但是Array相对较慢。
29.在 Queue 中 poll()和 remove()有什么区别?
- 队列(queue)是一个典型的先进先出(FIFO)的容器。即从容器的一端放入事物,从另一端取出,并且事物放入容器的顺序与取出的顺序是相同的。
相同点:
- 都是返回第一个元素,并在队列中删除返回的对象。
不同点:
- remove() ,如果队列为空的时候,则会抛出异常
- 而poll()只会返回null
30.哪些集合类是线程安全的?
- Vector:就比Arraylist多了个同步化机制(线程安全)。
- Hashtable:就比Hashmap多了个线程安全。
- Stack: 栈,也是线程安全的,继承于Vector。
- ConcurrentHashMap:是一种高效但是线程安全的集合
31.迭代器 Iterator 是什么?
- Iterator 是可以遍历集合的对象,为各种容器提供了公共的操作接口,隔离对容器的遍历操作和底层实现(解耦)。
- 缺点是增加新的集合类需要对应增加新的迭代器类,迭代器类与集合类也成对增加。
32.Iterator 怎么使用?有什么特点?
使用:
Iterator 接口源码中的方法:
- java.lang.Iterable 接口被 java.util.Collection 接口继承,java.util.Collection 接口的 iterator() 方法返回一个 Iterator 对象
- next() 方法获得集合中的下一个元素
- hasNext() 检查集合中是否还有元素
- remove() 方法将迭代器新返回的元素删除
//是否有下一个元素
boolean hasNext();
//下一个元素
E next();
//从迭代器指向的集合中删除迭代器返回的最后一个元素
default void remove() {
throw new UnsupportedOperationException("remove");
}
//遍历所有元素
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
33.Iterator 和 ListIterator 有什么区别?
ListIterator 继承 Iterator,且比 Iterator 有更多方法。
- add(E e) 将指定的元素插入列表,插入位置为迭代器当前位置之前
- set(E e) 迭代器返回的最后一个元素替换参数e
- hasPrevious() 迭代器当前位置,反向遍历集合是否含有元素
- previous() 迭代器当前位置,反向遍历集合,下一个元素
- previousIndex() 迭代器当前位置,反向遍历集合,返回下一个元素的下标
- nextIndex() 迭代器当前位置,返回下一个元素的下标
区别:
- 使用范围不同,Iterator可以迭代所有集合;ListIterator 只能用于List及其子类。
- ListIterator 有 add 方法,可以向 List 中添加对象;Iterator 不能
- ListIterator 有 set()方法,可以实现对 List 的修改;Iterator 仅能遍历,不能修改
- ListIterator 有 hasPrevious() 和 previous() 方法,可以实现逆向遍历;Iterator不能
- ListIterator 有 nextIndex() 和previousIndex() 方法,可定位当前索引的位置;Iterator不能
34.怎么确保一个集合不能被修改?
- 可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java.lang.UnsupportedOperationException 异常。
List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错
System. out. println(list. size());
- 同理:Collections包也提供了对list和set集合的方法。
- Collections.unmodifiableList(List)
- Collections.unmodifiableSet(Set)
三、多线程
35.并行和并发有什么区别?
- 并行(Parallel):当系统有一个以上CPU时,一个CPU执行一个进程,而另一个CPU可以执行另一个进程。两个进程互不抢占CPU资源,可以同时进行。
- 并发(Concurrent):多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,但从逻辑上来看那些任务是同时执行。
并发和并行的区别:
- 并行:多个事情,在同一时间点上同时发生了;且多个任务之间是不互相抢占资源的。
- 并发:多个事情,在同一时间段内同时发生了;且多个任务之间是互相抢占资源的。
- 只有在多CPU的情况中,才会发生并行。否则看似同时发生的事情,其实都是并发执行的。
36.线程和进程的区别?
- 根本区别:进程是一段正在执行的程序,是资源分配的基本单元;而线程是CPU调度(程序执行)的基本单元。
- 地址空间:进程有自己独立的地址空间(锁在房子里工作),系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段;线程没有独立的地址空间(在公共区工作),同一进程的线程共享本进程的地址空间。
- 资源拥有:进程之间的资源是独立的;同一进程内的线程共享本进程的资源。
- 系统开销:进程执行开销大,线程执行开销小。
拓展:
- 进程:是程序的一次执行过程,是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进程都有一个自己的地址空间,至少有 5 种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。
- 线程:是CPU调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
- 联系:线程是进程的一部分,一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
37.守护线程是什么?
概念:
- 守护线程:专门用于服务其他的线程,如果非守护线程(即用户自定义线程)都执行完毕,程序终止,那么jvm就会退出(即停止运行)——此时,连jvm都停止运行了,守护线程当然也就停止执行了。
- 反过来说,只要任何非守护线程还在运行,程序就不会终止。
- 换一种说法:如果有用户自定义线程存在的话,jvm就不会退出——此时,守护线程也不能退出,也就是它还要运行,干嘛呢,就是为了执行垃圾回收的任务。
拓展:
- 守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点。
- 通常来说,守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是你的首选。
- 在 Java 中垃圾回收线程就是特殊的守护线程。
- 其他的线程只有一种,那就是用户线程。(应用程序里的自定义线程)
- 应用程序里的线程,一般都是用户自定义线程。
- 用户也可以在应用程序代码自定义守护线程,只需要调用Thread类的设置方法设置一下即可。
总结:
- 只要任何非守护线程还在运行,程序就不会终止,守护线程会运行。
- 当 JVM 中不存在任何一个正在运行的非守护线程时,则 JVM 进程即会退出(官方解释)如果非守护线程(即用户自定义线程)都执行完毕,程序终止,守护线程也会终止。
- 守护线程用于又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭。
38.创建线程有哪几种方式?
主要有三种:
- 继承 Thread 重写 run 方法;
- 实现Runnable接口,重写 run 方法;
- 实现Callable接口,通过FutureTask包装器来创建Thread线程。
你更喜欢哪种方式?为什么?
- 实现Runnable接口这种方式更受欢迎,因为这不需要继承Thread类。在应用设计中已经继承了别的对象的情况下,这需要多继承(而Java不支持多继承),只能实现接口。同时,线程池也是非常高效的,很容易实现和使用。
一、继承Thread类创建线程类
- 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
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 == 50) {
new FirstThreadTest().start();
new FirstThreadTest().start();
}
}
}
}
二、通过Runnable接口创建线程类
- 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动该线程。
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();
}
}
}
}
三、通过Callable和Future创建线程
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
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<>(ctt);
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;
}
}
四、创建线程的三种方式的对比
1.1采用实现Runnable、Callable接口的方式创见多线程时,优势是:
- 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
- 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
1.2劣势是:
- 编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
2.1使用继承Thread类的方式创建多线程时优势是:
- 编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
2.2劣势是:
- 线程类已经继承了Thread类,所以不能再继承其他父类。
39.说一下 runnable 和 callable 有什么区别?
不同点:
- 两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;
- Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;
注意点:
- Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!
Callable接口
public interface Callable<V> {
V call() throws Exception;
}
Runnable接口
public interface Runnable {
public abstract void run();
}
Callable工作的Demo:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* Created on 2016/5/18.
*/
public class CallableImpl implements Callable<String> {
public CallableImpl(String acceptStr) {
this.acceptStr = acceptStr;
}
private String acceptStr;
@Override
public String call() throws Exception {
// 任务阻塞 1 秒
Thread.sleep(1000);
return this.acceptStr + " append some chars and return it!";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<String> callable = new CallableImpl("my callable test!");
FutureTask<String> task = new FutureTask<>(callable);
long beginTime = System.currentTimeMillis();
// 创建线程
new Thread(task).start();
// 调用get()阻塞主线程,反之,线程不会阻塞
String result = task.get();
long endTime = System.currentTimeMillis();
System.out.println("hello : " + result);
System.out.println("cast : " + (endTime - beginTime) / 1000 + " second!");
}
}
Runnable工作的Demo:
public class RunnableImpl implements Runnable {
public RunnableImpl(String acceptStr) {
this.acceptStr = acceptStr;
}
private String acceptStr;
@Override
public void run() {
try {
// 线程阻塞 1 秒,此时有异常产生,只能在方法内部消化,无法上抛
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 最终处理结果无法返回
System.out.println("hello : " + this.acceptStr);
}
public static void main(String[] args) {
Runnable runnable = new RunnableImpl("my runable test!");
long beginTime = System.currentTimeMillis();
new Thread(runnable).start();
long endTime = System.currentTimeMillis();
System.out.println("cast : " + (endTime - beginTime) / 1000 + " second!");
}
}
Callable的使用
import java.util.*;
import java.util.concurrent.*;
import static java.util.Arrays.asList;
public class Sums {
static class Sum implements Callable<Long> {
private final long from;
private final long to;
Sum(long from, long to) {
this.from = from;
this.to = to;
}
@Override
public Long call() {
long acc = 0;
for (long i = from; i <= to; i++) {
acc = acc + i;
}
System.out.println(Thread.currentThread().getName() + " : " + acc);
return acc;
}
}
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(3);
List<Future<Long>> results = executor.invokeAll(asList(
new Sum(0, 10), new Sum(0, 1_000), new Sum(0, 1_000_000)
));
executor.shutdown();
for (Future<Long> result : results) {
System.out.println(result.get());
}
}
}
40.线程有哪些状态?
线程从创建、运行到结束总是处于下面五个状态之一: 新建状态 、 就绪状态 、 运行状态 、 阻塞状态 、 死亡状态 。
新建状态(New):
- 当用new操作符创建一个线程时, 例如new Thread®,线程还没有开始运行,此时线程处在新建状态。当一个线程处于新生状态时,程序还没有开始运行线程中的代码。
就绪状态(Runnable):
- 一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。
- 处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。
运行状态(Running)
- 当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.
阻塞状态(Blocked)
- 阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
- 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
- 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
- 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
死亡状态(Dead)
- 有两个原因会导致线程死亡:
- run方法正常退出而自然死亡,
- 一个未捕获的异常终止了run方法而使线程猝死。
- 为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.
41.sleep() 和 wait() 有什么区别?
sleep()和wait()都是线程暂停执行的方法。
同步锁的对待不同:(最大区别)
- sleep()后,程序并不会释放同步锁。
- wait()后,程序会释放同步锁。 使得其他线程可以使用同步控制块或者方法。
用法的不同:
- sleep()可以用时间指定来使他自动醒过来。如果时间不到你只能调用interreput()来强行打断。
- wait()可以用notify()直接唤起。
- wait,notify,notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用.
属于不同的类属:
- sleep()的类是Thread。
- wait()的类是Object。
其实两者都可以让线程暂停一段时间,但是本质的区别是一个线程的运行状态控制,一个是线程之间的通讯的问题
- 在java.lang.Thread类中,提供了sleep(),
- 而java.lang.Object类中提供了wait(), notify()和notifyAll()方法来操作线程
- wait有两种形式wait()和wait(milliseconds).
- sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
synchronized(x){
x.notify()
//或者wait()
}
42.notify()和 notifyAll()有什么区别?
notify、notifyAll 的区别
- 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
- 当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
- 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
43.线程的 run()和 start()有什么区别?
- 调用 start() 方法是用来启动线程的,轮到该线程执行时,会自动调用 run();直接调用 run() 方法,无法达到启动多线程的目的,相当于主线程线性执行 Thread 对象的 run() 方法。
- 一个线程对线的 start() 方法只能调用一次,多次调用会抛出 java.lang.IllegalThreadStateException 异常;run() 方法没有限制。
44.创建线程池有哪几种方式?
newCachedThreadPool
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。
public class ThreadPoolExecutorTest {
public static void main(String[] args ) {
//可缓存的线程池,如果线程池的容量超过了任务数,自动回收空闲线程
ExecutorService cacheThreadPool =Executors.newCachedThreadPool();
//测试可缓存线程池
for(int i =1;i<=5;i++){
final int index=i ;
try{
Thread.sleep(1000);
}catch(InterruptedException e ) {
e.printStackTrace();
}
cacheThreadPool.execute(new Runnable(){
@Override
public void run() {
System.out.println("第" +index +"个线程" +Thread.currentThread().getName());
}
});
}
}
}
//输出结果
第1个线程pool-1-thread-1
第2个线程pool-1-thread-1
第3个线程pool-1-thread-1
第4个线程pool-1-thread-1
第5个线程pool-1-thread-1
由结果可看出 当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
newFixedThreadPool
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
public static ExecutorService newFixedThreadPool(int nThreads)
( nThreads - 池中的线程数 )
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
//定长线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程数量不再变化
ExecutorService fixedThreadPool =Executors. newFixedThreadPool(3);
//测试定长线程池,线程池的容量为3,提交5个任务,根据打印结果可以看出先执行前3个任务,3个任务结束后再执行后面的任务
for (int i =1; i<=5;i++){
final int index=i ;
fixedThreadPool.execute(new Runnable(){
@Override
public void run() {
try {
System.out.println("第" +index + "个线程" +Thread.currentThread().getName());
Thread.sleep(1000);
} catch(InterruptedException e ) {
e .printStackTrace();
}
}
});
}
}
}
由于设置最大线程数为3,所以在输出三个数后等待2秒后才继续输出。
newScheduledThreadPool
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
( corePoolSize - 池中所保存的线程数,即使线程是空闲的也包括在内。 )
第一个延迟执行示例代码:
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
scheduledThreadPool.schedule(newRunnable(){
@Override
public void run() {
System.out.println("延迟三秒");
}
}, 3, TimeUnit.SECONDS);
}
}
表示延迟3秒执行。
第二个定期执行示例代码:
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
//定长线程池,可执行周期性的任务
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
scheduledThreadPool.scheduleAtFixedRate(newRunnable(){
@Override
public void run() {
System.out.println("延迟1秒后每三秒执行一次");
}
},1,3,TimeUnit.SECONDS);
}
}
表示延迟1秒后每3秒执行一次。
newSingleThreadExecutor
创建一个使用单个 worker 线程的 Executor,以*队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newFixedThreadPool(1)不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
//单线程的线程池,线程异常结束,会创建一个新的线程,能确保任务按提交顺序执行
ExecutorService singleThreadPool= Executors.newSingleThreadExecutor();
//测试单线程的线程池
for(int i=1;i<=5;i++){
int index=i;
singleThreadPool.execute(new Runnable(){
@Override
public void run() {
try{
System.out.println("第"+index+"个线程");
Thread.sleep(2000);
}catch(InterruptedException e) {
e.printStackTrace();
}
} });
}
}
}
拓展:
一、线程池使用场景
- 单个任务处理时间短
- 将需处理的任务数量大
二、使用Java线程池好处
使用new Thread()创建线程的弊端:
- 每次通过new Thread()创建对象性能不佳。
- 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
- 缺乏更多功能,如定时执行、定期执行、线程中断。
使用Java线程池的好处:
- 重用存在的线程,减少对象创建、消亡的开销,提升性能。
- 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
- 提供定时执行、定期执行、单线程、并发数控制等功能。
45.线程池都有哪些状态?
线程池的5种状态:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED。
**RUNNING:
**线程池一旦被创建,就处于 RUNNING 状态,任务数为 0,能够接收新任务,对已排队的任务进行处理。
**SHUTDOWN:
**不接收新任务,但能处理已排队的任务。调用线程池的 shutdown() 方法,线程池由 RUNNING 转变为 SHUTDOWN 状态。
**STOP:
**不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。调用线程池的 shutdownNow() 方法,线程池由(RUNNING 或 SHUTDOWN ) 转变为 STOP 状态。
TIDYING:
- SHUTDOWN 状态下,任务数为 0, 其他所有任务已终止,线程池会变为 TIDYING 状态,会执行 terminated() 方法。线程池中的 terminated() 方法是空实现,可以重写该方法进行相应的处理。
- 线程池在 SHUTDOWN 状态,任务队列为空且执行中任务为空,线程池就会由 SHUTDOWN 转变为 TIDYING 状态。
- 线程池在 STOP 状态,线程池中执行中任务为空时,就会由 STOP 转变为 TIDYING 状态。
**TERMINATED:
**线程池彻底终止。线程池在 TIDYING 状态执行完 terminated() 方法就会由 TIDYING 转变为 TERMINATED 状态。
状态转换如图
46.线程池中 submit()和 execute()方法有什么区别?
submit() 和 execute()都可以开启线程执行池中的任务。但是 submit()可以提交指定的任务去执行并且返回Future对象,即执行的结果。
区别:
1.返回值不一样
- submit有Future < T > 类型的返回值,而execute没有
2.submit方便Exception处理
- 感知这些exception并做出及时的处理,那么就需要用到submit,通过捕获Future.get抛出的异常。
public class MainTest {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(10);
Future submit = pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("submit");
System.out.println(0/0);
}
});
try {
System.out.println("result=" + submit.get());
}catch (Exception e){
System.out.println(e);
}
}
}
47.在 java 程序中怎么保证多线程的运行安全?
- 使用synchronied关键字,可以用于代码块,方法(静态方法,同步锁是当前字节码对象;实例方法,同步锁是实例对象)
- 使用volatile 关键字,防止指令重排,被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量
- lock锁机制
- 使用线程安全的类,比如Vector、HashTable、StringBuffer
49.什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
死锁发生的原因?
虽然进程在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件。
- 互斥条件:即当资源被一个线程使用(占有)时,别的线程不能使用
- 请求和保持条件:即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
- 环路等待条件:指在发生死锁时,存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
50.怎么防止死锁?
怎么防止死锁
- 尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
- 尽量使用 Java. util. concurrent 并发类代替自己手写锁。
- 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
- 尽量减少同步的代码块。
打破四个必要条件之一
- 打破互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。
- 打破不可抢占条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。
- 打破占有且申请条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。
- 打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式
四、反射
57.什么是反射?
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性,这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
Class类
这个类用于操纵(反向解析)一个类的属性,方法,构造器等。
Person: name,age,address(Class只要知道你这个Person类,那么它便可知道你所有的属性,不止属性,甚至方法上的注解等等,都会知道。)
范例:获取一个Class对象
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
public class Test01 {
public static void main(String[] args) throws ClassNotFoundException {
String a = "java.lang.String";
// String a = "java.lang.HashMap";
// 根据一类的全名字符串来获得一个类的类对象
Class<?> clazz = Class.forName(a);
// 获得传递过来的类的所有方法
Method[] methods = clazz.getDeclaredMethods();
// String s = Arrays.toString(methods);
for (Method m: methods) {
System.out.println(m);
}
System.out.println("------------------------------------------");
// 获得类的所有属性
Field[] declaredFields = clazz.getDeclaredFields();
for(Field d : declaredFields) {
System.out.println(d);
}
System.out.println("-------------------------------------");
// 获得类的所有构造器
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
for (Constructor c : constructors) {
System.out.println(c);
}
}
}
什么是反射,反射能干嘛?
- 反射是:指程序可以访问、检测和修改它本身状态或行为的一种能力
我们平时用反射主要做:
- 获取类型的相关信息
- 动态调用方法
- 动态构造对象
- 从程序集中获得类型。
我们获取已加载程序集中类型的Type对象的几种方法:(以StringBuilder 类型为例)
- 直接使用typeof操作符 Type T1 = typeof(StringBuilder);
- 通过类型实例 Type T2 = new StringBuilder().GetType();
- 通过Type类的静态方法 Type T3 = Type.GetType(“System.IO.Stream”);
58.什么是 java 序列化?什么情况下需要序列化?
序列化:将 Java 对象转换成字节流的过程。
Java 序列化是为了保存各种对象在内存中的状态,并且可以把保存的对象状态再读出来。
以下情况需要使用 Java 序列化:
- Java 对象需要在网络上传输
- Java 对象需要持久化存储到文件中
59.动态代理是什么?有哪些应用?
动态代理:在运行时,创建目标类,可以调用和扩展目标类的方法。
应用场景如:
- 统计每个 api 的请求耗时
- 统一的日志输出
- 校验被调用的 api 是否已经登录和权限鉴定
- Spring的 AOP 功能模块就是采用动态代理的机制来实现切面编程
五、对象拷贝
61.为什么要使用克隆?
想对一个对象进行处理,又想保留原有的数据进行接下来的操作,就需要克隆了。
克隆分浅克隆和深克隆
浅克隆后的对象中非基本对象和原对象指向同一块内存,因此对这些非基本对象的修改会同时更改克隆前后的对象。
深克隆可以实现完全的克隆,可以用反射的方式或序列化的方式实现。
62.如何实现对象克隆?
有两种方式:
1.实现 Cloneable
接口并重写 Object 类中的 clone() 方法;
2.实现 Serializable
接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。
实现 Cloneable 接口,重写 clone() 方法。
不实现 Cloneable 接口,会报 CloneNotSupportedException 异常。
public class TestClone {
public static void main(String[] args) throws CloneNotSupportedException {
Person p1 = new Person(1, "ConstXiong");//创建对象 Person p1
Person p2 = (Person)p1.clone();//克隆对象 p1
p1.setName("其不答");//修改 p2的name属性,p1的name未变
System.out.println(p1);
System.out.println(p2);
}
}
class Person implements Cloneable {
private int pid;
private String name;
public Person(int pid, String name) {
this.pid = pid;
this.name = name;
System.out.println("Person constructor call");
}
public int getPid() {
return pid;
}
public void setPid(int pid) {
this.pid = pid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "Person [pid:"+pid+", name:"+name+"]";
}
}
打印输出
Person constructor call
Person [pid:1, name:其不答]
Person [pid:1, name:其不答]
Object 的 clone() 方法是浅拷贝,即如果类中属性有自定义引用类型,只拷贝引用,不拷贝引用指向的对象。
方法2、结合序列化(JDK java.io.Serializable 接口、JSON格式、XML格式等),完成深拷贝
结合 java.io.Serializable 接口,完成深拷贝
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class TestSeriazableClone {
public static void main(String[] args) {
SPerson p1 = new SPerson(1, "ConstXiong", new SFood("米饭"));//创建 SPerson 对象 p1
SPerson p2 = (SPerson)p1.cloneBySerializable();//克隆 p1
p2.setName("其不答");//修改 p2 的 name 属性
p2.getFood().setName("面条");//修改 p2 的自定义引用类型 food 属性
System.out.println(p1);//修改 p2 的自定义引用类型 food 属性被改变,p1的自定义引用类型 food 属性未随之改变,说明p2的food属性,只拷贝了引用和 food 对象
System.out.println(p2);
}
}
class SPerson implements Cloneable, Serializable {
private static final long serialVersionUID = -7710144514831611031L;
private int pid;
private String name;
private SFood food;
public SPerson(int pid, String name, SFood food) {
this.pid = pid;
this.name = name;
this.food = food;
System.out.println("Person constructor call");
}
public int getPid() {
return pid;
}
public void setPid(int pid) {
this.pid = pid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
/**
* 通过序列化完成克隆
* @return
*/
public Object cloneBySerializable() {
Object obj = null;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(this);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
obj = ois.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return obj;
}
@Override
public String toString() {
return "Person [pid:"+pid+", name:"+name+", food:"+food.getName()+"]";
}
public SFood getFood() {
return food;
}
public void setFood(SFood food) {
this.food = food;
}
}
class SFood implements Serializable {
private static final long serialVersionUID = -3443815804346831432L;
private String name;
public SFood(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
63.深拷贝和浅拷贝区别是什么?
浅拷贝:复制基本类型的属性;引用类型的属性复制,复制栈中的变量 和 变量指向堆内存中的对象的指针,不复制堆内存中的对象。
深拷贝:复制基本类型的属性;引用类型的属性复制,复制栈中的变量 和 变量指向堆内存中的对象的指针和堆内存中的对象。
六、Java Web
64.jsp 和 servlet 有什么区别?
Servlet
- 一种服务器端的Java应用程序
- 由 Web 容器加载和管理
- 用于生成动态 Web 内容
- 负责处理客户端请求
Jsp(Java Server Pages)
- 是 Servlet 的扩展,本质上还是 Servlet
- 每个 Jsp 页面就是一个 Servlet 实例
- Jsp 页面会被 Web 容器编译成 Servlet,Servlet 再负责响应用户请求
区别
- jsp更擅长表现于页面显示,servlet更擅长于逻辑控制.
- Servlet中没有内置对象,Jsp中的内置对象都是必须通过HttpServletRequest对象,HttpServletResponse对象以及HttpServlet对象得到.
- Servlet的实现方式是在java代码中嵌入HTML代码,编写和修改HTML非常不方便,所以适合做流程控制和业务逻辑的处理;JSP实现的方式是在HTML中嵌入java代码,比较适合页面的显示
65.jsp 有哪些内置对象?作用分别是什么?
JSP 有 9 大内置对象:
-
request
:封装客户端的请求,其中包含来自 get 或 post 请求的参数; -
response
:封装服务器对客户端的响应; -
pageContext
:通过该对象可以获取其他对象; -
session
:封装用户会话的对象; -
application
:封装服务器运行环境的对象; -
out
:输出服务器响应的输出流对象; -
config
:Web 应用的配置对象; -
page
:JSP 页面本身(相当于 Java 程序中的 this); -
exception
:封装页面抛出异常的对象。
一共有九个内置对象
pageContextjavax.servlet.jsp.PageContext
requestjavax.servlet.http.HttpServletRequest
responsejavax.servlet.http.HttpServletResponse
sessionjavax.servlet.http.HttpSession
applicationjavax.servlet.Servlet Context–>可用this.getServletContext()替代
configjavax.servlet.ServletConfig
exceptionjava.lang.Throwable
pagejava.lang.Object
outjavax.servlet.jsp.JspWriter
作用:
1、pageContext表示页容器–>EL、标签、上传
2、request服务器端取得客户端的信息:头信息、Cookie、请求参数、MVC设计模式
3、response服务器端回应给客户端信息:Cookie、重定向
4、session表示每一个用户,用于登录验证上
5、application表示整个服务器,getRealPath()
6、config去的初始化参数,初始化参数在web.xml中配置
7、exception表示的是错误页的处理操作
8、page如同this一样,表示整个JSP页面
9、out输出,但是尽量使用表达式输出
66.说一下 jsp 的 4 种作用域?
首先要声明一点,所谓“作用域”就是“信息共享的范围”,也就是说一个信息能够在多大的范围内有效。
4个JSP内置对象的作用域分别为:page(页面作用域)、request(请求作用域)、session(会话作用域)、application(应用程序作用域)。
JSP内置对象作用域表如下:
名称 | 作用域 |
---|---|
application | 在所有应用程序中有效 |
session | 在当前会话中有效 |
request | 在当前请求中有效 |
page | 在当前页面有效 |
67.session 和 cookie 有什么区别?
cookie与session的区别有:
cookie以文本格式存储在浏览器上,存储量有限;
而会话存储在服务端,可以无限量存储多个变量并且比cookie更安全
1、存储位置不同
-
cookie的数据信息存放在客户端浏览器上。
-
session的数据信息存放在服务器上。
2、存储容量不同
-
单个cookie保存的数据<=4KB,一个站点最多保存20个Cookie。
-
对于session来说并没有上限,但出于对服务器端的性能考虑,session内不要存放过多的东西,并且设置session删除机制。
3、存储方式不同
-
cookie中只能保管ASCII字符串,并需要通过编码方式存储为Unicode字符或者二进制数据。
-
session中能够存储任何类型的数据,包括且不限于string,integer,list,map等。
4、隐私策略不同
-
cookie对客户端是可见的,别有用心的人可以分析存放在本地的cookie并进行cookie欺骗,所以它是不安全的。
-
session存储在服务器上,对客户端是透明,不存在敏感信息泄漏的风险。
5、有效期上不同
-
开发可以通过设置cookie的属性,达到使cookie长期有效的效果。
-
session依赖于名为JSESSIONID的cookie,而cookie JSESSIONID的过期时间默认为-1,只需关闭窗口该session就会失效,因而session不能达到长期有效的效果。
6、服务器压力不同
-
cookie保管在客户端,不占用服务器资源。对于并发用户十分多的网站,cookie是很好的选择。
-
session是保管在服务器端的,每个用户都会产生一个session。假如并发访问的用户十分多,会产生十分多的session,耗费大量的内存。
7、浏览器支持不同
假如客户端浏览器不支持cookie:
-
cookie是需要客户端浏览器支持的,假如客户端禁用了cookie,或者不支持cookie,则会话跟踪会失效。关于WAP上的应用,常规的cookie就派不上用场了。
-
运用session需要使用URL地址重写的方式。一切用到session程序的URL都要进行URL地址重写,否则session会话跟踪还会失效。
假如客户端支持cookie:
-
cookie既能够设为本浏览器窗口以及子窗口内有效,也能够设为一切窗口内有效。
-
session只能在本窗口以及子窗口内有效。
8、跨域支持上不同
-
cookie支持跨域名访问。
-
session不支持跨域名访问。
68.说一下 session 的工作原理?
session 的工作原理是客户端登录完成之后,服务器会创建对应的 session,session 创建完之后,会把 session 的 id 发送给客户端,客户端再存储到浏览器中。这样客户端每次访问服务器时,都会带着 sessionid,服务器拿到 sessionid 之后,在内存找到与之对应的 session 这样就可以正常工作了。
69.如果客户端禁止 cookie 能实现 session 还能用吗?
如果客户端禁用了Cookie,那PHPSESSIONID都无法写入客户端,Session还能用?
- 答案显而易见:不能
- 并且服务端因为没有得到PHPSESSIONID的cookie,会不停的生成session_id文件
但是可以通过其他方法在禁用 cookie 的情况下,可以继续使用session。
- 通过url重写,把 sessionid 作为参数追加的原 url 中,后续的浏览器与服务器交互中携带 sessionid 参数。
- 服务器的返回数据中包含 sessionid,浏览器发送请求时,携带 sessionid 参数。
- 通过 Http 协议其他 header 字段,服务器每次返回时设置该 header 字段信息,浏览器中 js 读取该 header 字段,请求服务器时,js设置携带该 header 字段。
70.spring mvc 和 struts 的区别是什么?
一、拦截机制的不同
-
Struts2是类级别的拦截。
-
每次请求就会创建一个Action,和Spring整合时Struts2的ActionBean注入作用域是原型模式prototype,然后通过setter,getter吧request数据注入到属性。Struts2中,一个Action对应一个request,response上下文,在接收参数时,可以通过属性接收,这说明属性参数是让多个方法共享的。Struts2中Action的一个方法可以对应一个url,而其类属性却被所有方法共享,这也就无法用注解或其他方式标识其所属方法了,只能设计为多例。
-
SpringMVC是方法级别的拦截。
-
一个方法对应一个Request上下文,所以方法直接基本上是独立的,独享request,response数据。而每个方法同时又何一个url对应,参数的传递是直接注入到方法中的,是方法所独有的。处理结果通过ModeMap返回给框架。在Spring整合时,SpringMVC的Controller Bean默认单例模式Singleton,所以默认对所有的请求,只会创建一个Controller,有应为没有共享的属性,所以是线程安全的,如果要改变默认的作用域,需要添加@Scope注解修改。
Struts2有自己的拦截Interceptor机制,SpringMVC这是用的是独立的Aop方式,这样导致Struts2的配置文件量还是比SpringMVC大。
二、底层框架的不同
-
Struts2采用Filter(StrutsPrepareAndExecuteFilter)实现。Filter在容器启动之后即初始化;服务停止以后坠毁,晚于Servlet。
-
SpringMVC(DispatcherServlet)则采用Servlet实现。Servlet在是在调用时初始化,先于Filter调用,服务停止后销毁。
三、性能方面
-
Struts2是类级别的拦截,每次请求对应实例一个新的Action,需要加载所有的属性值注入,SpringMVC实现了零配置,
-
由于SpringMVC基于方法的拦截,有加载一次单例模式bean注入。所以,SpringMVC开发效率和性能高于Struts2。
71.如何避免 sql 注入?
- 推荐用 PreparedStatement 。他是 Statement 的子类。
两者相比较, PreparedStatement 的优势是:- 代码的可读性更高
- 代码的效率更高
- 安全性更高
七、异常
74.throw 和 throws 的区别?
throw:
- 表示方法内抛出某种异常对象
- 如果异常对象是非 RuntimeException 则需要在方法申明时加上该异常的抛出 即需要加上 throws 语句 或者 在方法体内 try catch 处理该异常,否则编译报错
- 执行到 throw 语句则后面的语句块不再执行
throws:
- 方法的定义上使用 throws 表示这个方法可能抛出某种异常
- 需要由方法的调用者进行异常处理
75.final、finally、finalize 有什么区别?
- final用于声明属性,方法和类,分别表示属性不可交变,方法不可覆盖,类不可继承。
- finally是异常处理语句结构的一部分,表示总是执行。
- finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,供垃圾收集时的其他资源回收,例如关闭文件等。
76.try-catch-finally 中哪个部分可以省略?
- 以下三种情况都是可以的:
- try-catch
- try-finally
- try-catch-finally
- 可以省略catch或者finally。catch和finally不可以同时省略。
77.try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?
finally 一定会执行,即使是 catch 中 return 了,catch 中的 return 会等 finally 中的代码执行完之后,才会执行。
try-catch-finally中return的执行情况:
- 在try中没有异常的情况下try、catch、finally的执行顺序 try — finally
- 如果try中有异常,执行顺序是try — catch — finally
- 如果try中没有异常并且try中有return这时候正常执行顺序是try ---- finally — return
- 如果try中有异常并且try中有return这时候正常执行顺序是try----catch—finally— return
总之 finally 永远执行
九、设计模式
89.简单工厂和抽象工厂有什么区别?
-
简单工厂模式
- 是由一个工厂对象创建产品实例,简单工厂模式的工厂类一般是使用静态方法,通过不同的参数的创建不同的对象实例
- 可以生产结构中的任意产品,不能增加新的产品
-
抽象工厂模式
- 提供一个创建一系列相关或相互依赖对象的接口,而无需制定他们具体的类,生产多个系列产品
生产不同产品族的全部产品,不能新增产品,可以新增产品族
- 提供一个创建一系列相关或相互依赖对象的接口,而无需制定他们具体的类,生产多个系列产品
十、Spring/Spring MVC
90.为什么要使用 spring?
- 方便解耦,便于开发(Spring就是一个大工厂,可以将所有对象的创建和依赖关系维护都交给spring管理)
- spring支持aop编程(spring提供面向切面编程,可以很方便的实现对程序进行权限拦截和运行监控等功能)
- 声明式事务的支持(通过配置就完成对事务的支持,不需要手动编程)
- 方便程序的测试,spring 对junit4支持,可以通过注解方便的测试spring 程序
- 方便集成各种优秀的框架()
- 降低javaEE API的使用难度(Spring 对javaEE开发中非常难用的一些API 例如JDBC,javaMail,远程调用等,都提供了封装,是这些API应用难度大大降低)
91.解释一下什么是 aop?
AOP:Aspect Oriented Programming,面向切面编程。
通过预编译和运行期动态代理实现程序功能的统一维护。
AOP 利用一种称为横切的技术,剖开对象的封装,并将影响多个类的公共行为封装到一个可重用模块,组成一个切面,即 Aspect 。
"切面"就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,利于可操作性和可维护性。
实现 AOP 的方式,主要有两大类:
- 采用动态代理技术,利用拦截方法的方式,对该方法进行装饰,以取代原有对象行为的执行;
- 采用静态织入的方式,引入特定的语法创建"切面",从而使得编译器可以在编译期间织入有关"切面"的代码。
AOP 相关概念
切面(Aspect)、连接点(Join point)、通知(Advice)、切点(Pointcut)、引入(Introduction)、目标对象(Target Object)、AOP代理(AOP Proxy)、织入(Weaving)
92.解释一下什么是 ioc?
IoC,Inversion of Control(控制反转)。
是一种设计思想,在Java开发中,将你设计好的对象交给容器控制,而不是显示地用代码进行对象的创建。
把创建和查找依赖对象的控制权交给 IoC 容器,由 IoC 容器进行注入、组合对象。这样对象与对象之间是松耦合、便于测试、功能可复用(减少对象的创建和内存消耗),使得程序的整个体系结构可维护性、灵活性、扩展性变高。
使用 IoC 的好处:
- 资源集中管理,实现资源的可配置和易管理
- 降低了资源的依赖程度,即松耦合
- 便于测试
- 功能可复用(减少对象的创建和内存消耗)
- 使得程序的整个体系结构可维护性、灵活性、扩展性变高
DI(Dependency Injection)依赖注入,是 IoC 容器装配、注入对象的一种方式。
通过依赖注入机制,简单的配置即可注入需要的资源,完成自身的业务逻辑,不需要关心资源的出处和具体实现。
93.spring 有哪些主要模块?
Spring Core
框架的最基础部分,提供 IoC 容器,对 bean 进行管理。
Spring Context
基于 bean,提供上下文信息,扩展出JNDI、EJB、电子邮件、国际化、校验和调度等功能。
Spring DAO
提供了JDBC的抽象层,它可消除冗长的JDBC编码和解析数据库厂商特有的错误代码,还提供了声明性事务管理方法。
Spring ORM
提供了常用的“对象/关系”映射APIs的集成层。 其中包括JPA、JDO、Hibernate、MyBatis 等。
Spring AOP
提供了符合AOP Alliance规范的面向方面的编程实现。
Spring Web
提供了基础的 Web 开发的上下文信息,可与其他 web 进行集成。
Spring Web MVC
提供了 Web 应用的 Model-View-Controller 全功能实现。
94.spring 常用的注入方式有哪些?
1、xml中配置
- bean 的申明、注册
节点注册 bean
节点的 factory-bean 参数指工厂 bean,factory-method 参数指定工厂方法
- bean 的注入
节点使用 set 方式注入
节点使用 构造方法注入
a) + ,set方法注入
b) 修改为 配置文件和class Person, + 节点使用 构造方法注入
c) 节点 factory-method 参数指定静态工厂方法
2、注解
- bean 的申明、注册
@Component //注册所有bean
@Controller //注册控制层的bean
@Service //注册服务层的bean
@Repository //注册dao层的bean
- bean 的注入
@Autowired 作用于 构造方法、字段、方法,常用于成员变量字段之上。
@Autowired + @Qualifier 注入,指定 bean 的名称
@Resource JDK 自带注解注入,可以指定 bean 的名称和类型等
95.spring 中的 bean 是线程安全的吗?
spring 管理的 bean 的线程安全跟 bean 的创建作用域和 bean 所在的使用环境是否存在竞态条件有关,spring 并不能保证 bean 的线程安全。
96.spring 支持几种 bean 的作用域?
- singleton:单例模式,在整个Spring IoC容器中,使用 singleton 定义的 bean 只有一个实例
- prototype:原型模式,每次通过容器的getbean方法获取 prototype 定义的 bean 时,都产生一个新的 bean 实例
只有在 Web 应用中使用Spring时,request、session、global-session 作用域才有效
- request:对于每次 HTTP 请求,使用 request 定义的 bean 都将产生一个新实例,即每次 HTTP 请求将会产生不同的 bean 实例。
- session:同一个 Session 共享一个 bean 实例。
- global-session:同 session 作用域不同的是,所有的Session共享一个Bean实例。
97.spring 自动装配 bean 有哪些方式?
spring 配置文件中 节点的 autowire 参数可以控制 bean 自动装配的方式
- default - 默认的方式和 “no” 方式一样
- no - 不自动装配,需要使用 节点或参数
- byName - 根据名称进行装配
- byType - 根据类型进行装配
- constructor - 根据构造函数进行装配
98.spring 事务实现方式有哪些?
100.说一下 spring mvc 运行流程?
1、在 web 项目的 web.xml 文件配置 DispatcherServlet,启动 web 项目完成初始化动作
2、http 请求到 DispatcherServlet
3、根据 HttpServletRequest 查找 HandlerExecutionChain
4、根据 HandlerExecutionChain 获取 HandlerAdapter、执行 handler
5、Handler 执行完成返回 ModelAndView
6、DispatcherServlet 进行结合异常处理 ModelAndView
7、DispatcherServlet 进行视图渲染,将 Model 数据在 View 中填充
8、DispatcherServlet 返回结果
aaa@qq.com 的作用是什么?
@RequestMapping 是一个注解,用来标识 http 请求地址与 Controller 类的方法之间的映射。
可作用于类和方法上,方法匹配的完整是路径是 Controller 类上 @RequestMapping 注解的 value 值加上方法上的 @RequestMapping 注解的 value 值。
aaa@qq.com 的作用是什么?
@Autowired 是一个注释,它可以对类成员变量、方法及构造函数进行标注,让 spring 完成 bean 自动装配的工作。
@Autowired 默认是按照类去匹配,配合 @Qualifier 指定按照名称去装配 bean。
十一、Spring Boot/Spring Cloud
104.什么是 spring boot/为什么要用?
Spring Boot 其设计目的是用来简化Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。
springboot(微框架) = springmvc(控制器) + springcore(项目管理)
- 创建独立的Spring应用程序
- 嵌入的Tomcat,无需部署WAR文件
- 简化Maven配置
- 自动配置Spring,没有XML配置
106.spring boot 核心配置文件是什么?
- Spring Boot 有两种类型的配置文件,application 和 bootstrap 文件
- Spring Boot会自动加载classpath目前下的这两个文件,文件格式为 properties 或 yml 格式
*.properties
文件是 key=value 的形式*.yml
是 key: value 的形式*.yml
加载的属性是有顺序的,但不支持 @PropertySource 注解来导入配置,一般推荐用yml文件,看下来更加形象
- bootstrap 配置文件是系统级别的,用来加载外部配置,如配置中心的配置信息,也可以用来定义系统不会变化的属性.bootstatp 文件的加载先于application文件
- application 配置文件是应用级别的,是当前应用的配置文件
108.spring boot 有哪些方式可以实现热部署?
- spring Loaded
- spring-boot-devtools
- JRebel插件
110.什么是 spring cloud?
是在Spring Boot 基础上构建的,用于快速构建分布式系统的通用模式的工具集.
Spring Cloud有以下特点:
- 约定优于配置
- 适用于各种环境.开发,部署在PC Server 或各种云环境均可
- 隐藏了组件的复杂性,并提供声明式,无xml的配置方式
- 开箱即用
- 轻量级的组件. Spring Cloud整合的组件大多比较轻量.例如Eureka,Zuul,等等
- 组件丰富,功能齐全. Spring Cloud 为微服务架构提供了非常完整的支持.例如,配置管理,服务发现,断路器,微服务网关等;
- 选型中立,丰富. 例如,Spring Cloud 支持使用Eureka,Zookeeper或Consul实现服务发现.
- 灵活.Spring Cloud的组成部分是解耦的,开发人员可按需灵活挑选技术选型.
111.spring cloud 的核心组件有哪些?
**Eureka
**是微服务架构中的注册中心,专门负责服务的注册与发现。
- Eureka **Client:**负责将这个服务的信息注册到Eureka Server中
- **Eureka Server:**注册中心,里面有一个注册表,保存了各个服务所在的机器和端口号
**Feign
**是声明式的web service客户端,它让微服务之间的调用变得更简单了,类似controller调用service。Spring Cloud集成了Ribbon和Eureka,可在使用Feign时提供负载均衡的http客户端。
-
Feign的一个关键机制就是使用了动态代理
- 首先,如果你对某个接口定义了@FeignClient注解,Feign就会针对这个接口创建一个动态代理
- 接着你要是调用那个接口,本质就是会调用 Feign创建的动态代理,这是核心中的核心
- Feign的动态代理会根据你在接口上的@RequestMapping等注解,来动态构造出你要请求的服务的地址
- 最后针对这个地址,发起请求、解析响应
**Ribbon
**它的作用是负载均衡,会帮你在每次请求时选择一台机器,均匀的把请求分发到各个机器上
- Ribbon的负载均衡默认使用的最经典的Round Robin轮询算法
此外,Ribbon是和Feign以及Eureka紧密协作,完成工作的,具体如下:
- 首先Ribbon会从 Eureka Client里获取到对应的服务注册表,也就知道了所有的服务都部署在了哪些机器上,在监听哪些端口号。
- 然后Ribbon就可以使用默认的Round Robin算法,从中选择一台机器
- Feign就会针对这台机器,构造并发起请求。
Hystrix
(断路器)Hystrix是隔离、熔断以及降级的一个框架 啥意思呢?说白了,Hystrix会搞很多个小小的线程池,比如订单服务请求库存服务是一个线程池,请求仓储服务是一个线程池,请求积分服务是一个线程池。每个线程池里的线程就仅仅用于请求那个服务。
**Zuul
**也就是微服务网关。这个组件是负责网络路由的。
总结:
最后再来总结一下,上述几个Spring Cloud核心组件,在微服务架构中,分别扮演的角色:
- Eureka:各个服务启动时,Eureka Client都会将服务注册到Eureka Server,并且Eureka Client还可以反过来从Eureka Server拉取注册表,从而知道其他服务在哪里
- Ribbon:服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台
- Feign:基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求
- Hystrix:发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题
- Zuul:如果前端、移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给对应的服务
以上就是我们通过一个电商业务场景,阐述了Spring Cloud微服务架构几个核心组件的底层原理。
十三、Mybatis
125.介绍一下MyBatis
MyBatis 是一款优秀的持久层框架。
- 支持自定义 SQL、存储过程以及高级映射
- 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作
- 通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录
126.Mybaits 的优缺点
优点:
- 消除 JDBC 中的重复代码
- 可以在 XML 或注解中直接编写 SQL 语句,比较灵活,方便对 SQL 的优化与调整
- SQL 写在 XML 中,与代码解耦,按照对应关系方便管理
- XML 中提供了动态 SQL 的标签,方便根据条件拼接 SQL
- 提供了 XML、注解与 Java 对象的映射机制
- 与 Spring 集成比较方便
缺点:
- 字段较多、关联表多时,编写 SQL 工作量较大
- SQL 语句依赖了数据库特性,会导致程序的移植性较差,切换数据库困难
127.MyBatis 与 Hibernate 的区别
- MyBatis 不完全是一个 ORM 框架,它需要程序员自己编写 SQL;Hibernate 可以做到无 SQL 对数据库进行操作
- MyBatis 直接编写原生 SQL,可以严格控制 SQL 执行性能,灵活度高,快速响应需求变化;Hibernate 会根据模型配置自动生成和执行 SQL 语句,面对多变的需求,灵活度没那么高
- MyBatis 书写 SQL 可能依赖数据库特性,导致应用程序数据库一致性差;Hibernate 可以屏蔽掉数据库差异,数据库一致性好
- MyBatis 考验程序编写 SQL 的功底,编写大量 SQL,效率可能不高;Hibernate 对象关系映射能力强,可以节省很多代码,提高开发效率
- MyBatis 没法根据模型自动初始化数据库中的表;Hibernate 是根据模型的配置生成 DDL 语句在数据库中自动初始化对应表、索引、序列等
128.mybatis 中 #{}和 ${}的区别是什么?
- MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为 ?,预编译 SQL,通过 PreparedStatement 的 setXxxx 的方法进行参数赋值。使用 #{} 可以有效地防止 SQL 注入。
- MyBatis 在处理${} 时,会直接把 ${} 替换为参数值,存在 SQL 注入的风险。
- #{} 比 ${} 安全,但还是提供了 ${} 这种动态替换参数的方式,是因为有些复杂的 SQL 使用场景通过预编译的方式比较麻烦,且在代码中完全可以做到控制非法参数,有些参数可能是一些常量或字段值。
129.MyBatis 有哪些分页的方式?分页插件的原理是什么?
- 使用 RowBounds 对象进行分页,它是对 ResultSet 结果集进行内存分页
- 在 xml 或者 注解的 SQL 中传递分页参数
- 使用分页插件 Mybatis-PageHelper
其中分页插件的原理是,使用 MyBatis 提供的插件接口,拦截待执行的 SQL,根据数据库种类的配置与分页参数,生成带分页 SQL 语句,执行。
130.RowBounds 是一次性查询全部结果吗?为什么?
查询增加RowBounds限制查询条数,默认是0到1000条
131.mybatis 逻辑分页和物理分页的区别是什么?
- 逻辑分页 内存开销比较大,在数据量比较小的情况下效率比物理分页高;在数据量很大的情况下,内存开销过大,容易内存溢出,不建议使用
- 物理分页 内存开销比较小,在数据量比较小的情况下效率比逻辑分页还是低,在数据量很大的情况下,建议使用物理分页
132.mybatis 有哪些执行器(Executor)?
SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。简言之,就是重复使用Statement对象。
BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。
十六、Zookeeper
157.zookeeper 是什么?
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
158.zookeeper 都有哪些功能?
- 集群管理:监控节点存货状态,运行请求等
- 主节点选举:主节点挂了,Zookeeper协助选取一个新的主节点
- 分布式锁:Zookeeper可以对分布式锁进行控制
- Zookeeper提供两种锁:
独占锁:独占锁是只有一个线程使用资源
共享锁:共享锁是读锁共享,读写互斥
- Zookeeper提供两种锁:
- 命名服务:在分布式系统中,通过使用命名服务,客户端应用能够很快根据指定 名字来获取资源或者服务的路径等
159.zookeeper 有几种部署模式?
- 单机部署:一台集群上部署
- 集群部署:多台集群运行
- 伪集群部署:一台集群启动多个Zookeeper实例运行
160.zookeeper 怎么保证主从节点的状态同步?
Zookeeper的核心是原子广播,这个机制保证了各个server之间的同步,实现这 个机制的协议叫做zab协议。zab协议有两种模式,分别是恢复模式和广播模式。当服 务启动或在领导者崩溃后,zab就进入了恢复模式,当领导者被选举出来,并且大多 数server完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了 leader和server具有相同的系统状态。
161.集群中为什么要有主节点?
在分布式环境中,有些业务逻辑只需要集群中的某一台机器进行执行即可,其他的机器可以共享这个结果,这样就可以大大减少重复计算,提高性能,所以说集群中就需要有主节点。
162.集群中有 3 台服务器,其中一个节点宕机,这个时候 zookeeper 还可以使用吗?
可以使用,单数服务器只要没超过一半的服务器宕机就可以继续使用。
当然如果大于一半的服务器宕机就不可用了。
163.说一下 zookeeper 的通知机制?
在 ZooKeeper 中,引入了 Watcher 机制来实现这种分布式的通知功能。ZooKeeper 允许客户端向服务端注册一个 Watcher 监听,当服务器的一些特定事件触发了这个 Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能。
十七、MySql
164.数据库的三范式是什么?
第一范式(1NF):
数据表中的每一列(每个字段)必须是不可拆分的最小单元,也就是确保每一列的原子性;
例如:用户表1中的用户信息字段可以拆分为用户表2中的地址和电话字段,而用户表2中的地址字段还可以拆分为用户表3中的省份和市区字段。
第二范式(2NF):
满足1NF后,要求表中的所有列,都必须依赖于主键,而不能有任何一列与主键没有关系,也就是说一个表只描述一件事情;
例如:产品编号与订单编号并没有明确的关系,订购日期与订单编号有关系,因为一旦订单编号确定下来了,订购日期也确定了,价格与订单编号也没有直接关系,而与产品有关,所以上面的表实际上可以拆分:
第三范式(3NF):
必须先满足第二范式(2NF),要求:表中的每一列只与主键直接相关而不是间接相关,(表中的每一列只能依赖于主键);
例如:订单表中需要有客户相关信息,在分离出客户表之后,订单表中只需要有一个用户编号即可,而不能有其他的客户信息。因为其他的客户信息直接关联于用户编号,而不是直接与订单编号直接相关。
165.一张自增表里面总共有 7 条数据,删除了最后 2 条数据,重启 mysql 数据库,又插入了一条数据,此时 id 是几?
一般情况下,我们创建的表的类型是InnoDB,如果新增一条记录(不重启mysql的情况下),这条记录的ID是8;但是如果重启MySQL的话,这条记录的ID是6。因为InnoDB表只把自增主键的最大ID记录到内存中,所以重启数据库或者对表OPTIMIZE操作,都会使最大ID丢失。
但是,如果我们使用表的类型是MylSAM,那么这条记录的ID就是8。因为MylSAM表会把自增主键的最大ID记录到数据文件里面,重启MYSQL后,自增主键的最大ID也不会丢失。
如果在这7条记录里面删除的是中间的几个记录(比如删除的是3,4,5三条记录),重启MySQL数据库后,insert一条记录后,ID都是8。因为内存或者数据库文件存储都是自增主键最大ID
166.如何获取当前数据库版本?
利用navicat连接mysql的客户端工具,运行下边sql select version()
167.说一下 ACID 是什么?
Atomicity
- 原子性体现在对于一个事务来讲,要么一起执行成功要么一起失败,执行的过程中是不能被打断或者执行其他操作的。
Consistency
- 一致性表现为事务进行过后和执行前,整体系统都是稳定的,比如对于入账出账操作是不会有总资金的变化的。
Isolation
- 隔离性表示各个事务之间不会互相影响,数据库一般会提供多种级别的隔离。实际上多个事务是并发执行的,但是他们之间不会互相影响。
Durability
- 持久性表示一旦一个事务成功了,那么他的改变是永久性的被记录和操作。
168.char 和 varchar 的区别是什么?
- char的长度是不可变的,而varchar的长度是可变的,
- 定义一个char[10]和varchar[10],如果存进去的是‘abcd’,那么char所占的长度依然为10,除了字符‘abcd’外,后面跟六个空格,而varchar就立马把长度变为4了,取数据的时候,char类型的要用trim()去掉多余的空格,而varchar是不需要的,
- char的存取数度还是要比varchar要快得多,因为其长度固定,方便程序的存储与查找;但是char也为此付出的是空间的代价,因为其长度固定,所以难免会有多余的空格占位符占据空间,可谓是以空间换取时间效率,而varchar是以空间效率为首位的。
- char的存储方式是,对英文字符(ASCII)占用1个字节,对一个汉字占用两个字节;而varchar的存储方式是,对每个英文字符占用2个字节,汉字也占用2个字节,两者的存储数据都非unicode的字符数据。
169.float 和 double 的区别是什么?
- .在内存中占有的字节数不同
单精度浮点数在机内存占4个字节
双精度浮点数在机内存占8个字节 - 有效数字位数不同
单精度浮点数有效数字8位
双精度浮点数有效数字16位 - 数值取值范围
单精度浮点数的表示范围:-3.40E+38~3.40E+38
双精度浮点数的表示范围:-1.79E+308~-1.79E+308 - 在程序中处理速度不同
一般来说,CPU处理单精度浮点数的速度比处理双精度浮点数快
如果不声明,默认小数为double类型,所以如果要用float的话,必须进行强转
例如:float a=1.3; 会编译报错,正确的写法 float a = (float)1.3;或者float a = 1.3f;(f或F都可以不区分大小写)
float是8位有效数字,第7位数字将会四舍五入
170.mysql 的内连接、左连接、右连接有什么区别?
1.内连接,显示两个表中有联系的所有数据;
2.左链接,以左表为参照,显示所有数据,右表没有的数据补充为null;
3.右链接,以右表为参照显示数据,左表没有的数据补充为null;
171.mysql 索引是怎么实现的?
参考博客:https://blog.csdn.net/waeceo/article/details/78702584
172.怎么验证 mysql 的索引是否满足需求?
使用 explain 查看 SQL 是如何执行查询语句的,从而分析你的索引是否满足需求。
explain 语法:explain select * from table where type=1。
173.说一下数据库的事务隔离?
- 读未提交(Read Uncommitted):只处理更新丢失。如果一个事务已经开始写数据,则不允许其他事务同时进行写操作,但允许其他事务读此行数据。可通过“排他写锁”实现。
- 读已提交(Read Committed):处理更新丢失、脏读。读取数据的事务允许其他事务继续访问改行数据,但是未提交的写事务将会禁止其他事务访问改行。可通过“瞬间共享读锁”和“排他写锁”实现。
- 可重复读(Repeatable Read):处理更新丢失、脏读和不可重复读取。读取数据的事务将会禁止写事务,但允许读事务,写事务则禁止任何其他事务。可通过“共享读锁”和“排他写锁”实现。
- 可串行化(Serializable):提供严格的事务隔离。要求失去序列化执行,事务只能一个接一个地执行,不能并发执行。仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。
174.说一下 mysql 常用的引擎?
InnoDB:支持事务处理,支持外键,支持崩溃修复能力和并发控制。如果需要对事务的完整性要求比较高(比如银行),要求实现并发控制(比如售票),那选择InnoDB有很大的优势。如果需要频繁的更新、删除操作的数据库,也可以选择InnoDB,因为支持事务的提交(commit)和回滚(rollback)。
MyISAM:插入数据快,空间和内存使用比较低。如果表主要是用于插入新记录和读出记录,那么选择MyISAM能实现处理高效率。如果应用的完整性、并发性要求比 较低,也可以使用。
MEMORY:所有的数据都在内存中,数据的处理速度快,但是安全性不高。如果需要很快的读写速度,对数据的安全性要求较低,可以选择MEMOEY。它对表的大小有要求,不能建立太大的表。所以,这类数据库只使用在相对较小的数据库表。
注意:
同一个数据库也可以使用多种存储引擎的表。如果一个表要求比较高的事务处理,可以选择InnoDB。这个数据库中可以将查询要求比较高的表选择MyISAM存储。如果该数据库需要一个用于查询的临时表,可以选择MEMORY存储引擎。
175.说一下 mysql 的行锁和表锁?
MyISAM 只支持表锁,InnoDB 支持表锁和行锁,默认为行锁。
- 表级锁:开销小,加锁快,不会出现死锁。锁定粒度大,发生锁冲突的概率最高,并发量最低。
- 行级锁:开销大,加锁慢,会出现死锁。锁力度小,发生锁冲突的概率小,并发度最高。
176.说一下乐观锁和悲观锁?
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
177.mysql 问题排查都有哪些手段?
- 使用 show processlist 命令查看当前所有连接信息。
- 使用 explain 命令查询 SQL 语句执行计划。
- 开启慢查询日志,查看慢查询的 SQL。
178.如何做 mysql 的性能优化?
1. 选择正确的存储引擎
以 MySQL为例,包括有两个存储引擎 MyISAM 和 InnoDB,每个引擎都有利有弊。
MyISAM 适合于一些需要大量查询的应用,但其对于有大量写操作并不是很好。甚至你只是需要update一个字段,整个表都会被锁起来,而别的进程,就算是读进程都无法操作直到读操作完成。另外,MyISAM 对于 SELECT COUNT(*) 这类的计算是超快无比的。
InnoDB 的趋势会是一个非常复杂的存储引擎,对于一些小的应用,它会比 MyISAM 还慢。但是它支持“行锁” ,于是在写操作比较多的时候,会更优秀。并且,他还支持更多的高级应用,比如:事务。
2. 优化字段的数据类型
记住一个原则,越小的列会越快。如果一个表只会有几列罢了(比如说字典表,配置表),那么,我们就没有理由使用 INT 来做主键,使用 MEDIUMINT, SMALLINT 或是更小的 TINYINT 会更经济一些。如果你不需要记录时间,使用 DATE 要比 DATETIME 好得多。当然,你也需要留够足够的扩展空间。
3. 为搜索字段添加索引
索引并不一定就是给主键或是唯一的字段。如果在你的表中,有某个字段你总要会经常用来做搜索,那么最好是为其建立索引,除非你要搜索的字段是大的文本字段,那应该建立全文索引。
4. 避免使用Select *
从数据库里读出越多的数据,那么查询就会变得越慢。并且,如果你的数据库服务器和WEB服务器是两*立的服务器的话,这还会增加网络传输的负载。即使你要查询数据表的所有字段,也尽量不要用*通配符,善用内置提供的字段排除定义也许能给带来更多的便利。
5. 使用 ENUM 而不是 VARCHAR
ENUM 类型是非常快和紧凑的。在实际上,其保存的是 TINYINT,但其外表上显示为字符串。这样一来,用这个字段来做一些选项列表变得相当的完美。例如,性别、民族、部门和状态之类的这些字段的取值是有限而且固定的,那么,你应该使用 ENUM 而不是 VARCHAR。
6. 尽可能的使用 NOT NULL
除非你有一个很特别的原因去使用 NULL 值,你应该总是让你的字段保持 NOT NULL。 NULL其实需要额外的空间,并且,在你进行比较的时候,你的程序会更复杂。 当然,这里并不是说你就不能使用NULL了,现实情况是很复杂的,依然会有些情况下,你需要使用NULL值。
7. 固定长度的表会更快
如果表中的所有字段都是“固定长度”的,整个表会被认为是 “static” 或 “fixed-length”。 例如,表中没有如下类型的字段: VARCHAR,TEXT,BLOB。只要你包括了其中一个这些字段,那么这个表就不是“固定长度静态表”了,这样,MySQL 引擎会用另一种方法来处理。
固定长度的表会提高性能,因为MySQL搜寻得会更快一些,因为这些固定的长度是很容易计算下一个数据的偏移量的,所以读取的自然也会很快。而如果字段不是定长的,那么,每一次要找下一条的话,需要程序找到主键。
并且,固定长度的表也更容易被缓存和重建。不过,唯一的副作用是,固定长度的字段会浪费一些空间,因为定长的字段无论你用不用,他都是要分配那么多的空间。
十八、Redis
179.redis 是什么?都有哪些使用场景?
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、高性能、Key-Value缓存数据库,并提供多种语言的API。
Redis 全称REmote DIctionary Server(远程字典服务器)。
它通常也被称为数据结构服务器,因为值(value)可以是字符串(String), 哈希(Map), 列表(list), 集合(sets) 和有序集合(sorted sets)等类型。
Redis 使用场景:
- 缓存——热数据
- 计数器
- 队列
- 位操作(大数据处理)
- 分布式锁与单线程机制
- 最新列表
- 排行榜
180.redis 有哪些功能?
- 数据缓存功能
- 分布式锁的功能
- 支持数据持久化
- 支持事务
- 支持消息队列
181.redis 和 memecache 有什么区别?
1、存储方式:
memecache 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小
redis有部份存在硬盘上,这样能保证数据的持久性,支持数据的持久化。
2、数据支持类型:
redis在数据支持上要比memecache多的多。
3、使用底层模型不同:
新版本的redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
4、运行环境不同:
redis目前官方只支持LINUX 上去行,从而省去了对于其它系统的支持,这样的话可以更好的把精力用于本系统 环境上的优化,虽然后来微软有一个小组为其写了补丁。但是没有放到主干上
182.redis 为什么是单线程的?
注意:redis 单线程指的是网络请求模块使用了一个线程,即一个线程处理所有网络请求,其他模块仍用了多个线程。
因为CPU不是Redis的瓶颈。Redis的瓶颈最有可能是机器内存或者网络带宽,既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。关于redis的性能,官方网站也有,普通笔记本轻松处理每秒几十万的请求
183.什么是缓存穿透?怎么解决?
缓存穿透,是指查询一个数据库一定不存在的数据。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存。
当通过某一个key去查询数据的时候,如果对应在数据库中的数据都不存在,我们将此key对应的value设置为一个默认的值,比如“NULL”,并设置一个缓存的失效时间,这时在缓存失效之前,所有通过此key的访问都被缓存挡住了。后面如果此key对应的数据在DB中存在时,缓存失效之后,通过此key再去访问数据,就能拿到新的value了。
184.redis 支持的数据类型有哪些?
-
String(字符串)
string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。
string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。
string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。 -
Hash(哈希)
Redis hash 是一个键值(key=>value)对集合。
Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。 -
List(列表)
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。 -
Set(集合)
Redis的Set是string类型的无序集合。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 -
zset(sorted set:有序集合)
Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
zset的成员是唯一的,但分数(score)却可以重复。
185.redis 支持的 java 客户端都有哪些?
Redisson,Jedis,lettuce等,官方推荐使用Redisson。
186.jedis 和 redisson 有哪些区别?
1. 概况对比
Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
2. 编程模型
Jedis中的方法调用是比较底层的暴露的Redis的API,也即Jedis中的Java方法基本和Redis的API保持着一致,了解Redis的API,也就能熟练的使用Jedis。而Redisson中的方法则是进行比较高的抽象,每个方法调用可能进行了一个或多个Redis方法调用。
3. 可伸缩性
Jedis使用阻塞的I/O,且其方法调用都是同步的,程序流需要等到sockets处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis。Redisson使用非阻塞的I/O和基于Netty框架的事件驱动的通信层,其方法调用是异步的。Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作。
4. 数据结构
Jedis仅支持基本的数据类型如:String、Hash、List、Set、Sorted Set。
Redisson不仅提供了一系列的分布式Java常用对象,基本可以与Java的基本数据结构通用,还提供了许多分布式服务,其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service)。在分布式开发中,Redisson可提供更便捷的方法。
5. 第三方框架整合
- Redisson提供了和Spring框架的各项特性类似的,以Spring XML的命名空间的方式配置RedissonClient实例和它所支持的所有对象和服务;
- Redisson完整的实现了Spring框架里的缓存机制;
- Redisson在Redis的基础上实现了Java缓存标准规范;
- Redisson为Apache Tomcat集群提供了基于Redis的非黏性会话管理功能。该功能支持Apache Tomcat的6、7和8版。
- Redisson还提供了Spring Session会话管理器的实现。
187.怎么保证缓存和数据库数据的一致性?
合理设置缓存的过期时间。
新增、更改、删除数据库操作时同步更新 Redis,可以使用事物机制来保证数据的一致性。
188.redis 持久化有几种方式?
redis提供两种方式进行持久化:
RDB持久化(原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化)。
AOF(append only file)持久化(原理是将Reids的操作日志以追加的方式写入文件)。
189.redis 怎么实现分布式锁?
分布式锁一般有三种实现方式:
- 数据库乐观锁;
- 基于Redis的分布式锁;
- 基于ZooKeeper的分布式锁。
190.redis 分布式锁有什么缺陷?
- 实现复杂,需要考虑超时,原子性,误删等情况
- 没有等待锁的队列,只能在客户端自旋来等锁,效率低下
191.redis 如何做内存优化?
- 尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。
- 根据业务场景,考虑使用BITMAP
- Redis提供内存回收策略,根据使用的情况可以选择适当的回收策略,比如过期数据清除,expire设置数据过期时间
192.redis 淘汰策略有哪些?
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰;
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰;
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰 ;
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰;
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰;
- no-enviction(驱逐):禁止驱逐数据。
193.redis 常见的性能问题有哪些?该如何解决?
- Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照。
- Master AOF持久化,如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化,如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
- Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
- Redis主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内
- 尽量避免在压力很大的主库上增加从库
- 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3…。这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以立刻启用Slave1做Master,其他不变。
十九、JVM
194.说一下 jvm 的主要组成部分?及其作用?
- 类加载器(ClassLoader)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
- 本地库接口(Native Interface)
作用: 首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
195.说一下 jvm 运行时数据区?
- 堆。 堆是Java对象的存储区域,任何用new字段分配的Java对象实例和数组,都被分配在堆上,Java堆可使用-Xms -Xmx进行内存控制,值得一提的是从JDK1.7版本之后,运行时常量池从方法区移到了堆上。
- 方法区。它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据,方法区在JDK1.7版本及以前被称为永久代,从JDK1.8永久代被移除。
- 虚拟机栈。虚拟机栈中执行每个方法的时候,都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
- 本地方法栈。与虚拟机栈发挥的作用相似,相比于虚拟机栈为Java方法服务,本地方法栈为虚拟机使用的Native方法服务,执行每个本地方法的时候,都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
- 程序计数器。指示Java虚拟机下一条需要执行的字节码指令。
196.说一下堆栈的区别?
- 栈内存:栈内存首先是一片内存区域,存储的都是局部变量,凡是定义在方法中的都是局部变量(方法外的是全局变量),for循环内部定义的也是局部变量,是先加载函数才能进行局部变量的定义,所以方法先进栈,然后再定义变量,变量有自己的作用域,一旦离开作用域,变量就会被释放。栈内存的更新速度很快,因为局部变量的生命周期都很短。
- 堆内存:存储的是数组和对象(其实数组就是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象),实体用于封装数据,而且是封装多个(实体的多个属性),如果一个数据消失,这个实体也没有消失,还可以用,所以堆是不会随时释放的,但是栈不一样,栈里存放的都是单个变量,变量被释放了,那就没有了。堆里的实体虽然不会被释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取。
堆与栈的区别:
- 栈内存存储的是局部变量而堆内存存储的是实体;
- 栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短;
- 栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。
197.队列和栈是什么?有什么区别?
栈:是一端受限,一段允许进行操作的线性表。我自己理解时,会将它理解成一个装书的盒子。放书,取书,就是进行的操作。这个的特点就是,你放了一踏书,现在你想取书,你只能先把上面的书一个个取出来,即:先放的后取,后放的先取。放在栈上说,就是先进后出。
队列:是一种限定性的线性表。这样理解比较好,学生排队买饭。有什么特点呢?当然,你先来,就先打饭,先吃饭。抽象到队列上说,有队头,队尾,要想加入(入队),只能从队尾加,想走(出队),只能从队头走。即:先进先出。
队列和栈的区别:
- 操作的名称不同。队列的插入称为入队,队列的删除称为出队。栈的插入称为进栈,栈的删除称为出栈。
- 可操作的方式不同。队列是在队尾入队,队头出队,即两边都可操作。而栈的进栈和出栈都是在栈顶进行的,无法对栈底直接进行操作。
- 操作的方法不同。队列是先进先出(FIFO),即队列的修改是依先进先出的原则进行的。新来的成员总是加入队尾(不能从中间插入),每次离开的成员总是队列头上(不允许中途离队)。而栈为后进先出(LIFO),即每次删除(出栈)的总是当前栈中最新的元素,即最后插入(进栈)的元素,而最先插入的被放在栈的底部,要到最后才能删除。
198.什么是双亲委派模型?
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类请求都会传给顶层的启动类加载器(Bootstrap ClassLoader),只有当父加载器反馈自己无法完成该加载请求时,子加载器才会尝试自己去加载。
199.说一下类加载的执行过程?
类加载分为以下 5 个步骤:
加载:根据查找路径找到相应的 class 文件然后导入;
检查:检查加载的 class 文件的正确性;
准备:给类中的静态变量分配内存空间;
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标 示,而在直接引用直接指向内存中的地址;
初始化:对静态变量和静态代码块执行初始化工作。
200.怎么判断对象是否可以被回收?
一般有两种方法来判断:
- 引用计数器:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
- 可达性分析:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
201.java 中都有哪些引用类型?
- StrongReference(强引用)
写法:Test test = new Test()
这是最常见的引用类型,也是最牢固的引用类型,当jvm发生gc时,对象被引用不会被gc回收,jvm内存满了将要发生OOM(out of memory)的时候,强引用类型也不会被回收
- SoftReference(软引用)
写法:SoftReference<String> softReference = new SoftReference<>(new String("Hello World"));
较强引用来说,软引用在发生gc时,被引用的对象不会被回收,当内存满时,将要发生OOM时,gc会回收软引用的对象
- WeakReference(弱引用)
写法:WeakReference<String> weakReference = new WeakReference<>(new String("Hello World"));
弱引用在发生gc时,就会被gc回收,不管内存用了多少,弱引用最长的生命周期是两次gc的间隔时间
- PhantomReference(虚引用,幻引用)
写法:ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
PhantomReference<String> phantomReference = new PhantomReference<>(new String("Hello World"), referenceQueue);
虚引用和上面两种引用有一点点小区别,多了一个依赖队列。虚引用并不会决定对象的生命周期,有他没他都一个样,无法通过虚引用获取对象。
202.说一下 jvm 有哪些垃圾回收算法?
1.标记清除
标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。
在标记阶段首先通过根节点(GC Roots),标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
2.复制算法
从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存(图中下边的那一块儿内存)上去,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉
3.标记整理
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。
这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。
标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。
首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
4.分代收集算法
分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于老年代的问题,将内存分为各个年代。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。
在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率搞,没有额外空间对它进行分配担保,所以只能使用标记清除或者标记整理算法。
203.说一下 jvm 有哪些垃圾回收器?
- Serial:最早的单线程串行垃圾回收器。
- Serial Old:Serial 垃圾回收器的老年版本,同样也是单线程的,可以作为 CMS 垃圾回收器的备选预案。
- ParNew:是 Serial 的多线程版本。
- Parallel 和 ParNew 收集器类似是多线程的,但 Parallel 是吞吐量优先的收集器,可以牺牲等待时间换取系统的吞吐量。
- Parallel Old 是 Parallel 老生代版本,Parallel 使用的是复制的内存回收算法,Parallel Old 使用的是标记-整理的内存回收算法。
- CMS:一种以获得最短停顿时间为目标的收集器,非常适用 B/S 系统。
- G1:一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK 9 以后的默认 GC 选项。
204.详细介绍一下 CMS 垃圾回收器?
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。
CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
205.新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?
新生代回收器:Serial、ParNew、Parallel Scavenge
老年代回收器:Serial Old、Parallel Old、CMS
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。
206.简述分代垃圾回收器是怎么工作的?
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
把 Eden + From Survivor 存活的对象放入 To Survivor 区;
清空 Eden 和 From Survivor 分区;
From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
207.说一下 jvm 调优的工具?
Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。
JProfiler:商业软件,需要付费。功能强大。
VisualVM:JDK自带,功能强大,与JProfiler类似。推荐。
具体参考博客:https://pengjiaheng.iteye.com/blog/552456
208.常用的 jvm 调优的参数都有哪些?
ce = new WeakReference<>(new String(“Hello World”));
弱引用在发生gc时,就会被gc回收,不管内存用了多少,弱引用最长的生命周期是两次gc的间隔时间
- **PhantomReference(虚引用,幻引用)**
写法:ReferenceQueue referenceQueue = new ReferenceQueue<>();
PhantomReference phantomReference = new PhantomReference<>(new String(“Hello World”), referenceQueue);
``
虚引用和上面两种引用有一点点小区别,多了一个依赖队列。虚引用并不会决定对象的生命周期,有他没他都一个样,无法通过虚引用获取对象。
202.说一下 jvm 有哪些垃圾回收算法?
1.标记清除
标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。
在标记阶段首先通过根节点(GC Roots),标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
2.复制算法
从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存(图中下边的那一块儿内存)上去,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉
3.标记整理
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。
这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。
标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。
首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
4.分代收集算法
分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于老年代的问题,将内存分为各个年代。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。
在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率搞,没有额外空间对它进行分配担保,所以只能使用标记清除或者标记整理算法。
203.说一下 jvm 有哪些垃圾回收器?
- Serial:最早的单线程串行垃圾回收器。
- Serial Old:Serial 垃圾回收器的老年版本,同样也是单线程的,可以作为 CMS 垃圾回收器的备选预案。
- ParNew:是 Serial 的多线程版本。
- Parallel 和 ParNew 收集器类似是多线程的,但 Parallel 是吞吐量优先的收集器,可以牺牲等待时间换取系统的吞吐量。
- Parallel Old 是 Parallel 老生代版本,Parallel 使用的是复制的内存回收算法,Parallel Old 使用的是标记-整理的内存回收算法。
- CMS:一种以获得最短停顿时间为目标的收集器,非常适用 B/S 系统。
- G1:一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK 9 以后的默认 GC 选项。
204.详细介绍一下 CMS 垃圾回收器?
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。
CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
205.新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?
新生代回收器:Serial、ParNew、Parallel Scavenge
老年代回收器:Serial Old、Parallel Old、CMS
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。
206.简述分代垃圾回收器是怎么工作的?
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
把 Eden + From Survivor 存活的对象放入 To Survivor 区;
清空 Eden 和 From Survivor 分区;
From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
207.说一下 jvm 调优的工具?
Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。
JProfiler:商业软件,需要付费。功能强大。
VisualVM:JDK自带,功能强大,与JProfiler类似。推荐。
具体参考博客:https://pengjiaheng.iteye.com/blog/552456
208.常用的 jvm 调优的参数都有哪些?
下一篇: 关于百度站长工具的使用测评