你遇到过哪些质量很高的 Java 面试?,nginx 教程阮一峰
第 2 题之前的回答欠妥,现已改正,在此非常感谢提出质疑的知友!
3. 方法区是 jvm 规范里要求的,永久区是 Hotspot 虚拟机对方法区的具体实现,前者是规范,后者是实现方式。jdk1.8 作了改变。本题看看对方在思想层面对 jvm 的理解程度,很基础的一个题目。
4. 文件中有几个类编译后就有几个 class 文件。
5. 成员变量是可以不经初始化的,在类加载过程的准备阶段即可给它赋予默认值,但局部变量使用前需要显式赋予初始值,javac 不是推断不出不可以这样做,而是没有这样做,对于成员变量而言,其赋值和取值访问的先后顺序具有不确定性,对于成员变量可以在一个方法调用前赋值,也可以在方法调用后进行,这是运行时发生的,编译器确定不了,交给 jvm 去做比较合适。而对于局部变量而言,其赋值和取值访问顺序是确定的。这样设计是一种约束,尽最大程度减少使用者犯错的可能(假使局部变量可以使用默认值,可能总会无意间忘记赋值,进而导致不可预期的情况出现)。
6. ReadWriteRock 读写锁,使用场景可分为读/读、读/写、写/写,除了读和读之间是共享的,其它都是互斥的,接着会讨论下怎样实现互斥锁和同步锁的, 想了解对方对 AQS,CAS 的掌握程度,技术学习的深度。
7. Semaphore 拿到执行权的线程之间是否互斥,Semaphore、CountDownLatch、CyclicBarrier、Exchanger 为 java 并发编程的 4 个辅助类,面试中常问的 CountDownLatch CyclicBarrier 之间的区别,面试者肯定是经常碰到的, 所以问起来意义不大,Semaphore 问的相对少一些,有些知识点如果没有使用过还是会忽略,Semaphore 可有多把锁,可允许多个线程同时拥有执行权,这些有执行权的线程如并发访问同一对象,会产生线程安全问题。
8. 写一个你认为最好的单例模式, 这题面试者都可能遇到过,也算是工作中最常遇到的设计模式之一,想考察面试者对经常碰到的题目的理解深度,单例一共有几种实现方式:饿汉、懒汉、静态内部类、枚举、双检锁,要是写了简单的懒汉式可能就会问:要是多线程情况下怎样保证线程安全呢,面试者可能说双检锁,那么聊聊为什么要两次校验,接着会问光是双检锁还会有什么问题,这时候基础好的面试者就会说了:对象在定义的时候加上 volatile 关键字,接下来会继续引申讨论下原子性和可见性、java 内存模型、类的加载过程。
其实没有最好,枚举方式、静态内部类、双检锁都是可以的,就想听下对不同的单例写法认识程度,写个双检锁的方式吧:
public class Singleton {
private Singleton() {
}
private volatile static Singleton instance;
public static Singleton getInstance() {
if (null == instance) {
synchronized (Singleton.class) {
if (null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
9. B 树和 B+树,这题既问 mysql 索引的实现原理,也问数据结构基础,首先从二叉树说起,因为会产生退化现象,提出了平衡二叉树,再提出怎样让每一层放的节点多一些来减少遍历高度,引申出 m 叉树,m 叉搜索树同样会有退化现象,引出 m 叉平衡树,也就是 B 树,这时候每个节点既放了 key 也放了 value,怎样使每个节点放尽可能多的 key 值,以减少遍历高度呢(访问磁盘次数),可以将每个节点只放 key 值,将 value 值放在叶子结点,在叶子结点的 value 值增加指向相邻节点指针,这就是优化后的 B+树。然后谈谈数据库索引失效的情况,为什么给离散度低的字段(如性别)建立索引是不可取的,查询数据反而更慢,如果将离散度高的字段和性别建立联合索引会怎样,有什么需要注意的?
10. 生产者消费者模式,synchronized 锁住一个 LinkedList,一个生产者,只要队列不满,生产后往里放,一个消费者只要队列不空,向外取,两者通过 wait()和 notify()进行协调,写好了会问怎样提高效率,最后会聊一聊消息队列设计精要思想及其使用。
11. 写一个死锁,觉得这个问题真的很不错,经常说的死锁四个条件,背都能背上,那写一个看看,思想为:定义两个 ArrayList,将他们都加上锁 A,B,线程 1,2,1 拿住了锁 A ,请求锁 B,2 拿住了锁 B 请求锁 A,在等待对方释放锁的过程中谁也不让出已获得的锁。
public class DeadLock {
public static void main(String[] args) {
final List<Integer> list1 = Arrays.asList(1, 2, 3);
final List<Integer> list2 = Arrays.asList(4, 5, 6);
new Thread(new Runnable() {
@Override
public void run() {
synchronized (list1) {
for (Integer i : list1) {
System.out.println(i);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (list2) {
for (Integer i : list2) {
System.out.println(i);
}
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (list2) {
for (Integer i : list2) {
System.out.println(i);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (list1) {
for (Integer i : list1) {
System.out.println(i);
}
}
}
}
}).start();
}
}
12. cpu 100%怎样定位,这题是一个应用性题目,网上搜一下即可,比较常见,说实话,把这题放进来有点后悔。
13. String a = "ab"; String b = "a" + "b"; a ,b 是相等的(各位要写代码验证一下,我看到有人写了错误答案)。常规的问法是 new 一个对象赋给变量,问:这行表达式创建了几个对象,但这样的题目太常见。
14. int a = 1; 是原子性操作。
15. for 循环直接删除 ArrayList 中的特定元素是错的,不同的 for 循环会发生不同的错误,泛型 for 会抛出 ConcurrentModificationException,普通的 for 想要删除集合中重复且连续的元素,只能删除第一个。
错误原因:打开 JDK 的 ArrayList 源码,看下 ArrayList 中的 remove 方法(注意 ArrayList 中的 remove 有两个同名方法,只是入参不同,这里看的是入参为 Object 的 remove 方法)是怎么实现的,一般情况下程序的执行路径会走到 else 路径下最终调用 faseRemove 方法,会执行 System.arraycopy 方法,导致删除元素时涉及到数组元素的移动。针对普通 for 循环的错误写法,在遍历第一个字符串 b 时因为符合删除条件,所以将该元素从数组中删除,并且将后一个元素移动(也就是第二个字符串 b)至当前位置,导致下一次循环遍历时后一个字符串 b 并没有遍历到,所以无法删除。针对这种情况可以倒序删除的方式来避免
解决方案:用 Iterator。
List<String> list = new ArrayList(Arrays.asList("a", "b", "b" , "c", "d"));
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()) {
String element = iterator.next();
if(element.equals("b")) {
iterator.remove();
}
将本问题扩展一下,下面的代码可能会出现什么问题?
ArrayList<String> array = new ArrayList<String>();
array.add(1,"hello world");
--2019.2.7 更新(写了 16 - 23 题解答大纲)
16. 第一步 :线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则执行第二步。
第二步 :线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里进行等待。如果工作队列满了,则执行第三步。
第三步 :线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
17. 抽象队列同步器 AQS(AbstractQueuedSychronizer),如果说 java.util.concurrent 的基础是 CAS 的话,那么 AQS 就是整个 Java 并发包的核心了,ReentrantLock、CountDownLatch、Semaphore 等都用到了它。AQS 实际上以双向队列的形式连接所有的 Entry,比方说 ReentrantLock,所有等待的线程都被放在一个 Entry 中并连成双向队列,前面一个线程使用 ReentrantLock 好了,则双向队列实际上的第一个 Entry 开始运行。AQS 定义了对双向队列所有的操作,而只开放了 tryLock 和 tryRelease 方法给开发者使用,开发者可以根据自己的实现重写 tryLock 和 tryRelease 方法,以实现自己的并发功能。
比较并替换 CAS(Compare and Swap),假设有三个操作数:内存值 V、旧的预期值 A、要修改的值 B,当且仅当预期值 A 和内存值 V 相同时,才会将内存值修改为 B 并返回 true,否则什么都不做并返回 false,整个比较并替换的操作是一个原子操作。CAS 一定要 volatile 变量配合,这样才能保证每次拿到的变量是主内存中最新的相应值,否则旧的预期值 A 对某条线程来说,永远是一个不会变的值 A,只要某次 CAS 操作失败,下面永远都不可能成功。
CAS 虽然比较高效的解决了原子操作问题,但仍存在三大问题。
循环时间长开销很大。
只能保证一个共享变量的原子操作。
ABA 问题。
18. synchronized (this)原理:涉及两条指令:monitorenter,monitorexit;再说同步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令 monitorenter 和 monitorexit 来实现,相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。
JVM 就是根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor,获取成功之后才能执行方法体,方法执行完后再释放 monitor。在方法执行期间,其他任何线程都无法再获得同一个 monitor 对象。
这个问题会接着追问:java 对象头信息,偏向锁,轻量锁,重量级锁及其他们相互间转化。
19. 理解 volatile 关键字的作用的前提是要理解 Java 内存模型,volatile 关键字的作用主要有两点:
多线程主要围绕可见性和原子性两个特性而展开,使用 volatile 关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到 volatile 变量,一定是最新的数据
代码底层执行不像我们看到的高级语言—-Java 程序这么简单,它的执行是 Java 代码–>字节码–>根据字节码执行对应的 C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互,现实中,为了获取更好的性能 JVM 可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用 volatile 则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率
从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。
20. AOP 和 IOC 是 Spring 精华部分,AOP 可以看做是对 OOP 的补充,对代码进行横向的扩展,通过代理模式实现,代理模式有静态代理,动态代理,Spring 利用的是动态代理,在程序运行过程中将增强代码织入原代码中。IOC 是控制反转,将对象的控制权交给 Spring 框架,用户需要使用对象无需创建,直接使用即可。AOP 和 IOC 最可贵的是它们的思想。
21. 什么是循环依赖,怎样检测出循环依赖,Spring 循环依赖有几种方式,使用基于 setter 属性的循环依赖为什么不会出现问题,接下来会问:Bean 的生命周期。
22. 上一张图,从这张图去理解
具体流程:
1). 用户发请求-->DispatcherServlet,前端控制器收到请求后自己不进行处理,而是委托给其他的解析器进行处理,作为统一访问点,进行全局的流程控制。
2).DispatcherServlet-->HandlerMapping,HandlerMapping 将会把请求映射为 HandlerExecutionChain 对象(包含一个 Handler 处理器,多个 HandlerInterceptor 拦截器)。
3).DispatcherServlet-->HandlerAdapter,HandlerAdapter 将会把处理器包装为适配器,从而支持多种类型的处理器。
4).HandlerAdapter-->处理器功能处理方法的调用,HandlerAdapter 将会根据适配的结果调用真正的处理器的功能处理方法,完成功能处理,并返回一个 ModelAndView 对象(包含模型数据,逻辑视图名)
5).ModelAndView 的逻辑视图名-->ViewResolver,ViewResoler 将把逻辑视图名解析为具体的 View。
6).View-->渲染,View 会根据传进来的 Model 模型数据进行渲染,此处的 Model 实际是一个 Map 数据结构
7).返回控制权给 DispatcherServlet,由 DispatcherServlet 返回响应给用户。
23. 先上结论:重复性较强的字段,不适合添加索引。mysql 给离散度低的字段,比如性别设置索引,再以性别作为条件进行查询反而会更慢。
一个表可能会涉及两个数据结构(文件),一个是表本身,存放表中的数据,另一个是索引。索引是什么?它就是把一个或几个字段(组合索引)按规律排列起来,再附上该字段所在行数据的物理地址(位于表中)。比如我们有个字段是年龄,如果要选取某个年龄段的所有行,那么一般情况下可能需要进行一次全表扫描。但如果以这个年龄段建个索引,那么索引中会按年龄值根据特定数据结构建一个排列,这样在索引中就能迅速定位,不需要进行全表扫描。为什么性别不适合建索引呢?因为访问索引需要付出额外的 IO 开销,从索引中拿到的只是地址,要想真正访问到数据还是要对表进行一次 IO。假如你要从表的 100 万行数据中取几个数据,那么利用索引迅速定位,访问索引的这 IO 开销就非常值了。但如果是从 100 万行数据中取 50 万行数据,就比如性别字段,那你相对需要访问 50 万次索引,再访问 50 万次表,加起来的开销并不会比直接对表进行一次完整扫描小。
当然如果把性别字段设为表的聚集索引,那么就肯定能加快大约一半该字段的查询速度了。聚集索引指的是表本身数据按哪个字段的值来进行排序。因此,聚集索引只能有一个,而且使用聚集索引不会付出额外 IO 开销。当然你得能舍得把聚集索引这么宝贵资源用到性别字段上。
可以根据业务场景需要,将性别和其它字段建立联合索引,比如时间戳,但是建立索引记得把时间戳字段放在性别前面。
感谢各位知友,作者这段时间在构思写一个**Java 知识精要系列,**将 java 知识点全部串起来,适用于各个层次的 java 学习者,有兴趣可以关注下。
后加
jvm gc 复制算法是怎样实现的
注解的原理
进程间通信的方式有哪些
Reen
trantLock 是可重入锁,什么是可重入锁
线程执行过程中遇到异常会发生什么,怎样处理
HashMap put()元素产生冲突,为什么用 LinkedList(拉链法)而不用 ArrayList 解决,产生冲突时 key 值不等,新元素怎样加入链表,为什么这么设计(jdk1.8 之前)
双检锁写一个单例模式,为什么要用 volatile 修饰对象,Object object = new Object(); object 为 null 吗?为什么
Object object = new Object(); 初始化的顺序是什么在 jvm 各区域做了什么
线程怎样按顺序执行
true or false ?
ArrayList<Integer> list1 = new ArrayList<>();
ArrayList<String> list2 = new ArrayList<>();
System.out.print(list1.getClass() == list2.getClass);
11. 修饰类的锁和修饰方法的锁的区别
12. 下面代码中的 method()方法会互斥访问吗,为什么
评论