Java面试知识点
2021-02-26 13:33:05 # 总结

Java面试知识点总结

参考:JavaGuide

JVM相关

JVM的内存分布

JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【JAVA 堆、方法区】、直接内存。

JVM GC相关

Java类加载机制

Java类加载过程

Java类加载模型——双亲委派模型

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。

关键字相关

关于 final 关键字的一些总结

final 关键字主要用在三个地方:变量、方法、类。

  1. 对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改; 如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
  2. 当用 final 修饰一个类时,表明这个类不能被继承。final 类中的所有成员方法都会被隐式地 指定为 final 方法。
  3. 使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第 二个原因是效率。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法 过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。类中所有的 private 方法都隐式地指定为 final。

== 与 equals

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

equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

  1. 情况 1:类没有覆盖 equals() 方法。则通过 equals() 比􏰃该类的两个对象时,等价于通过 “WX”比􏰃这两个对象。
  2. 情况 2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比􏰃两个对象的内容是 否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

hashCode 与 equals

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用 是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。

我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode: 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与该位置其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如 果发现有相同 hashcode 值的对象,这时会调用 equals() 方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

synchronized和ReentrantLock 的区别

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

不同点

  1. synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API。 准确来说,synchronized是一个关键字,是从JVM层面来支持的;但是ReentrantLock是一个类,是一个API,通过lock和unlock来实现的。
  2. ReentrantLock 比 synchronized 增加了一些高级功能
    ReentrantLock提供了一种能够中断等待锁的线程的机制。通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
    ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平 锁就是先等待的线程先获得锁。
    选择性通知。自己没写过,不再详细赘述。

CAS(Compare and Swap)和synchronize有什么区别?都用synchronize不行么?

CAS是乐观锁,不需要阻塞,硬件级 别实现的原子性;synchronize会阻塞,JVM级别实现的原子性。使用场景不同,线程冲突严重时 CAS会造成CPU压力过大,导致吞吐量下降,synchronize的原理是先自旋然后阻塞,线程冲突严 重仍然有􏰃高的吞吐量,因为线程都被阻塞了,不会占用CPU。

Java类相关

String StringBuffer 和 StringBuilder 的区别是什么?

线程安全性:
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、 append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

性能:
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象 引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升, 但却要冒多线程不安全的⻛险。

(重点)HashMap,HashTable和ConcurrentHashMap

HashMap

特性: 非线程安全,支持NULL为key,扩容每次都是double(和hash算法有关),负载因子是0.75.

  1. Java 7实现方式: 数组+链表
  2. Java 8实现方式: 最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。

HashTable

Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类,并且是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。

ConcurrentHashMap

涉及知识点:可重入锁,自旋锁……(To be continued)

特性:线程安全,ConcurrentHashMap是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每 个 Segment 是线程安全的,也就实现了全局的线程安全。Segment默认为16.

  1. Java 7实现方式: 数组+链表
  2. Java 8实现方式: 与HashMap类似引入红黑树。且是使用synchronized+CAS,取消了ReentrantLock。

补充:

1. get需要加锁么,为什么? (不用,volatile关键字)

其他

BIO,NIO,AIO 有什么区别?

相关知识点:select, poll, epoll

  1. BIO (Blocking I/O): 同步阻塞 I/O 模式。最传统的一种 IO 模型,即在读写数据过程中会发生阻塞现象。当用户线程发出 IO 请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用 户线程交出 CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除 block 状态。典型的阻塞 IO 模型的例子为:data = socket.read();如果数据没有就绪,就会一直阻塞在 read 方法。
  2. NIO (Non-blocking/New I/O): NIO 是一种同步非阻塞的 I/O 模型。当用户线程发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备 好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。 所以事实上,在非阻塞 IO 模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞 IO 不会交出 CPU,而会一直占用 CPU。
  3. 多路复用 IO 模型:是目前使用得比较多的模型。Java NIO 实际上就是多路复用 IO。在多路复用 IO 模型中,会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真 正调用实际的 IO 读写操作。因为在多路复用 IO 模型中,只需要使用一个线程就可以管理多个 socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有 socket 读写事件进行时,才会使用 IO 资源,所以它大大减少了资源占用。在 Java NIO 中,是通 过 selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这 种方式会导致用户线程的阻塞。多路复用 IO 模式,通过一个线程就可以管理多个 socket,只有当 socket 真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用 IO 比较适合连接数比较多的情况。另外多路复用 IO 为何比非阻塞 IO 模型的效率高是因为在非阻塞 IO 中,不断地询问 socket 状态时通过用户线程去进行的,而在多路复用 IO 中,轮询每个 socket 状态是内核在进行的,这个效 率要比用户线程要高的多。
  4. AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。异步 IO 模型才是最理想的 IO 模型,在异步 IO 模型中,当用户线程发起 read 操作之后,立刻就 可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个 asynchronous read 之后, 它会立刻返回,说明 read 请求已经成功发起了,因此不会对用户线程产生任何 block。然后,内 核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程 发送一个信号,告诉它 read 操作完成了。也就说用户线程完全不需要实际的整个 IO 操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示 IO 操作已经完成,可以直接去使用数据了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

List<String> res = new ArrayList<>();
int[] dx = {-1, 1, 0, 0};
int[] dy = {0, 0, -1, 1};

List<String> findWords(char[][] m, String[] ws){
for (String word : ws) {
find(m, word);
}
return res;
}

void find(char[][] m, String word) {
int l1 = m.length, l2 = m[0].length;
boolean[][] isVisited = new boolean[l1][l2];

for (int i = 0; i < l1; i++){
for (int j = 0; j < l2; j++) {
dfs(m, isVisited, i, j, word, 0);
if (res.contains(word)) {
return;
}
}
}

return;
}

// isVisited记录是否dfs到,i为当前横坐标,j为纵坐标,word为待查找单词,length为当前查找的word里的index,
// 终止条件为length和word.length()相等
void dfs(char[][] m, boolean[][] isVisited, int i, int j, String word, int length) {
if (length == word.length()) {
res.add(word);
return;
}

// i和j的坐标越界
if (i < 0 || i >= m.length || j < 0 || j >= m[0].length || isVisited[i][j]) {
return;
}

// 当前位置符合下一个元素要求
if (m[i][j] == word.charAt(length)) {
isVisited[i][j] = true;
for (int x = 0; x < 4; x++) {
dfs(m, isVisited, i + dx[x], j + dy[x], word, length+1);
}
isVisited[i][j] = false;
} else {
// 当前位置不符合条件,直接return
return;
}
}