谈面试时从写一个单例开始究竟能问多深及终极解决方案

http://www.cnblogs.com/xiexj/p/6845029.html

看了左潇龙的《回答阿里社招面试如何准备,顺便谈谈对于 Java 程序猿学习当中各个阶段的建议》这篇文章,在想一个问题,从一个最简单的问题入手究竟能把问题问多深?下面就模拟一场面试问答,要是我是面试官,大概就只能问到下面的深度了。

旁白:一般的面试都是从最简单基本的问题开始。

面试官:请在黑板上写出一个线程安全的单例模式的例子。

面试者:

  其实线程安全的实现有很多种,根据业务场景可以 new 一个实例作为私有静态成员变量,这样程序一启动,实例就生成,私有化构造函数,利用公用的静态函数 getInstance 返回实例。这种预加载的是能保证线程安全的但是如果不是确定会被使用,会造成内存的浪费,所以可以将实例放到私有静态类中作为成员变量。下面只写一种利用锁机制来保证的懒加载方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton { 
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

或者

1
2
3
4
5
6
7
8
public class Singleton{    
private static Singletoninstance = new Singleton();
private Singleton(){}

public static Singleton getInstance() {
return instance;
}
}

旁白:从这个例子上我能想到的知识点主要有三个

   ☆ volatile 关键字,可深入到 Java VM 内存相关

   ☆ synchronized 关键字,可深入到 Java 锁机制,高并发相关

   ☆ new 关键字,可深入到 Java VM 类加载机制相关

但是面试官一开始可能要先考察一下面试者是否真的理解自己写的代码

面试官:你写的这个程序是怎么保证线程安全的?

面试者:将类的构造方法私有起来,外部调用进行初始化的时候只能通过调用 getSingleton 这个静态方法来获得实例,静态方法是整个 Java 虚拟机中只有一个实例。在创建的时候首先进行非空判断,这时候如果实例不存在,对整个类进行加锁同步,为了避免过程中非空状态的改变,同步块内再进行一次判断,如果不存在实例则创建实例返回。使用 volatile 关键字,下次访问这个方法就能直接看到实例的最新非空状态,直接返回实例。

面试官:volatile 起到了什么作用?

面试者:volatile 这个英文单词意思是易变的,用在多线程中来同步变量。Java 的对象都是在内存堆中分配空间。但是 Java 有主内存和线程自己独有的内存拷贝。对于没有 volatile 修饰的局部变量,线程在运行过程中访问的是工作内存中的变量值,其修改对于主内存不是立即可见。而 volatile 修饰的值在线程独有的工作内存中无副本,线程直接和主内存交互,修改对主内存立即可见。

面试官:synchronized 起到了什么作用?

面试者:锁定对象,限制当前对象只能被一个线程访问。

面试官:synchronized 里你传 Singleton.class 这个参数,起到什么作用,换成别的行不行?

面试者:对当前类加锁,使得这个代码块一次只能被一个线程访问。这里 Singleton.class 可以换成一个常量字符串或者自己定义一个内部静态 Object。

面试官:那传 Singleton.class,常量字符串,自己定义一个内部静态 Object 有区别吗?

面试者:因为这是一个静态方法,相当于一个概念上的类锁,所以在这里起到的效果是一样的。但是如果是原型模式,或者直接每个类都是 new 出来的,实例不同的话,在一个非静态方法里加这三种锁,这时是一个对象锁,因为 Singleton.class 或者是静态的一个 Object 或者是 JVM 只存一份的字符串常量,这些对象线程间是共享的,会对所有的实例的同步块都加同一把锁,每个实例访问到此对象的同步代码块都会被阻塞。但是如果这时 synchronized 的参数是 this,或者是内部 new 出来的一个内部非静态 Object,则各个实例拥有不同的锁,访问同一个代码相同同步块也是互不干扰。只有实例内部使用了同一个对象锁才会同步等待。

面试官:那你知道 synchronized 关键字实现同步的原理吗?

面试者:synchronized 在 Java 虚拟机中使用监视器锁来实现。每个对象都有一个监视器锁,当监视器锁被占用时就会处于锁定状态。

  线程执行一条叫 monitorenter 的指令来获取监视器锁的所有权。如果此监视器锁的进入数为 0,则线程进入并将进入数设置为 1,成为线程所有者。如果线程已经拥有该锁,因为是可重入锁,可以重新进入,则进入数加 1. 如果线程的监视器锁被其他线程占用,则阻塞直到此监视器锁的进入数为 0 时才能进入该锁。

  线程执行一条叫 monitorexit 的指令来释放所有权。执行 monitorexit 的必须是线程的所有者。每次执行此指令,线程进入数减 1,直到进入数为 0。监视器锁被释放。

面试官:你刚才提到的可重入锁是什么概念,有不可重入锁吗?

面试者:我说的可重入锁是广义的可重入锁,当然 jdk1.5 引入了 concurrent 包,里面有 Lock 接口,它有一个实现叫 ReentrantLock。广义的可重入锁也叫递归锁,是指同一线程外层函数获得锁之后,内层还可以再次获得此锁。可重入锁的设计是为了避免死锁。sun 的 corba 里的 mutex 互斥锁是一种不可重入锁的实现。自旋锁也是一种不可重入锁,本质上是一种忙等锁,CPU 一直循环执行 “测试并设置” 直到可用并取得该锁,在递归的调用该锁时必然会引起死锁。另外,如果锁占用时间较长,自旋锁会过多的占用 CPU 资源,这时使用基于睡眠原理来实现的锁更加合适。

面试官:你刚才提到了 concurrent 包,它里面有哪些锁的实现?

面试者:常用的有 ReentrantLock, 它是一种独占锁。ReadWriteLock 接口也是一个锁接口,和 Lock 接口是一种关联关系,它返回一个只读的 Lock 和只写的 Lock。读写分离,在没有写锁的情况下,读锁是无阻塞的,提高了执行效率,它是一种共享锁。ReadWriteLock 的实现类为 ReentrantReadWriteLock。ReentrantLock 和 ReentrantReadWriteLock 实现都依赖于 AbstractQueuedSynchronizer 这种抽象队列同步器。

面试官:锁还有其他维度的分类吗?

面试者:还可以分为公平锁和非公平锁。非公平锁是如果一个线程尝试获取锁时可以获取锁,就直接成功获取。公平锁则在锁被释放后将锁分配给等待队列队首的线程。

面试官:AQS 是什么?

面试者:AQS 是一个简单的框架,这个框架为同步状态的原子性管理,线程的阻塞和非阻塞以及排队提供了一种通用机制。表现为一个同步器,主要支持获取锁和释放锁。获取锁的时候如果是独占锁就有可能阻塞,如果是共享锁就有可能失败。如果是阻塞,线程就要进入阻塞队列,当状态变成可获得锁就修改状态,已进入阻塞队列的要从阻塞队列中移除。释放锁时修改状态位及唤醒其他被阻塞的线程。

AQS 本质是采用 CHL 模型完成了一个先进先出的队列。对于入队,采用 CAS 操作,每次比较尾节点是否一致,然后插入到尾节点中。对于出队列,因为每个节点缓存了一个状态位,不满足条件时自旋等待,直到满足条件时将头节点设置为下一个节点。

面试官:那知道这个队列的数据结构吗?

面试者:这个队列是用一个双向链表实现的。

面试官:你刚才提到 AQS 是一种通用机制,那它还有哪些应用?

面试者:AQS 除了刚才提到的可重入锁 ReentrantLock 和 ReentrantReadWriteLock 之外,还用于不可重入锁 Mutex 的实现。java 并发包中的同步器如:Semphore,CountDownLatch,FutureTask,CyclicBarrier 都是采用这个机制实现的。

旁白:既然问到了并发工具包中的东西,每个都可以引出一堆,但是基本原理已经问出来了,其他的问下去没什么意思。转向下一个问题。

面试官:你黑板上写的实例是通过 new 对象创建出来的,还可不可以采用别的方法来创建对象呢?

面试者:还可以使用 class 类的 newInstance 方法,Constructor 构造器类的 newInstance 方法,克隆方法和反序列法方法。

面试官:两种 newInstance 方法有没有区别?

面试者:

  ☆ Class 类位于 java 的 lang 包中,而构造器类是 java 反射机制的一部分。

  ☆ Class 类的 newInstance 只能触发无参数的构造方法创建对象,而构造器类的 newInstance 能触发有参数或者任意参数的构造方法来创建对象。

  ☆ Class 类的 newInstance 需要其构造方法是共有的或者对调用方法可见的,而构造器类的 newInstance 可以在特定环境下调用私有构造方法来创建对象。

  ☆ Class 类的 newInstance 抛出类构造函数的异常,而构造器类的 newInstance 包装了一个 InvocationTargetException 异常。

  Class 类本质上调用了反射包构造器类中无参数的 newInstance 方法,捕获了 InvocationTargetException,将构造器本身的异常抛出。

面试官:类加载的时候,自己定义了一个类和 java 自己的类类名和命名空间都一样,JVM 加载的是哪一个呢?

面试者:调用的是 java 自身的,根据双亲委派模型,最委派 Bootstrap 的 ClassLoader 来加载,找不到才去使用 Extension 的 ClassLoader,还找不到才去用 Application 的 ClassLoader,这种机制利于保证 JVM 的安全。

面试官:你刚才提到的 java 的反射机制是什么概念?

面试者:java 的反射机制是在运行状态中,对于任何一个类,都能够知道它所有的属性和方法;对于任何一个对象,都能够调用它的任何一个方法和属性。这种动态的获取信息和动态调用对象的方法的功能就是 java 的反射机制。它是 jdk 动态代理的实现方法。

面试官:java 还有没有其他的动态代理实现?

面试者:还有 cglib 动态代理。

面试官:这两种动态代理哪个比较好呢?

面试者:AOP 源码中同时使用了这两种动态代理,因为他们各有优劣。jdk 动态代理是利用 java 内部的反射机制来实现,在生成类的过程中比较高效,cglib 动态代理则是借助 asm 来实现,可以利用 asm 将生成的类进行缓存,所以在类生成之后的相关执行过程中比较高效。但是 jdk 的动态代理前提是目标类必须基于统一的接口,所以有一定的局限性。

旁白:面试者都已经提到 AOP 了,那么接下来横向,纵向,怎样都能问出一大堆问题,就不赘述。基于上面问题,读者也可以自己画出一棵知识树,然后就能找到能对答如流的终极方案:就是基本都没超过《深入理解 java 虚拟器》《java 并发编程实践》这两本书,大学学过的《数据结构与算法》《编译原理》掌握的好也可以在面试中加分哦。

多谢支持