高频快问快答
阅读提示
这页适合“面试前 30 分钟”查漏补缺。
用法建议:先看题目,自己口述 20~40 秒,再对照参考回答补关键术语。
使用方式
- 每次抽 5 题,限时回答。
- 对回答卡顿的题,加上自己的项目例子再复述一遍。
- 对“线程、锁、反射、关键字”这四类题优先复盘。
Java有哪些数据类型

访问修饰符 public,private,protected,以及不写(默认)时的区别
- private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
- default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。 使用对象:类、接口、变量、方法。
- protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
- public : 对所有类可见。使用对象:类、接口、变量、方法

final finally finalize区别
- final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表 示该变量是一个常量不能被重新赋值。
- finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法 finally代码块 中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
- finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调 用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的 最后判断。
this与super的区别
- super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参)
- this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名)
- super()和this()类似,区别是,super()在子类中调用父类的构造方法,this()在本类内调用本类的其它构造方法。
- super()和this()均需放在构造方法内第一行。
- 尽管可以用this调用一个构造器,但却不能调用两个。
- this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
- this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。
- 从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。
static存在的主要意义
- static的主要意义是在于创建独立于具体对象的域变量或者方法。以致于即使没有创建对象,也能使用属性和调用方法!
- static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。
- 为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。
面向对象五大基本原则是什么
- 单一职责原则SRP(Single Responsibility Principle) :类的功能要单一,不能包罗万象,跟杂货铺似的。
- 开放封闭原则OCP(Open-Close Principle) :一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。
- 里式替换原则LSP(the Liskov Substitution Principle LSP) : 子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活。哈哈~~
- 依赖倒置原则DIP(the Dependency Inversion Principle DIP) :高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的抽象是中国人,而不是你是xx村的。
- 接口分离原则ISP(the Interface Segregation Principle ISP) :设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。
重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?
- 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
- 重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分
- 重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。
反射机制的应用场景有哪些?
- 反射是框架设计的灵魂。
- 在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。
- 举例:①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; 2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性
线程的sleep、wait、join、yield如何使用?
- sleep:使当前线程暂停执行一段时间。例如,
Thread.sleep(1000);会让线程暂停1秒。sleep期间线程不会释放锁,时间到后自动恢复执行。 - wait:使当前线程进入等待状态,并释放对象锁,直到其他线程调用notify或notifyAll方法。wait必须在synchronized代码块中使用,否则会抛出异常。
- join:用于等待一个或多个线程执行结束。例如,
thread.join();会让当前线程等待thread线程执行完毕后才继续执行。也可以设置等待时间,如thread.join(1000);表示最多等待1秒。 - yield:提示当前线程让出CPU使用权,使其他线程有机会执行。但yield不会让线程进入阻塞状态,而是进入就绪状态,重新参与CPU调度。
start 与 run 区别 ?
功能不同:
start()方法是Thread类的一个方法,用于启动一个新线程,使得线程进入就绪状态,等待JVM调度执行。调用start()后,线程会异步执行run()方法中的代码。
run()方法则是线程的主体方法,包含了线程要执行的代码。但是,如果直接调用run()方法,它会在当前线程的上下文中同步执行,而不会创建新的线程。
执行效果:
调用start()会创建新线程,实现多线程的并发执行效果,提高程序效率和响应能力。
直接调用run()只是普通方法调用,程序仍然只有主线程一个执行路径,无法实现多线程的效果。
代码示例:
package com.javatiaocao.threading.startrun;
public class ThreadExample extends Thread{
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(i + " 由 " + Thread.currentThread().getName() + " 输出");
try {
Thread.sleep(400); // 让线程休息一会儿
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ThreadExample thread = new ThreadExample();
System.out.println("调用 run() 方法时:");
thread.run(); // 这行代码直接在主线程中执行 run 方法
// 等待 run 方法执行完毕
try {
Thread.sleep(5000); // 确保 run 方法执行完成,避免输出混淆
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("调用 start() 方法时:");
thread.start(); // 这行代码会启动一个新的线程然后在新线程中执行 run 方法
// 在主线程中也进行输出
for (int i = 1; i <= 10; i++) {
System.out.println(i + " 由 " + Thread.currentThread().getName() + " 输出");
try {
Thread.sleep(400); // 主线程休息一会儿
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}什么是用户态,什么是内核态
用户态:
用户态是指程序在较低的特权级别下执行。
在用户态下,程序只能访问自己的内存空间和受限的系统资源,无法直接操作硬件设备或修改操作系统内核数据。
Java程序一般运行在用户态,通过Java虚拟机(JVM)来间接访问底层系统资源和硬件设备。
内核态:
内核态是指程序在较高的特权级别下执行。
在内核态下,程序拥有更高的权限,可以直接访问和操作操作系统的核心功能和硬件资源,如CPU、内存、硬盘等。
内核态下的程序可以执行特权操作,如修改内核数据结构、执行特权指令等。但同时,也需要承担相应的风险和责任。
总结来说,用户态和内核态的主要区别在于特权级别和资源访问能力。用户态下程序权限较低,受到限制;而内核态下程序具有更高的权限,可以直接访问和操作操作系统的核心资源。
各种类型的锁介绍
独占锁
- 独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
*共享锁*
- 共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
重量级锁
- Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁”。JDK 中对 Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。
轻量级锁
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁
*锁升级*
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。
“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
偏向锁
Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换ThreadID 的时候依赖一次 CAS 原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
分段锁
分段锁也并非一种实际的锁,而是一种思想 ConcurrentHashMap 是学习分段锁的最好实践。
锁的优化机制了解吗?
- 从JDK1.6版本之后,synchronized本身也在不断优化锁的机制,有些情况下他并不会是一个很重量级的锁了。优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。
- 锁的状态从低到高依次为无锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到高,降级在一定条件也是有可能发生的。
- 自旋锁:由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环, 可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置。
- 自适应锁:自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。
- 锁消除:锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。
- 锁粗化:锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。
- 偏向锁:当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置-XX:+UseBiasedLocking开启偏向锁。
- 轻量级锁:JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。整个锁升级的过程非常复杂,我尽力去除一些无用的环节,简单来描述整个升级的机制。简单点说,偏向锁就是通过对象头的偏向线程ID来对比,甚至都不需要CAS了,而轻量级锁主要就是通过CAS修改对象头锁记录和自旋来实现,重量级锁则是除了拥有锁的线程其他全部阻塞。

