大家好,我是二哥呀。
今天是五一假期的第一天,仍然有同学在继续准备面试,他已经面了阿里的大部分部门,包括前面提到的饿了吗、阿里妈妈、支付宝、阿里国际等等。这次我们就以《Java 面试指南》中同学 5 为例,来看看阿里的面试官都喜欢问哪些问题。
Java 面试指南专栏
还在冲刺的同学可以收藏起来,好做到知彼知己百战不殆。
对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。
悲观锁的代表有 synchronized 关键字和 Lock 接口:
乐观锁,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。一旦多个线程发生冲突,乐观锁通常使用一种称为 CAS 的技术来保证线程执行的安全性。
由于乐观锁假想操作中没有锁的存在,因此不太可能出现死锁的情况,换句话说,乐观锁天生免疫死锁。
线程说简单点就是我们在 Java 程序中启动的一个 main 线程,一个进程至少会有一个线程。当然了,我们也可以启动多个线程,比如说一个线程进行 IO 读写,一个线程进行加减乘除计算,这样就可以充分发挥多核 CPU 的优势,因为 IO 读写相对 CPU 计算来说慢得多。线程是 CPU 分配资源的基本单位。
线程池,简单来说,就是一个管理线程的池子。
三分恶面渣逆袭:管理线程的池子
①、频繁地创建和销毁线程会消耗系统资源,线程池能够复用已创建的线程。
②、提高响应速度,当任务到达时,任务可以不需要等待线程创建就立即执行。
③、线程池支持定时执行、周期性执行、单线程执行和并发数控制等功能。
在确定一个系统最多可以创建多个线程时,除了需要考虑系统的内存大小外,Java 虚拟机栈的大小也是值得考虑的因素。
线程在创建的时候会被分配一个虚拟机栈,在 64 位操作系统中,默认大小为 1M。
通过 java -XX:+PrintFlagsFinal -version | grep ThreadStackSize
这个命令可以查看 JVM 栈的默认大小。
二哥的 Java 进阶之路:默认的虚拟机栈大小
其中 ThreadStackSize 的单位是字节,也就是说默认的 JVM 栈大小是 1024 KB,也就是 1M。
换句话说,8GB = 8 * 1024 MB = 8 * 1024 * 1024 KB,所以一个 8G 内存的系统可以创建的线程数为 8 * 1024 = 8192 个。
但操作系统本身的运行也需要消耗一定的内存,所以实际上可以创建的线程数肯定会比 8192 少一些。
可以通过下面这段代码来验证一下:
public class StackOverflowErrorTest1 {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
while (true) {
testStackOverflowError();
}
}
public static void testStackOverflowError() {
System.out.println(count.incrementAndGet());
testStackOverflowError();
}
}
首先是 main 线程,这是程序开始执行的入口。
然后是垃圾回收线程,它是一个后台线程,负责回收不再使用的对象。
还有编译器线程,在及时编译中(JIT),负责把一部分热点代码编译后放到 codeCache 中,以提升程序的执行效率。
二哥的 Java 进阶之路:JIT
可以通过下面这段代码进行检测:
class ThreadLister {
public static void main(String[] args) {
// 获取所有线程的堆栈跟踪
Map<Thread, StackTraceElement[]> threads = Thread.getAllStackTraces();
for (Thread thread : threads.keySet()) {
System.out.println("Thread: " + thread.getName() + " (ID=" + thread.getId() + ")");
}
}
}
结果如下所示:
Thread: Monitor Ctrl-Break (ID=5)
Thread: Reference Handler (ID=2)
Thread: main (ID=1)
Thread: Signal Dispatcher (ID=4)
Thread: Finalizer (ID=3)
简单解释下:
Thread: main (ID=1)
- 主线程,Java 程序启动时由 JVM 创建。Thread: Reference Handler (ID=2)
- 这个线程是用来处理引用对象的,如软引用(SoftReference)、弱引用(WeakReference)和虚引用(PhantomReference)。负责清理被 JVM 回收的对象。Thread: Finalizer (ID=3)
- 终结器线程,负责调用对象的 finalize 方法。对象在垃圾回收器标记为可回收之前,由该线程执行其 finalize 方法,用于执行特定的资源释放操作。Thread: Signal Dispatcher (ID=4)
- 信号调度线程,处理来自操作系统的信号,将它们转发给 JVM 进行进一步处理,例如响应中断、停止等信号。Thread: Monitor Ctrl-Break (ID=5)
- 监视器线程,通常由一些特定的 IDE 创建,用于在开发过程中监控和管理程序执行或者处理中断。对象通常会现在年轻代中分配,然后随着时间的推移和垃圾收集的处理,某些对象会进入到老年代中。
三分恶面渣逆袭:对象进入老年代
①、长期存活的对象将进入老年代
对象在年轻代中存活足够长的时间(即经过足够多的垃圾回收周期)后,会晋升到老年代。
每次 GC 未被回收的对象,其年龄会增加。当对象的年龄超过一个特定阈值(默认通常是 15),它就会被移动到老年代。这个年龄阈值可以通过 JVM 参数-XX:MaxTenuringThreshold
来设置。
②、大对象直接进入老年代
为了避免在年轻代中频繁复制大对象,JVM 提供了一种策略,允许大对象直接在老年代中分配。
这些是所谓的“大对象”,其大小超过了预设的阈值(由 JVM 参数-XX:PretenureSizeThreshold
控制)。直接在老年代分配可以减少在年轻代和老年代之间的数据复制。
③、动态对象年龄判定
除了固定的年龄阈值,还会根据各个年龄段对象的存活大小和总空间等因素动态调整对象的晋升策略。
如果在 Survivor 空间中相同年龄的所有对象大小总和大于 Survivor 空间的一半,那么年龄大于或等于该年龄的对象就可以直接进入老年代。
Minor GC 也称为 Young GC,是指发生在年轻代(Young Generation)的垃圾收集。年轻代包含 Eden 区以及两个 Survivor 区。
二哥的 Java 进阶之路:Java 堆划分
Full GC 是最彻底的垃圾收集,涉及整个 Java 堆和方法区(或元空间)。它是最耗时的 GC,通常在 JVM 压力很大时发生。
我从数据结构上来说吧。
①、B+树索引:最常见的索引类型,一种将索引值按照一定的算法,存入一个树形的数据结构中(二叉树),每次查询都从树的根节点开始,一次遍历叶子节点,找到对应的值。查询效率是 O(logN)。
也是 InnoDB 存储引擎的默认索引类型。
B+ 树是 B 树的升级版,B+ 树中的非叶子节点都不存储数据,只存储索引。叶子节点中存储了所有的数据,并且构成了一个从小到大的有序双向链表,使得在完成一次树的遍历定位到范围查询的起点后,可以直接通过叶子节点间的指针顺序访问整个查询范围内的所有记录,而无需对树进行多次遍历。这在处理大范围的查询时特别高效。
一颗剽悍的种子:B+树的结构
因为 B+ 树是 InnoDB 的默认索引类型,所以创建 B+ 树的时候不需要指定索引类型。
CREATE TABLE example_btree (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
INDEX name_index (name)
) ENGINE=InnoDB;
②、Hash 索引:基于哈希表的索引,查询效率可以达到 O(1),但是只适合 = 和 in 查询,不适合范围查询。
Hash 索引在原理上和 Java 中的 HashMap 类似,当发生哈希冲突的时候也是通过拉链法来解决。
业余码农:哈希索引
可以通过下面的语句创建哈希索引:
CREATE TABLE example_hash (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
UNIQUE HASH (name)
) ENGINE=MEMORY;
注意,我们这里创建的是 MEMORY 存储引擎,InnoDB 并不提供直接创建哈希索引的选项,因为 B+ 树索引能够很好地支持范围查询和等值查询,满足了大多数数据库操作的需要。
不过,InnoDB 存储引擎内部使用了一种名为“自适应哈希索引”(Adaptive Hash Index, AHI)的技术。
自适应哈希索引并不是由用户显式创建的,而是 InnoDB 根据数据访问的模式自动建立和管理的。当 InnoDB 发现某个索引被频繁访问时,会在内存中创建一个哈希索引,以加速对这个索引的访问。
可以通过下面的语句查看自适应哈希索引的状态:
SHOW VARIABLES LIKE 'innodb_adaptive_hash_index';
如果返回的值是 ON,说明自适应哈希索引是开启的。
二哥的 Java 进阶之路
尽管索引能提高查询性能,但不当的使用也会带来一系列问题。在加索引时需要注意以下几点:
①、选择合适的列作为索引
②、避免过多的索引
③、利用前缀索引和索引列的顺序